#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Utilities for data encoding/decoding (PSM timers, SIM BCD, PLMN lists). """ from typing import List, Union, Optional def encode_psm_timer(seconds: int, is_t3324: bool = False) -> str: """Encodes seconds into 3GPP binary string.""" if is_t3324: if seconds <= 2 * 31: return f"000{seconds // 2:05b}" if seconds <= 60 * 31: return f"001{seconds // 60:05b}" if seconds <= 360 * 31: return f"010{seconds // 360:05b}" return "11100000" else: if seconds <= 30 * 31: return f"100{seconds // 30:05b}" if seconds <= 60 * 31: return f"101{seconds // 60:05b}" if seconds <= 3600 * 31: return f"001{seconds // 3600:05b}" return f"010{min(31, seconds // 36000):05b}" def decode_plmn(bcd_data: bytes) -> Optional[str]: """Decodes a 3-byte BCD PLMN (MCC/MNC) from SIM storage.""" if len(bcd_data) < 3 or bcd_data == b'\xff\xff\xff': return None mcc = f"{(bcd_data[0] & 0x0F)}{(bcd_data[0] >> 4)}{(bcd_data[1] & 0x0F)}" mnc_digit3 = (bcd_data[1] >> 4) mnc = f"{(bcd_data[2] & 0x0F)}{(bcd_data[2] >> 4)}" if mnc_digit3 != 0xF: mnc = f"{(bcd_data[2] & 0x0F)}{(bcd_data[2] >> 4)}{mnc_digit3}" return f"{mcc}-{mnc}" def decode_act(act_bytes: bytes) -> str: """Decodes Access Technology bitmask (2 bytes).""" if len(act_bytes) < 2: return "Unknown" techs = [] b1, b2 = act_bytes[0], act_bytes[1] if b1 & 0x80: techs.append("UTRAN") if b1 & 0x40: techs.append("E-UTRAN") if b1 & 0x20: techs.append("NG-RAN") if b2 & 0x80: techs.append("GSM") if b2 & 0x40: techs.append("cdma2000 HRPD") if b2 & 0x20: techs.append("cdma2000 1xRTT") return "/".join(techs) if techs else "None" def decode_plmn_list(data: bytes, has_act: bool = False) -> List[str]: """Decodes a list of PLMNs, optionally with Access Technology.""" stride = 5 if has_act else 3 results = [] for i in range(0, len(data), stride): chunk = data[i:i+stride] plmn = decode_plmn(chunk[:3]) if plmn: if has_act: act = decode_act(chunk[3:5]) results.append(f"{plmn} [{act}]") else: results.append(plmn) return results def decode_iccid(bcd_data: bytes) -> str: """Decodes raw ICCID bytes (swapped nibbles).""" return "".join([f"{(b & 0x0F)}{(b >> 4):X}" for b in bcd_data]).replace("F", "") def bands_to_hex_mask(bands: List[int]) -> str: """Converts a list of band numbers into a hex bitmask string.""" mask = 0 for b in bands: if b > 0: mask |= (1 << (b - 1)) return f"{mask:X}" def hex_mask_to_bands(hex_mask: str) -> List[int]: """Converts a hex bitmask string back into a list of band numbers.""" try: mask = int(hex_mask, 16) except (ValueError, TypeError): return [] bands = [] for i in range(128): # Support up to 128 bands if (mask >> i) & 1: bands.append(i + 1) return bands def fmt_val(val: Union[str, List, None]) -> str: """Helper to format values for display in reports and shells.""" if val is None or val == "" or val == []: return "None (Not set on SIM)" if isinstance(val, list): return ", ".join(val) return str(val)