#!/bin/sh # Bananarchy Dual-SIM & QMI UIM Provisioning Helper for Qualcomm MSM8905/MSM8909 set -eu CONF_FILE="/etc/conf.d/msm-modem-uim-selection" if [ -r "$CONF_FILE" ]; then # shellcheck disable=SC1090 . "$CONF_FILE" fi SIM_WAIT_TIME="${SIM_WAIT_TIME:-${sim_wait_time:-4}}" PREFERRED_SIM_SLOT="${PREFERRED_SIM_SLOT:-${preferred_sim_slot:-1}}" QMICLI_MODEM="" ensure_usb_network_gadget() { CONFIGFS="/sys/kernel/config/usb_gadget" if ! [ -d "$CONFIGFS" ]; then modprobe libcomposite 2>/dev/null || true mount -t configfs configfs /sys/kernel/config 2>/dev/null || true fi udc_dev="$(ls /sys/class/udc 2>/dev/null | head -n 1 || true)" if [ -z "$udc_dev" ]; then modprobe pm8916-lbc 2>/dev/null || true w=0 while [ -z "$udc_dev" ] && [ "$w" -lt 20 ]; do sleep 0.1 udc_dev="$(ls /sys/class/udc 2>/dev/null | head -n 1 || true)" w=$((w + 1)) done fi if [ -d "$CONFIGFS" ] && [ -n "$udc_dev" ]; then if ! [ -d "$CONFIGFS/g1" ]; then mkdir -p "$CONFIGFS/g1" 2>/dev/null || true echo "0x18D1" > "$CONFIGFS/g1/idVendor" 2>/dev/null || true echo "0xD001" > "$CONFIGFS/g1/idProduct" 2>/dev/null || true mkdir -p "$CONFIGFS/g1/strings/0x409" 2>/dev/null || true echo "Nokia" > "$CONFIGFS/g1/strings/0x409/manufacturer" 2>/dev/null || true echo "Bananarchy" > "$CONFIGFS/g1/strings/0x409/serialnumber" 2>/dev/null || true echo "Nokia 8110 4G" > "$CONFIGFS/g1/strings/0x409/product" 2>/dev/null || true func="ncm.usb0" if ! mkdir -p "$CONFIGFS/g1/functions/$func" 2>/dev/null; then func="rndis.usb0" mkdir -p "$CONFIGFS/g1/functions/$func" 2>/dev/null || true fi mkdir -p "$CONFIGFS/g1/configs/c.1/strings/0x409" 2>/dev/null || true echo "USB network" > "$CONFIGFS/g1/configs/c.1/strings/0x409/configuration" 2>/dev/null || true ln -sf "$CONFIGFS/g1/functions/$func" "$CONFIGFS/g1/configs/c.1/" 2>/dev/null || true fi cur_udc="$(cat "$CONFIGFS/g1/UDC" 2>/dev/null | tr -d '[:space:]' || true)" if [ -z "$cur_udc" ]; then echo "$udc_dev" > "$CONFIGFS/g1/UDC" 2>/dev/null || true fi fi if ip link show dev usb0 >/dev/null 2>&1; then ip link set usb0 up 2>/dev/null || true ip -4 addr show dev usb0 2>/dev/null | grep -q "172.16.42.1" || \ ip addr add 172.16.42.1/16 dev usb0 2>/dev/null || true if ! pidof unudhcpd >/dev/null 2>&1; then (unudhcpd -i usb0 -s 172.16.42.1 -c 172.16.42.2 >/dev/null 2>&1) & fi fi } detect_qmicli_modem() { if ! command -v qmicli >/dev/null 2>&1; then return 1 fi if [ -e "/dev/wwan0qmi0" ]; then if timeout 3 qmicli --silent -pd /dev/wwan0qmi0 --device-open-qmi --uim-noop >/dev/null 2>&1; then QMICLI_MODEM="timeout 4 qmicli --silent -pd /dev/wwan0qmi0 --device-open-qmi" return 0 elif timeout 3 qmicli --silent -d /dev/wwan0qmi0 --device-open-qmi --uim-noop >/dev/null 2>&1; then QMICLI_MODEM="timeout 4 qmicli --silent -d /dev/wwan0qmi0 --device-open-qmi" return 0 fi fi 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" return 0 elif timeout 3 qmicli --silent -d /dev/modem --uim-noop >/dev/null 2>&1; then QMICLI_MODEM="timeout 4 qmicli --silent -d /dev/modem" return 0 fi fi return 1 } wait_for_qmicli_modem() { max_wait="${1:-12}" count=0 while [ "$count" -lt "$max_wait" ]; do if detect_qmicli_modem; then return 0 fi sleep 1 count=$((count + 1)) done return 1 } # Parse `qmicli --uim-get-card-status` into shell-safe KEY=VALUE lines parse_uim_card_status() { raw_status="$1" pref_slot="$2" printf "%s\n" "$raw_status" | awk -v pref="$pref_slot" ' BEGIN { prim_slot = 0 prim_app = 0 sec_slot = 0 sec_app = 0 cur_slot = 0 in_app = 0 for (s = 1; s <= 2; s++) { cstate[s] = "absent" rawstate[s] = "absent" upin_state[s] = "not-initialized" upin_ret[s] = 0 upuk_ret[s] = 0 app_type[s] = "none" app_state[s] = "none" aid[s] = "" pin1_state[s] = "not-initialized" pin1_ret[s] = 0 puk1_ret[s] = 0 want_aid[s] = 0 need_aid_line[s] = 0 } } /Primary GW:/ { if ($0 ~ /slot \x27[0-9]+\x27/) { line = $0 sub(/.*slot \x27/, "", line) sub(/\x27.*/, "", line) prim_slot = line + 0 } if ($0 ~ /application \x27[0-9]+\x27/) { line = $0 sub(/.*application \x27/, "", line) sub(/\x27.*/, "", line) prim_app = line + 0 } } /Secondary GW:/ { if ($0 ~ /slot \x27[0-9]+\x27/) { line = $0 sub(/.*slot \x27/, "", line) sub(/\x27.*/, "", line) sec_slot = line + 0 } if ($0 ~ /application \x27[0-9]+\x27/) { line = $0 sub(/.*application \x27/, "", line) sub(/\x27.*/, "", line) sec_app = line + 0 } } /^[[:space:]]*Slot \[[0-9]+\]:/ { line = $0 sub(/.*Slot \[/, "", line) sub(/\].*/, "", line) cur_slot = line + 0 in_app = 0 next } cur_slot >= 1 && cur_slot <= 2 { if ($0 ~ /^[[:space:]]*Application \[[0-9]+\]:/) { in_app = 1 next } if (!in_app && $0 ~ /Card state:/) { line = $0 sub(/.*Card state:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) rawstate[cur_slot] = line if (line == "present") cstate[cur_slot] = "present" else if (line == "absent" || line ~ /no-atr-received/) cstate[cur_slot] = "absent" else cstate[cur_slot] = "error" } if (!in_app && $0 ~ /UPIN state:/) { line = $0 sub(/.*UPIN state:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) upin_state[cur_slot] = line } if (!in_app && $0 ~ /UPIN retries:/) { line = $0 sub(/.*UPIN retries:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) upin_ret[cur_slot] = line + 0 } if (!in_app && $0 ~ /UPUK retries:/) { line = $0 sub(/.*UPUK retries:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) upuk_ret[cur_slot] = line + 0 } if (in_app && $0 ~ /Application type:/) { line = $0 sub(/.*Application type:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) if (line ~ /^usim/) { app_type[cur_slot] = "usim" want_aid[cur_slot] = 2 } else if (line ~ /^sim/ && app_type[cur_slot] != "usim") { app_type[cur_slot] = "sim" want_aid[cur_slot] = 1 } else if (app_type[cur_slot] == "none") { app_type[cur_slot] = "sim" want_aid[cur_slot] = 1 } else { want_aid[cur_slot] = 0 } } if (in_app && $0 ~ /Application state:/ && want_aid[cur_slot] > 0) { line = $0 sub(/.*Application state:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) app_state[cur_slot] = line } if (in_app && $0 ~ /Application ID:/ && want_aid[cur_slot] > 0) { need_aid_line[cur_slot] = 1 next } if (in_app && need_aid_line[cur_slot] == 1) { line = $0 gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) if (line != "" && line !~ /:.*\x27/) { aid[cur_slot] = line } need_aid_line[cur_slot] = 0 } if (in_app && $0 ~ /PIN1 state:/ && want_aid[cur_slot] > 0) { line = $0 sub(/.*PIN1 state:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) pin1_state[cur_slot] = line } if (in_app && $0 ~ /PIN1 retries:/ && want_aid[cur_slot] > 0) { line = $0 sub(/.*PIN1 retries:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) pin1_ret[cur_slot] = line + 0 } if (in_app && $0 ~ /PUK1 retries:/ && want_aid[cur_slot] > 0) { line = $0 sub(/.*PUK1 retries:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) puk1_ret[cur_slot] = line + 0 } } END { printf "PREFERRED_SLOT=%d\n", (pref == 2 ? 2 : 1) printf "PRIMARY_GW_SLOT=%d\n", prim_slot printf "PRIMARY_GW_APP=%d\n", prim_app printf "SECONDARY_GW_SLOT=%d\n", sec_slot printf "SECONDARY_GW_APP=%d\n", sec_app for (s = 1; s <= 2; s++) { gsub(/[^A-Za-z0-9_.:-]/, "_", cstate[s]) gsub(/[^A-Za-z0-9_.:-]/, "_", rawstate[s]) gsub(/[^A-Za-z0-9_.:-]/, "_", app_type[s]) gsub(/[^A-Za-z0-9_.:-]/, "_", app_state[s]) gsub(/[^A-Za-z0-9_.:-]/, "", aid[s]) gsub(/[^A-Za-z0-9_.:-]/, "_", pin1_state[s]) gsub(/[^A-Za-z0-9_.:-]/, "_", upin_state[s]) printf "SLOT%d_STATE=%s\n", s, cstate[s] printf "SLOT%d_RAW_STATE=%s\n", s, rawstate[s] printf "SLOT%d_APP_TYPE=%s\n", s, app_type[s] printf "SLOT%d_APP_STATE=%s\n", s, app_state[s] printf "SLOT%d_AID=%s\n", s, aid[s] printf "SLOT%d_PIN1_STATE=%s\n", s, pin1_state[s] printf "SLOT%d_PIN1_RETRIES=%d\n", s, pin1_ret[s] printf "SLOT%d_PUK1_RETRIES=%d\n", s, puk1_ret[s] printf "SLOT%d_UPIN_STATE=%s\n", s, upin_state[s] printf "SLOT%d_UPIN_RETRIES=%d\n", s, upin_ret[s] printf "SLOT%d_UPUK_RETRIES=%d\n", s, upuk_ret[s] } } ' } parse_nas_serving_system() { raw_nas="$1" printf "%s\n" "$raw_nas" | awk ' BEGIN { reg_state = "unknown" cs_state = "unknown" ps_state = "unknown" rat = "" mcc = "" mnc = "" op_desc = "" roam = "off" in_rat = 0 in_plmn = 0 in_roam = 0 } /^[[:space:]]*Registration state:/ { line = $0 sub(/.*Registration state:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) reg_state = line in_rat = 0; in_plmn = 0; in_roam = 0 next } /^[[:space:]]*CS:/ { line = $0 sub(/.*CS:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) cs_state = line next } /^[[:space:]]*PS:/ { line = $0 sub(/.*PS:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) ps_state = line next } /^[[:space:]]*Radio interfaces:/ { in_rat = 1; in_plmn = 0; in_roam = 0 next } /^[[:space:]]*Data service capabilities:/ { in_rat = 0 next } /^[[:space:]]*Current PLMN:/ { in_plmn = 1; in_rat = 0; in_roam = 0 next } /^[[:space:]]*Roaming indicators:/ { in_roam = 1; in_plmn = 0; in_rat = 0 next } /^[[:space:]]*Default roaming status:/ { line = $0 sub(/.*Default roaming status:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) roam = line in_roam = 0 next } in_rat && /^[[:space:]]*\[[0-9]+\]:/ { line = $0 sub(/.*\[[0-9]+\]:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) if (line != "" && line != "none" && rat == "") rat = line } in_plmn && /MCC:/ { line = $0 sub(/.*MCC:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) mcc = line } in_plmn && /MNC:/ { line = $0 sub(/.*MNC:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) mnc = line } in_plmn && /Description:/ { line = $0 sub(/.*Description:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) gsub(/[\r\n\t]/, " ", line) op_desc = line in_plmn = 0 } in_roam && /^[[:space:]]*\[[0-9]+\]:/ { line = $0 sub(/.*\[[0-9]+\]:[[:space:]]*\x27/, "", line) sub(/\x27.*/, "", line) if (line == "on") roam = "on" } END { printf "NAS_REG_STATE=%s\n", reg_state printf "NAS_CS_STATE=%s\n", cs_state printf "NAS_PS_STATE=%s\n", ps_state printf "NAS_RAT=%s\n", rat printf "NAS_RADIO=%s\n", rat printf "NAS_MCC=%s\n", mcc printf "NAS_MNC=%s\n", mnc printf "NAS_OPERATOR=%s\n", op_desc printf "NAS_ROAMING=%s\n", roam } ' } save_preferred_slot() { target_slot="$1" PREFERRED_SIM_SLOT="$target_slot" if [ -f "$CONF_FILE" ]; then if grep -q "^preferred_sim_slot=" "$CONF_FILE"; then sed -i "s/^preferred_sim_slot=.*/preferred_sim_slot=$target_slot/" "$CONF_FILE" else printf "\npreferred_sim_slot=%s\n" "$target_slot" >> "$CONF_FILE" fi else printf "sim_wait_time=10\npreferred_sim_slot=%s\n" "$target_slot" > "$CONF_FILE" fi } provision_slot() { req_slot="$1" wait_secs="$2" if ! wait_for_qmicli_modem "$wait_secs"; then echo "No modem available." >&2 return 2 fi QMI_CARDS="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)" count=0 while : ; do if [ -n "$QMI_CARDS" ]; then eval "$(parse_uim_card_status "$QMI_CARDS" "$req_slot")" if [ "$req_slot" = "2" ] && [ "$SLOT2_STATE" = "present" ] && [ "$SLOT2_APP_TYPE" != "none" ]; then break elif [ "$req_slot" != "2" ] && [ "$SLOT1_STATE" = "present" ] && [ "$SLOT1_APP_TYPE" != "none" ]; then break elif [ "$count" -ge 3 ] && { { [ "$SLOT1_STATE" = "present" ] && [ "$SLOT1_APP_TYPE" != "none" ]; } || { [ "$SLOT2_STATE" = "present" ] && [ "$SLOT2_APP_TYPE" != "none" ]; }; }; then break fi fi if [ "$count" -ge "$SIM_WAIT_TIME" ]; then break fi sleep 1 count=$((count + 1)) QMI_CARDS="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)" done if [ -z "$QMI_CARDS" ]; then echo "No SIM card status returned." return 0 fi eval "$(parse_uim_card_status "$QMI_CARDS" "$req_slot")" chosen_slot="" chosen_aid="" if [ "$req_slot" = "2" ]; then if [ "$SLOT2_STATE" = "present" ]; then chosen_slot="2" chosen_aid="$SLOT2_AID" elif [ "$SLOT1_STATE" = "present" ]; then chosen_slot="1" chosen_aid="$SLOT1_AID" fi else if [ "$SLOT1_STATE" = "present" ]; then chosen_slot="1" chosen_aid="$SLOT1_AID" elif [ "$SLOT2_STATE" = "present" ]; then chosen_slot="2" chosen_aid="$SLOT2_AID" fi fi if [ -z "$chosen_slot" ]; then echo "No SIM detected after $SIM_WAIT_TIME seconds." return 0 fi if [ "$chosen_slot" != "$PREFERRED_SIM_SLOT" ]; then save_preferred_slot "$chosen_slot" fi # If already provisioned on the chosen slot and application is ready or waiting for PIN, keep session intact eval "chosen_app_state=\$SLOT${chosen_slot}_APP_STATE" if [ "$PRIMARY_GW_SLOT" = "$chosen_slot" ] && { [ "$chosen_app_state" = "ready" ] || [ "$chosen_app_state" = "pin1-or-upin-pin-required" ] || [ "$chosen_app_state" = "puk1-or-upin-puk-required" ]; }; then echo "Slot $chosen_slot already bound to primary-gw-provisioning ($chosen_app_state)." return 0 fi if [ "${SECONDARY_GW_SLOT:-0}" = "$chosen_slot" ]; then $QMICLI_MODEM --uim-change-provisioning-session='activate=no,session-type=secondary-gw-provisioning' >/dev/null 2>&1 || true fi if [ "$PRIMARY_GW_SLOT" != "0" ]; then $QMICLI_MODEM --uim-change-provisioning-session='activate=no,session-type=primary-gw-provisioning' >/dev/null 2>&1 || true fi if [ -n "$chosen_aid" ]; then echo "Selecting AID $chosen_aid on slot $chosen_slot" if ! $QMICLI_MODEM --uim-change-provisioning-session="slot=$chosen_slot,activate=yes,session-type=primary-gw-provisioning,aid=$chosen_aid" >/dev/null 2>&1; then $QMICLI_MODEM --uim-change-provisioning-session="slot=$chosen_slot,activate=yes,session-type=primary-gw-provisioning" >/dev/null 2>&1 || true fi else echo "Selecting slot $chosen_slot" $QMICLI_MODEM --uim-change-provisioning-session="slot=$chosen_slot,activate=yes,session-type=primary-gw-provisioning" >/dev/null 2>&1 || true fi return 0 } prepare_slot_for_pin_op() { req_slot="${1:-1}" slot="$req_slot" target_aid="" if detect_qmicli_modem; then QMI_CARDS="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)" if [ -n "$QMI_CARDS" ]; then eval "$(parse_uim_card_status "$QMI_CARDS" "$PREFERRED_SIM_SLOT")" if [ "$slot" = "1" ] && [ "$SLOT1_STATE" != "present" ] && [ "$SLOT2_STATE" = "present" ]; then slot="2" elif [ "$slot" = "2" ] && [ "$SLOT2_STATE" != "present" ] && [ "$SLOT1_STATE" = "present" ]; then slot="1" fi eval "target_aid=\$SLOT${slot}_AID" if [ "$PRIMARY_GW_SLOT" != "$slot" ]; then if [ "${SECONDARY_GW_SLOT:-0}" = "$slot" ]; then $QMICLI_MODEM --uim-change-provisioning-session='activate=no,session-type=secondary-gw-provisioning' >/dev/null 2>&1 || true fi if [ "$PRIMARY_GW_SLOT" != "0" ]; then $QMICLI_MODEM --uim-change-provisioning-session='activate=no,session-type=primary-gw-provisioning' >/dev/null 2>&1 || true fi if [ -n "$target_aid" ]; then $QMICLI_MODEM --uim-change-provisioning-session="slot=$slot,activate=yes,session-type=primary-gw-provisioning,aid=$target_aid" >/dev/null 2>&1 || \ $QMICLI_MODEM --uim-change-provisioning-session="slot=$slot,activate=yes,session-type=primary-gw-provisioning" >/dev/null 2>&1 || true else $QMICLI_MODEM --uim-change-provisioning-session="slot=$slot,activate=yes,session-type=primary-gw-provisioning" >/dev/null 2>&1 || true fi sleep 0.5 fi fi fi } restart_modemmanager_clean() { if command -v rc-service >/dev/null 2>&1; then timeout 4 rc-service polkit start >/dev/null 2>&1 || true timeout 6 rc-service modemmanager restart >/dev/null 2>&1 || { killall -q ModemManager 2>/dev/null || true sleep 0.3 timeout 6 rc-service modemmanager zap >/dev/null 2>&1 || true timeout 6 rc-service modemmanager start >/dev/null 2>&1 || true } elif command -v systemctl >/dev/null 2>&1; then timeout 6 systemctl restart ModemManager >/dev/null 2>&1 || true fi } enable_and_register_modem() { target_slot="${1:-$PREFERRED_SIM_SLOT}" force_unlock="${2:-0}" if detect_qmicli_modem; then $QMICLI_MODEM --dms-set-operating-mode=online >/dev/null 2>&1 || true $QMICLI_MODEM --nas-initiate-network-register >/dev/null 2>&1 || true fi i=0 mm_state="" while [ "$i" -lt 8 ]; do mm_state="$(timeout 3 mmcli -m any -K 2>/dev/null | awk -F': ' '/^modem\.generic\.state / {print $2; exit}' || true)" if [ -n "$mm_state" ]; then break fi sleep 1 i=$((i + 1)) done if [ -z "$mm_state" ] || [ "$mm_state" = "failed" ] || { [ "$force_unlock" = "1" ] && [ "$mm_state" = "locked" ]; }; then restart_modemmanager_clean i=0 mm_state="" while [ "$i" -lt 10 ]; do mm_state="$(timeout 3 mmcli -m any -K 2>/dev/null | awk -F': ' '/^modem\.generic\.state / {print $2; exit}' || true)" if [ -n "$mm_state" ] && [ "$mm_state" != "failed" ]; then break fi sleep 1 i=$((i + 1)) done fi if [ -z "$mm_state" ] || [ "$mm_state" = "locked" ]; then return 0 fi i=0 while [ "$i" -lt 8 ]; do mm_state="$(timeout 3 mmcli -m any -K 2>/dev/null | awk -F': ' '/^modem\.generic\.state / {print $2; exit}' || true)" case "$mm_state" in disabled) timeout 4 mmcli -m any -e >/dev/null 2>&1 || true ;; enabled) timeout 4 mmcli -m any --3gpp-register-home >/dev/null 2>&1 || true break ;; searching|registered|connected|connecting) break ;; locked) break ;; esac sleep 1 i=$((i + 1)) done timeout 4 mmcli -m any --3gpp-register-home >/dev/null 2>&1 || true if command -v sxmo_hook_statusbar.sh >/dev/null 2>&1; then DISPLAY="${DISPLAY:-:0}" timeout 3 sxmo_hook_statusbar.sh modem >/dev/null 2>&1 || true fi } spawn_enable_and_register() { s_slot="${1:-$PREFERRED_SIM_SLOT}" s_force="${2:-0}" ( exec >/dev/null 2>&1 &- 4>&- 5>&- 6>&- 7>&- 8>&- 9>&- 2>/dev/null || true enable_and_register_modem "$s_slot" "$s_force" ) & } cmd="${1:-boot}" case "$cmd" in boot) ensure_usb_network_gadget if command -v systemd-notify >/dev/null 2>&1; then trap '[ $? -eq 0 ] && systemd-notify --ready' EXIT fi case "$(cat /sys/devices/soc0/machine 2>/dev/null || true)" in APQ*) echo "Skipping SIM configuration on APQ SoC." exit 0 ;; esac provision_slot "$PREFERRED_SIM_SLOT" 10 || true exit 0 ;; status) QMI_CARDS="" if command -v qmicli >/dev/null 2>&1; then if [ -e "/dev/wwan0qmi0" ]; then QMI_CARDS="$(timeout 3 qmicli --silent -pd /dev/wwan0qmi0 --device-open-qmi --uim-get-card-status 2>/dev/null || true)" if [ -n "$QMI_CARDS" ]; then QMICLI_MODEM="timeout 3 qmicli --silent -pd /dev/wwan0qmi0 --device-open-qmi" fi fi if [ -z "$QMI_CARDS" ] && detect_qmicli_modem; then QMI_CARDS="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)" fi fi if [ -z "$QMI_CARDS" ]; then echo "MODEM_AVAILABLE=0" echo "PREFERRED_SLOT=$PREFERRED_SIM_SLOT" exit 0 fi echo "MODEM_AVAILABLE=1" parse_uim_card_status "$QMI_CARDS" "$PREFERRED_SIM_SLOT" QMI_NAS="$($QMICLI_MODEM --nas-get-serving-system 2>/dev/null || true)" if [ -n "$QMI_NAS" ]; then parse_nas_serving_system "$QMI_NAS" fi exit 0 ;; switch) target="${2:-1}" if [ "$target" != "1" ] && [ "$target" != "2" ]; then echo "Invalid slot: $target (must be 1 or 2)" >&2 exit 1 fi save_preferred_slot "$target" SIM_WAIT_TIME=3 provision_slot "$target" 5 restart_modemmanager_clean spawn_enable_and_register "$target" 0 exit 0 ;; verify-pin) slot="${2:-1}" pin="${3:-}" if [ -z "$pin" ]; then echo "Missing PIN" >&2 exit 1 fi prepare_slot_for_pin_op "$slot" eval "before_ret=\${SLOT${slot}_PIN1_RETRIES:-3}" verified=0 wrong_pin=0 sim_id="$(timeout 3 mmcli -m any 2>/dev/null | grep -oE 'SIM/[0-9]+' | head -n1 | cut -d'/' -f2 || true)" if [ -n "$sim_id" ]; then mm_pin_out="$(timeout 5 mmcli -i "$sim_id" --pin="$pin" 2>&1)" && verified=1 || { if printf "%s\n" "$mm_pin_out" | grep -qiE "IncorrectPin|SimPin|SimPuk|PinBlocked|incorrect"; then wrong_pin=1 fi } fi if [ "$verified" = "0" ] && [ "$wrong_pin" = "0" ] && [ -n "$QMICLI_MODEM" ]; then qmi_pin_out="$($QMICLI_MODEM --uim-verify-pin="PIN1,$pin,session-type=primary-gw-provisioning" 2>&1)" && verified=1 || { if printf "%s\n" "$qmi_pin_out" | grep -qiE "IncorrectPin|PinBlocked|Blocked"; then wrong_pin=1 fi } if [ "$verified" = "0" ] && [ "$wrong_pin" = "0" ]; then qmi_pin_out="$($QMICLI_MODEM --uim-verify-pin="PIN1,$pin,session-type=card-slot-$slot" 2>&1)" && verified=1 || { if printf "%s\n" "$qmi_pin_out" | grep -qiE "IncorrectPin|PinBlocked|Blocked"; then wrong_pin=1 fi } fi fi if [ "$verified" = "0" ] && [ -n "$QMICLI_MODEM" ]; then QMI_CARDS_AFTER="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)" if [ -n "$QMI_CARDS_AFTER" ]; then eval "$(parse_uim_card_status "$QMI_CARDS_AFTER" "$slot")" eval "after_pin1=\$SLOT${slot}_PIN1_STATE" eval "after_app=\$SLOT${slot}_APP_STATE" eval "after_ret=\$SLOT${slot}_PIN1_RETRIES" if [ "$after_pin1" = "enabled-verified" ] || [ "$after_pin1" = "disabled" ] || [ "$after_app" = "ready" ]; then verified=1 elif [ "$after_ret" -lt "$before_ret" ] 2>/dev/null; then wrong_pin=1 fi fi fi if [ "$verified" = "0" ] && [ "$wrong_pin" = "0" ]; then at_cpin="$("$0" at auto "AT+CPIN=\"$pin\"" 2>/dev/null || true)" if printf "%s\n" "$at_cpin" | grep -qE "(^|[[:space:]])OK([[:space:]]|$)"; then verified=1 fi fi if [ "$verified" = "1" ]; then save_preferred_slot "$slot" SIM_WAIT_TIME=2 provision_slot "$slot" 3 >/dev/null 2>&1 || true timeout 4 mmcli -m any -e >/dev/null 2>&1 || true spawn_enable_and_register "$slot" 1 exit 0 fi exit 1 ;; unblock-puk) slot="${2:-1}" puk="${3:-}" newpin="${4:-}" if [ -z "$puk" ] || [ -z "$newpin" ]; then echo "Missing PUK or new PIN" >&2 exit 1 fi prepare_slot_for_pin_op "$slot" unblocked=0 sim_id="$(timeout 3 mmcli -m any 2>/dev/null | grep -oE 'SIM/[0-9]+' | head -n1 | cut -d'/' -f2 || true)" if [ -n "$sim_id" ] && timeout 4 mmcli -i "$sim_id" --puk="$puk" --pin="$newpin" >/dev/null 2>&1; then unblocked=1 fi if [ "$unblocked" = "0" ] && [ -n "$QMICLI_MODEM" ]; then if $QMICLI_MODEM --uim-unblock-pin="PIN1,$puk,$newpin,session-type=primary-gw-provisioning" >/dev/null 2>&1; then unblocked=1 elif $QMICLI_MODEM --uim-unblock-pin="PIN1,$puk,$newpin,session-type=card-slot-$slot" >/dev/null 2>&1; then unblocked=1 elif $QMICLI_MODEM --uim-unblock-pin="PIN1,$puk,$newpin" >/dev/null 2>&1; then unblocked=1 elif $QMICLI_MODEM --dms-uim-unblock-pin="PIN,$puk,$newpin" >/dev/null 2>&1; then unblocked=1 fi fi if [ "$unblocked" = "0" ]; then at_cpin="$("$0" at auto "AT+CPIN=\"$puk\",\"$newpin\"" 2>/dev/null || true)" if printf "%s\n" "$at_cpin" | grep -qE "(^|[[:space:]])OK([[:space:]]|$)"; then unblocked=1 fi fi if [ "$unblocked" = "1" ]; then save_preferred_slot "$slot" SIM_WAIT_TIME=2 provision_slot "$slot" 3 >/dev/null 2>&1 || true timeout 4 mmcli -m any -e >/dev/null 2>&1 || true spawn_enable_and_register "$slot" 1 exit 0 fi exit 1 ;; set-pin-lock) slot="${2:-1}" mode="${3:-enable}" pin="${4:-}" if [ -z "$pin" ] || { [ "$mode" != "enable" ] && [ "$mode" != "disable" ]; }; then echo "Usage: $0 set-pin-lock <1|2> " >&2 exit 1 fi prepare_slot_for_pin_op "$slot" if [ -n "$QMICLI_MODEM" ]; then if $QMICLI_MODEM --uim-set-pin-protection="PIN1,$mode,$pin,session-type=primary-gw-provisioning" >/dev/null 2>&1; then exit 0 fi if $QMICLI_MODEM --uim-set-pin-protection="PIN1,$mode,$pin,session-type=card-slot-$slot" >/dev/null 2>&1; then exit 0 fi if $QMICLI_MODEM --dms-uim-set-pin-protection="PIN,$mode,$pin" >/dev/null 2>&1; then exit 0 fi fi sim_id="$(mmcli -m any 2>/dev/null | grep -oE 'SIM/[0-9]+' | head -n1 | cut -d'/' -f2 || true)" if [ -n "$sim_id" ]; then if [ "$mode" = "enable" ] && mmcli -i "$sim_id" --enable-pin --pin="$pin" >/dev/null 2>&1; then exit 0 elif [ "$mode" = "disable" ] && mmcli -i "$sim_id" --disable-pin --pin="$pin" >/dev/null 2>&1; then exit 0 fi fi exit 1 ;; change-pin) slot="${2:-1}" oldpin="${3:-}" newpin="${4:-}" if [ -z "$oldpin" ] || [ -z "$newpin" ]; then echo "Usage: $0 change-pin <1|2> " >&2 exit 1 fi prepare_slot_for_pin_op "$slot" if [ -n "$QMICLI_MODEM" ]; then if $QMICLI_MODEM --uim-change-pin="PIN1,$oldpin,$newpin,session-type=primary-gw-provisioning" >/dev/null 2>&1; then exit 0 fi if $QMICLI_MODEM --uim-change-pin="PIN1,$oldpin,$newpin,session-type=card-slot-$slot" >/dev/null 2>&1; then exit 0 fi if $QMICLI_MODEM --dms-uim-change-pin="PIN,$oldpin,$newpin" >/dev/null 2>&1; then exit 0 fi fi sim_id="$(mmcli -m any 2>/dev/null | grep -oE 'SIM/[0-9]+' | head -n1 | cut -d'/' -f2 || true)" if [ -n "$sim_id" ] && mmcli -i "$sim_id" --change-pin="$newpin" --pin="$oldpin" >/dev/null 2>&1; then exit 0 fi exit 1 ;; imei) imei1="" imei2="" imei_sv="" if detect_qmicli_modem; then dms_ids="$($QMICLI_MODEM --dms-get-ids 2>/dev/null || true)" imei1="$(printf "%s\n" "$dms_ids" | sed -n "s/.*IMEI: '\([0-9]\{14,16\}\)'/\1/p" | head -n1)" imei_sv="$(printf "%s\n" "$dms_ids" | sed -n "s/.*IMEI SV: '\([^']*\)'/\1/p" | head -n1)" fi if [ -z "$imei1" ]; then imei1="$(mmcli -m any -K 2>/dev/null | awk -F': ' '/modem\.3gpp\.imei/ {print $2; exit}')" fi # Query secondary AT channel (/dev/wwan0at1) if present if [ -c "/dev/wwan0at1" ]; then at_out="$(timeout 1 sh -c ' stty -F /dev/wwan0at1 raw -echo 115200 2>/dev/null || true (cat /dev/wwan0at1 & pid=$!; printf "AT+CGSN\r\n" > /dev/wwan0at1; sleep 0.25; kill $pid 2>/dev/null) 2>/dev/null ' 2>/dev/null | tr -d "\r" | grep -oE '^[0-9]{15}$' | head -n1 || true)" if [ -n "$at_out" ] && [ "$at_out" != "$imei1" ]; then imei2="$at_out" fi fi # Scan factory partitions for Luhn-valid 15-digit IMEIs (TAC 35...) if [ -z "$imei2" ]; then for part in /dev/disk/by-partlabel/traceability /dev/disk/by-partlabel/tunning /dev/disk/by-partlabel/devinfo /dev/disk/by-partlabel/oem; do [ -e "$part" ] || continue cand_list="$(strings -n 14 "$part" 2>/dev/null | grep -oE '35[0-9]{13}' | awk ' { val = $0 sum = 0 for (i = 1; i <= 14; i++) { d = substr(val, i, 1) + 0 if (i % 2 == 0) { d *= 2 if (d > 9) d -= 9 } sum += d } chk = (10 - (sum % 10)) % 10 if (chk == (substr(val, 15, 1) + 0)) print val } ' || true)" for c in $cand_list; do if [ -z "$imei1" ]; then imei1="$c" elif [ "$c" != "$imei1" ] && [ -z "$imei2" ]; then imei2="$c" break 2 fi done done fi echo "IMEI1=${imei1}" echo "IMEI2=${imei2}" echo "IMEI_SV=${imei_sv}" ;; usb-tether) mode="${2:-status}" case "$mode" in on) ensure_usb_network_gadget sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true nft add table ip nat 2>/dev/null || true nft 'add chain ip nat postrouting { type nat hook postrouting priority 100 ; }' 2>/dev/null || true nft add rule ip nat postrouting oifname "wwan*" masquerade 2>/dev/null || true nft add rule ip nat postrouting oifname "wlan*" masquerade 2>/dev/null || true ip addr show dev usb0 2>/dev/null | grep -q "172.16.42.1" || ip addr add 172.16.42.1/16 dev usb0 2>/dev/null || true ip link set usb0 up 2>/dev/null || true echo "1" > /tmp/bananarchy_usb_tether.active ;; off) ensure_usb_network_gadget sysctl -w net.ipv4.ip_forward=0 >/dev/null 2>&1 || true nft delete table ip nat 2>/dev/null || true nmcli connection delete USBNetworkShare >/dev/null 2>&1 || true ip addr show dev usb0 2>/dev/null | grep -q "172.16.42.1" || ip addr add 172.16.42.1/16 dev usb0 2>/dev/null || true ip link set usb0 up 2>/dev/null || true rm -f /tmp/bananarchy_usb_tether.active ;; *) if [ -f /tmp/bananarchy_usb_tether.active ] && [ "$(sysctl -n net.ipv4.ip_forward 2>/dev/null || echo 0)" = "1" ]; then echo "ACTIVE=1" else echo "ACTIVE=0" fi ;; esac ;; at) port="${2:-auto}" shift 2 2>/dev/null || true at_cmd="$*" if [ -z "$at_cmd" ]; then echo "ERROR: Empty AT command" exit 1 fi resp="" if [ "$PREFERRED_SIM_SLOT" = "2" ]; then at_candidates="$port /dev/wwan0at1 /dev/wwan0at0" else at_candidates="$port /dev/wwan0at0 /dev/wwan0at1" fi for candidate in $at_candidates; do case "$candidate" in wwan0at0|wwan0at1) dev="/dev/$candidate" ;; /dev/wwan0at0|/dev/wwan0at1) dev="$candidate" ;; *) dev="" ;; esac if [ -n "$dev" ] && [ -c "$dev" ]; then resp="$(timeout 3 sh -c ' dev="$1" cmd="$2" tmpf="/tmp/at_resp_$$.txt" rm -f "$tmpf" cat "$dev" > "$tmpf" 2>/dev/null & rpid=$! sleep 0.04 printf "%s\r\n" "$cmd" > "$dev" 2>/dev/null || true i=0 while [ "$i" -lt 16 ]; do if grep -qE "(^|[[:space:]])(OK|ERROR|\+CME ERROR|\+CMS ERROR)([[:space:]]|$)" "$tmpf" 2>/dev/null; then break fi sleep 0.08 i=$((i + 1)) done kill "$rpid" 2>/dev/null || true wait "$rpid" 2>/dev/null || true tr -d "\r" < "$tmpf" 2>/dev/null || true rm -f "$tmpf" ' _ "$dev" "$at_cmd" 2>/dev/null | sed '/^[[:space:]]*$/d' || true)" if [ -n "$resp" ]; then printf "%s\n" "$resp" exit 0 fi fi if [ "$port" != "auto" ] && [ "$candidate" = "$port" ]; then break fi done mm_out="$(mmcli -m any --command="$at_cmd" 2>&1 || true)" if [ -n "$mm_out" ]; then printf "%s\n" "$mm_out" | sed "s/^response: '\(.*\)'$/\1/" exit 0 fi echo "ERROR: No response from modem AT channel" exit 1 ;; ussd) ussd_mode="${2:-initiate}" shift 2 2>/dev/null || true ussd_code="$*" if [ "$ussd_mode" = "cancel" ]; then mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1 || true "$0" at auto 'AT+CUSD=2' >/dev/null 2>&1 || true echo "USSD session cancelled." exit 0 fi if [ -z "$ussd_code" ]; then echo "ERROR: Empty USSD code" exit 1 fi mm_state="$(mmcli -m any -K 2>/dev/null | awk -F': ' '$1 == "modem.generic.state" {print $2; exit}' || true)" if [ "$mm_state" = "disabled" ] || [ -z "$mm_state" ]; then mmcli -m any -e >/dev/null 2>&1 || true sleep 1 fi modem_dbus_path="$(mmcli -m any -K 2>/dev/null | awk -F': ' '$1 == "modem.dbus-path" {print $2; exit}' || true)" get_ussd_prop() { prop_name="$1" if [ -n "$modem_dbus_path" ] && command -v busctl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then busctl --json=short call org.freedesktop.ModemManager1 "$modem_dbus_path" \ org.freedesktop.DBus.Properties Get ss \ org.freedesktop.ModemManager1.Modem.Modem3gpp.Ussd "$prop_name" 2>/dev/null | \ jq -r '.data[].data // empty' 2>/dev/null || true fi } prev_notif="$(get_ussd_prop NetworkNotification)" prev_req="$(get_ussd_prop NetworkRequest)" ussd_st="$(mmcli -m any -K 2>/dev/null | awk -F': ' '$1 == "modem.3gpp.ussd.status" || $1 == "modem.3gpp.ussd.state" {print $2; exit}' || true)" if [ "$ussd_mode" = "respond" ] && [ "$ussd_st" != "user-response" ]; then case "$ussd_code" in \**\#|\#*\#) ussd_mode="initiate" ;; esac fi mm_rc=0 mm_out="" if [ "$ussd_mode" = "respond" ]; then mm_out="$(mmcli --timeout=30 -m any --3gpp-ussd-respond="$ussd_code" 2>&1)" || mm_rc=$? else if [ "$ussd_st" = "active" ] || [ "$ussd_st" = "user-response" ]; then mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1 || true fi mm_out="$(mmcli --timeout=30 -m any --3gpp-ussd-initiate="$ussd_code" 2>&1)" || mm_rc=$? fi if [ "$mm_rc" -eq 0 ] && [ -n "$mm_out" ]; then reply="$(printf "%s\n" "$mm_out" | sed \ -e "1s/^USSD session initiated; new reply from network: '//" \ -e "1s/^response successfully sent in USSD session; new reply from network: '//" \ -e "1s/^USSD response sent; new reply from network: '//" \ -e "\$s/'$//")" if [ -n "$reply" ]; then printf "%s\n" "$reply" exit 0 fi fi # Check if the USSD response arrived asynchronously via D-Bus properties w=0 while [ "$w" -lt 25 ]; do cur_req="$(get_ussd_prop NetworkRequest)" cur_notif="$(get_ussd_prop NetworkNotification)" if [ -n "$cur_req" ] && [ "$cur_req" != "$prev_req" ]; then printf "%s\n" "$cur_req" exit 0 fi if [ -n "$cur_notif" ] && [ "$cur_notif" != "$prev_notif" ]; then printf "%s\n" "$cur_notif" exit 0 fi cur_st="$(mmcli -m any -K 2>/dev/null | awk -F': ' '$1 == "modem.3gpp.ussd.status" || $1 == "modem.3gpp.ussd.state" {print $2; exit}' || true)" if [ "$cur_st" != "active" ] && [ "$w" -ge 8 ]; then if [ -n "$cur_req" ]; then printf "%s\n" "$cur_req" exit 0 fi if [ -n "$cur_notif" ]; then printf "%s\n" "$cur_notif" exit 0 fi break fi sleep 0.2 w=$((w + 1)) done # Fallback to direct AT+CUSD on the active SIM AT port if [ "$PREFERRED_SIM_SLOT" = "2" ]; then at_candidates="/dev/wwan0at1 /dev/wwan0at0" else at_candidates="/dev/wwan0at0 /dev/wwan0at1" fi for dev in $at_candidates; do [ -c "$dev" ] || continue cusd_out="$(timeout 12 sh -c ' dev="$1" code="$2" tmpf="/tmp/ussd_resp_$$.txt" rm -f "$tmpf" cat "$dev" > "$tmpf" 2>/dev/null & rpid=$! sleep 0.05 printf "AT+CSCS=\"GSM\";+CUSD=1,\"%s\",15\r\n" "$code" > "$dev" 2>/dev/null || true i=0 while [ "$i" -lt 80 ]; do if grep -qE "\+CUSD:[[:space:]]*[0-9]+" "$tmpf" 2>/dev/null; then break fi if grep -qE "(^|[[:space:]])(\+CME ERROR|\+CMS ERROR|ERROR)([[:space:]]|$)" "$tmpf" 2>/dev/null; then break fi sleep 0.1 i=$((i + 1)) done kill "$rpid" 2>/dev/null || true wait "$rpid" 2>/dev/null || true tr -d "\r" < "$tmpf" 2>/dev/null || true rm -f "$tmpf" ' _ "$dev" "$ussd_code" 2>/dev/null || true)" cusd_msg="$(printf "%s\n" "$cusd_out" | sed -n 's/.*+CUSD:[[:space:]]*[0-9]*,[[:space:]]*"\([^"]*\)".*/\1/p' | tail -n1)" if [ -n "$cusd_msg" ]; then printf "%s\n" "$cusd_msg" exit 0 fi done if [ -n "$mm_out" ]; then printf "%s\n" "$mm_out" else echo "ERROR: USSD request failed" fi exit 1 ;; sim-contacts-import) req_slot="${2:-auto}" if ! wait_for_qmicli_modem 3; then echo "ERROR: No modem available" exit 1 fi QMI_CARDS="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)" if [ -z "$QMI_CARDS" ]; then echo "ERROR: Cannot read SIM card status" exit 1 fi eval "$(parse_uim_card_status "$QMI_CARDS" "$PREFERRED_SIM_SLOT")" target_slot="" target_aid="" if [ "$req_slot" = "1" ]; then if [ "$SLOT1_STATE" != "present" ]; then echo "ERROR: SIM 1 is not inserted" exit 1 fi target_slot="1" target_aid="$SLOT1_AID" elif [ "$req_slot" = "2" ]; then if [ "$SLOT2_STATE" != "present" ]; then echo "ERROR: SIM 2 is not inserted" exit 1 fi target_slot="2" target_aid="$SLOT2_AID" else if [ "$PRIMARY_GW_SLOT" = "1" ] && [ "$SLOT1_STATE" = "present" ]; then target_slot="1" target_aid="$SLOT1_AID" elif [ "$PRIMARY_GW_SLOT" = "2" ] && [ "$SLOT2_STATE" = "present" ]; then target_slot="2" target_aid="$SLOT2_AID" elif [ "$SLOT1_STATE" = "present" ]; then target_slot="1" target_aid="$SLOT1_AID" elif [ "$SLOT2_STATE" = "present" ]; then target_slot="2" target_aid="$SLOT2_AID" else echo "ERROR: No SIM card detected" exit 1 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("= 20: cmd, srv, inst, srv_node, srv_port = struct.unpack("= 4: res, err = struct.unpack("= 9: _, _, _, rs, rc = struct.unpack("= 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("= 4: res, err = struct.unpack("= 2: clen = struct.unpack("= 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 orig_gw_aid="$SLOT1_AID" elif [ "$orig_gw_slot" = "2" ]; then orig_gw_aid="$SLOT2_AID" fi switched_gw=0 restore_gw_slot() { 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 EXIT INT TERM 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 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 fi fi # 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}')" reccount="$(printf "%s\n" "$adn_attr" | awk '/Record count:/ {gsub(/[^0-9]/, "", $NF); print $NF + 0; exit}')" if [ -n "$reclen" ] && [ -n "$reccount" ] && [ "$reclen" -ge 14 ] 2>/dev/null && [ "$reccount" -ge 1 ] 2>/dev/null; then if [ "$reccount" -gt 250 ]; then reccount=250 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)" if [ -n "$raw_rec" ]; then printf "%s\n" "$raw_rec" | awk ' function hex2val(h, i, c, v, res) { res = 0 h = toupper(h) for (i = 1; i <= length(h); i++) { c = substr(h, i, 1) v = index("0123456789ABCDEF", c) - 1 if (v < 0) return -1 res = res * 16 + v } return res } function bcd_digit(n) { if (n >= 0 && n <= 9) return "" n if (n == 10) return "*" if (n == 11) return "#" return "" } /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) 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) exit num = "" if (int(ton_npi / 16) % 8 == 1) { num = "+" } bcd_bytes = bcd_len - 1 if (bcd_bytes > 10) bcd_bytes = 10 for (i = 1; i <= bcd_bytes; i++) { b = hex2val(hex[alpha_len + 2 + i]) if (b < 0 || b == 255) break lo = b % 16 hi = int(b / 16) d1 = bcd_digit(lo) if (d1 == "") break num = num d1 d2 = bcd_digit(hi) if (d2 == "") break num = num d2 } if (num == "" || num == "+") exit name = "" for (i = 1; i <= alpha_len; i++) { b = hex2val(hex[i]) if (b < 0 || b == 255) break if (b >= 32 && b < 127 && b != 9) { name = name sprintf("%c", b) } } gsub(/^[[:space:]]+|[[:space:]]+$/, "", name) if (name == "") name = num printf "%s\t%s\n", num, name } ' >> "$tmp_contacts" fi rec=$((rec + 1)) done fi fi cat "$tmp_contacts" rm -f "$tmp_contacts" exit 0 ;; sim-contacts-export) req_slot="${2:-auto}" src_tsv="${3:-/home/user/.config/sxmo/contacts.tsv}" if [ ! -r "$src_tsv" ]; then echo "ERROR: Contacts file not found" exit 1 fi if ! wait_for_qmicli_modem 3; then echo "ERROR: No modem available" exit 1 fi QMI_CARDS="$($QMICLI_MODEM --uim-get-card-status 2>/dev/null || true)" if [ -z "$QMI_CARDS" ]; then echo "ERROR: Cannot read SIM card status" exit 1 fi eval "$(parse_uim_card_status "$QMI_CARDS" "$PREFERRED_SIM_SLOT")" target_slot="" target_aid="" if [ "$req_slot" = "1" ]; then if [ "$SLOT1_STATE" != "present" ]; then echo "ERROR: SIM 1 is not inserted" exit 1 fi target_slot="1" target_aid="$SLOT1_AID" elif [ "$req_slot" = "2" ]; then if [ "$SLOT2_STATE" != "present" ]; then echo "ERROR: SIM 2 is not inserted" exit 1 fi target_slot="2" target_aid="$SLOT2_AID" else if [ "$PRIMARY_GW_SLOT" = "1" ] && [ "$SLOT1_STATE" = "present" ]; then target_slot="1" target_aid="$SLOT1_AID" elif [ "$PRIMARY_GW_SLOT" = "2" ] && [ "$SLOT2_STATE" = "present" ]; then target_slot="2" target_aid="$SLOT2_AID" elif [ "$SLOT1_STATE" = "present" ]; then target_slot="1" target_aid="$SLOT1_AID" elif [ "$SLOT2_STATE" = "present" ]; then target_slot="2" target_aid="$SLOT2_AID" else echo "ERROR: No SIM card detected" exit 1 fi fi if command -v python3 >/dev/null 2>&1; then exp_out="$(python3 - "$target_slot" "$src_tsv" << 'PY' import socket, struct, select, sys 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("= 20: cmd, srv, inst, srv_node, srv_port = struct.unpack("= 4: res, err = struct.unpack("= 9: _, _, _, rs, rc = struct.unpack("= 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("= 9 else norm for rnum in range(1, rec_count + 1): tlv_rec = struct.pack("= 4: res, err = struct.unpack("= 2: clen = struct.unpack("= 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("= 4: res, err = struct.unpack("= 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 printf "%s\n" "$exp_out" | grep -q "^ERROR:"; then printf "%s\n" "$exp_out" exit 1 fi fi echo "ERROR: SIM $target_slot phonebook not ready" exit 1 ;; *) echo "Usage: $0 [boot|status|switch <1|2>|verify-pin |unblock-puk |set-pin-lock |change-pin |imei|usb-tether |at |sim-contacts-import [1|2|auto]|sim-contacts-export [1|2|auto] [tsv_file]]" >&2 exit 1 ;; esac