import sys import os import time import threading import serial import re import platform import queue import binascii from datetime import datetime from PyQt5.QtCore import QObject, pyqtSignal, QDateTime, QTimer from sms_utils import text_to_ucs2, ucs2_to_text, is_chinese_text, format_phone_number class LTEManager(QObject): # Signals sms_received = pyqtSignal(str, str, str) # sender, timestamp, message call_received = pyqtSignal(str) # caller number call_ended = pyqtSignal(str) # duration status_changed = pyqtSignal(str) # status message dtmf_received = pyqtSignal(str) # DTMF tone pcm_audio_status = pyqtSignal(bool) # PCM audio registration status (True=registered, False=unregistered) def __init__(self): super().__init__() self.connected = False self.running = False self.at_serial = None self.nmea_serial = None self.read_thread = None self.command_queue = [] self.command_event = threading.Event() self.command_lock = threading.Lock() self.response_buffer = [] self.last_response_time = 0 # 通话状态属性 self.in_call = False self.call_number = "" self.call_connected = False # 标记通话是否已经接通(区分来电振铃和通话接通) self.call_notification_sent = False # 标记是否已发送来电通知 self.pcm_registered = False # 模块信息属性 self.manufacturer = "" self.model = "" self.imei = "" self.firmware = "" self.phone_number = "" self.carrier = "" self.network_type = "" self.signal_strength = "" # SMS 相关变量 self.waiting_for_sms_content = False self.pending_sms_sender = None self.pending_sms_timestamp = None self.concat_sms_parts = {} # 用于存储长短信的各个部分 self.concat_sms_timeout = 30 # 长短信合并超时时间(秒) # 兼容性处理:模拟队列以便兼容旧代码 self.response_queue = queue.Queue() # 通话状态定义 self.call_states = { 0: "正在进行", # active 1: "保持", # hold 2: "拨号中", # dialing (MO) 3: "振铃中", # alerting (MO) 4: "来电中", # incoming (MT) 5: "等待中" # waiting (MT) } self.cleanup_timer = QTimer() self.cleanup_timer.timeout.connect(self._cleanup_old_sms_parts) self.cleanup_timer.start(300000) # 每5分钟清理一次 # 添加信息缓存,用于减少AT命令频率 self.info_cache = { 'carrier': {'value': None, 'timestamp': 0, 'valid_time': 300}, # 运营商信息缓存5分钟 'phone_number': {'value': None, 'timestamp': 0, 'valid_time': 3600}, # 电话号码缓存1小时 'network': {'value': None, 'timestamp': 0, 'valid_time': 60}, # 网络信息缓存1分钟 'signal': {'value': None, 'timestamp': 0, 'valid_time': 10}, # 信号强度缓存10秒 'module_info': {'value': None, 'timestamp': 0, 'valid_time': 3600} # 模块信息缓存1小时 } # 设置日志文件 self.setup_log_file() def setup_log_file(self): """设置AT命令日志文件""" try: # 在用户主目录下的.LTE文件夹创建日志文件 user_home = os.path.expanduser('~') lte_dir = os.path.join(user_home, '.LTE') if not os.path.exists(lte_dir): os.makedirs(lte_dir) # 使用日期作为文件名 current_date = datetime.now().strftime("%Y-%m-%d") self.log_file_path = os.path.join(lte_dir, f"at_commands_{current_date}.log") # 打开日志文件,使用追加模式 self.log_file = open(self.log_file_path, 'a', encoding='utf-8') # 写入日志文件头 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.log_file.write(f"\n\n{'='*50}\n") self.log_file.write(f"LTE Manager AT交互日志 - 会话开始: {timestamp}\n") self.log_file.write(f"{'='*50}\n\n") self.log_file.flush() print(f"AT命令日志文件已创建: {self.log_file_path}") return True except Exception as e: print(f"创建AT命令日志文件失败: {str(e)}") self.log_file = None return False def log_at_command(self, is_command, content): """记录AT命令或响应到日志文件""" try: if not hasattr(self, 'log_file') or self.log_file is None: # 如果日志文件不存在,尝试重新创建 self.setup_log_file() if not hasattr(self, 'log_file') or self.log_file is None: # 如果创建失败,仅打印到控制台 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] prefix = ">>>" if is_command else "<<<" print(f"[{timestamp}] {prefix} {content}") return # 放弃写入日志 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] if is_command: log_entry = f"[{timestamp}] >>> {content}\n" else: log_entry = f"[{timestamp}] <<< {content}\n" self.log_file.write(log_entry) self.log_file.flush() except Exception as e: print(f"写入AT命令日志失败: {str(e)}") try: # 尝试重新创建日志 self.setup_log_file() except: # 如果仍然失败,放弃 pass def close_log_file(self): """关闭AT命令日志文件""" if self.log_file: try: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.log_file.write(f"\n{'='*50}\n") self.log_file.write(f"LTE Manager AT交互日志 - 会话结束: {timestamp}\n") self.log_file.write(f"{'='*50}\n") self.log_file.close() self.log_file = None print(f"AT命令日志文件已关闭: {self.log_file_path}") except Exception as e: print(f"关闭AT命令日志文件失败: {str(e)}") def _reset_cache(self, specific_key=None): """重置缓存,可以重置特定键或所有缓存""" if specific_key: if specific_key in self.info_cache: self.info_cache[specific_key]['value'] = None self.info_cache[specific_key]['timestamp'] = 0 else: # 重置所有缓存 for key in self.info_cache: self.info_cache[key]['value'] = None self.info_cache[key]['timestamp'] = 0 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 设备信息缓存已重置") def connect(self, at_port, at_baudrate=115200, nmea_port="", nmea_baudrate=9600): """Connect to the LTE module""" if self.connected: return False try: # 记录连接参数 self.log_at_command(False, f"[INFO] 连接到LTE模块 - AT端口: {at_port} 波特率: {at_baudrate}") if nmea_port: self.log_at_command(False, f"[INFO] NMEA端口: {nmea_port} 波特率: {nmea_baudrate}") # Connect to AT port self.at_serial = serial.Serial( port=at_port, baudrate=at_baudrate, timeout=1 ) # Set DTR/RTS self.at_serial.setDTR(True) self.at_serial.setRTS(True) # Connect to NMEA port (if specified) if nmea_port: self.nmea_serial = serial.Serial( port=nmea_port, baudrate=nmea_baudrate, timeout=1 ) # Initialize running flag and start read thread self.running = True self.read_thread = threading.Thread(target=self._read_serial) self.read_thread.daemon = True self.read_thread.start() # 重置所有缓存,确保获取最新信息 self._reset_cache() # Initialize module if not self._initialize_module(): self.log_at_command(False, f"[ERROR] 初始化模块失败") self.status_changed.emit("Failed to initialize module") self.disconnect() # 确保断开连接,释放资源 return False # 等待一段时间以确保模块初始化完成 time.sleep(1) # 获取模块基本信息 try: self._update_carrier_info() except Exception as e: self.log_at_command(False, f"[WARNING] 获取运营商信息失败: {str(e)}") print(f"获取运营商信息失败: {str(e)}") try: self._update_phone_number() except Exception as e: self.log_at_command(False, f"[WARNING] 获取电话号码失败: {str(e)}") print(f"获取电话号码失败: {str(e)}") try: self._update_signal_strength() except Exception as e: self.log_at_command(False, f"[WARNING] 获取信号强度失败: {str(e)}") print(f"获取信号强度失败: {str(e)}") # Update status self.connected = True self.status_changed.emit("Connected to LTE module") # 记录连接信息到日志 self.log_at_command(False, f"[INFO] 已连接到LTE模块") # 连接成功后日志中记录基本信息 self.log_at_command(False, f"[INFO] 运营商: {self.carrier if hasattr(self, 'carrier') and self.carrier else '未知'}") self.log_at_command(False, f"[INFO] 网络状态: {self.network_type if hasattr(self, 'network_type') and self.network_type else '未知'}") self.log_at_command(False, f"[INFO] 电话号码: {self.phone_number if hasattr(self, 'phone_number') and self.phone_number else '未知'}") self.log_at_command(False, f"[INFO] 信号强度: {self.signal_strength if hasattr(self, 'signal_strength') and self.signal_strength else '未知'}") return True except Exception as e: self.log_at_command(False, f"[ERROR] 连接失败: {str(e)}") self.status_changed.emit(f"Connection error: {str(e)}") # 确保所有资源被释放 try: if hasattr(self, 'at_serial') and self.at_serial and self.at_serial.is_open: self.at_serial.close() if hasattr(self, 'nmea_serial') and self.nmea_serial and self.nmea_serial.is_open: self.nmea_serial.close() self.running = False self.connected = False except Exception as cleanup_error: self.log_at_command(False, f"[ERROR] 清理连接资源时出错: {str(cleanup_error)}") return False def disconnect(self): """Disconnect from the LTE module""" if not self.connected: return False try: # 记录断开连接信息到日志 self.log_at_command(False, "[INFO] 正在断开与LTE模块的连接") # 重置所有缓存,确保下次连接时获取最新信息 self._reset_cache() # 确保所有通话已结束 if self.in_call: self.end_call() # 确保PCM音频已取消注册 if self.pcm_registered: self._ensure_pcm_audio_unregistered() # Stop running flag (will stop read thread) self.running = False # Close AT serial if self.at_serial and self.at_serial.is_open: try: # 正常关闭串口 self.at_serial.close() except Exception as e: self.log_at_command(False, f"[ERROR] 关闭AT串口出错: {str(e)}") # Close NMEA serial if self.nmea_serial and self.nmea_serial.is_open: try: self.nmea_serial.close() except Exception as e: self.log_at_command(False, f"[ERROR] 关闭NMEA串口出错: {str(e)}") # Wait for read thread to end if self.read_thread and self.read_thread.is_alive(): self.read_thread.join(timeout=2.0) # Update status self.connected = False self.in_call = False self.call_number = "" self.pcm_registered = False self.status_changed.emit("Disconnected from LTE module") # Stop cleanup timer self.cleanup_timer.stop() # 关闭日志文件 self.close_log_file() return True except Exception as e: self.status_changed.emit(f"Disconnection error: {str(e)}") # 记录断开连接错误 if self.log_file: self.log_at_command(False, f"[ERROR] 断开连接出错: {str(e)}") # 尝试关闭日志文件,即使发生错误 self.close_log_file() return False def is_connected(self): """Check if connected to the LTE module""" return self.connected def send_at_command(self, command, timeout=5.0, expect_response=True): """Send AT command to the module and wait for response""" if not self.connected or not self.at_serial: if self.log_file: self.log_at_command(True, f"{command} (未发送 - 模块未连接)") return None with self.command_lock: # 记录发送的AT命令 self.log_at_command(True, command) # 清空响应缓冲区 self.response_buffer.clear() self.last_response_time = time.time() # Send command cmd = command.strip() if not cmd.endswith('\r'): cmd += '\r' self.at_serial.write(cmd.encode()) if not expect_response: self.log_at_command(False, "(无需等待响应)") return None # Wait for response start_time = time.time() response = [] command_echo_received = False response_text = "" while time.time() - start_time < timeout: # 检查缓冲区是否有新响应 if self.response_buffer: # 获取并移除第一个响应 line = self.response_buffer.pop(0) # 跳过命令回显行 if not command_echo_received and line.strip() == command.strip(): command_echo_received = True continue response.append(line) # Check if response is complete (ends with OK or ERROR) if line.strip() in ["OK", "ERROR"] or "ERROR" in line: break # 短暂等待新响应 time.sleep(0.05) # 检查最后响应时间,如果超过一定时间没有新响应且已收到内容,可以提前结束 if response and time.time() - self.last_response_time > 0.5: break # 构建完整响应文本 response_text = '\n'.join(response) # 如果超时且没有收到任何响应,记录到日志 if not response: self.log_at_command(False, f"(命令响应超时,等待时间: {timeout}秒)") return response_text def _read_serial(self): """Read data from serial port in a separate thread""" buffer = "" while self.running: if not self.at_serial or not self.at_serial.is_open: time.sleep(0.1) continue try: # Read data from serial port data = self.at_serial.read(self.at_serial.in_waiting or 1) if data: text = data.decode('utf-8', errors='replace') buffer += text # Process complete lines while '\r\n' in buffer: line, buffer = buffer.split('\r\n', 1) line = line.strip() if not line: continue # 记录所有接收到的完整行,不论是否是命令回显 # 这确保了所有AT交互都被记录,包括模块的主动上报 if not line.startswith("AT"): # 不是命令回显 self.log_at_command(False, line) # Process unsolicited responses self._process_unsolicited(line) # 同时使用缓冲区列表和队列来存储响应,确保兼容性 self.response_buffer.append(line) self.response_queue.put(line) # 向旧的队列接口发送数据 self.last_response_time = time.time() except Exception as e: print(f"Serial read error: {str(e)}") # 记录读取错误 if self.log_file: self.log_at_command(False, f"[ERROR] 读取串口数据出错: {str(e)}") time.sleep(0.1) def _process_unsolicited(self, line): """Process unsolicited responses from the module""" # Incoming call if line.startswith("RING"): self.status_changed.emit("Incoming call") self.in_call = True # 设置为未接听状态 self.call_connected = False # Reset notification flag on new RING self.call_notification_sent = False # Caller ID elif "+CLIP:" in line: match = re.search(r'\+CLIP: "([^"]+)"', line) if match: number = match.group(1) self.call_number = number # Only emit the signal if we haven't sent a notification for this call yet if not self.call_notification_sent and self.in_call: self.call_received.emit(number) self.call_notification_sent = True print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Call notification sent for {number}") # Call ended elif "NO CARRIER" in line: self.in_call = False self.call_connected = False self.call_notification_sent = False # Reset the flag when call ends self.status_changed.emit("Call ended") # 记录通话结束日志,方便调试 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Call ended, NO CARRIER detected") # 通话结束时取消PCM音频注册 self._ensure_pcm_audio_unregistered() # 发送通话结束信号 self.call_ended.emit("Call ended") # Voice call begin - 这是通话实际建立的时间点 elif "VOICE CALL: BEGIN" in line: # 设置通话状态为活动 self.in_call = True # 设置为已接通状态 self.call_connected = True # 记录日志 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通话已建立 (VOICE CALL: BEGIN)") self.status_changed.emit("Call in progress") # 先确保任何可能存在的PCM注册已取消 self._unregister_pcm_audio() # 短暂延迟后再注册PCM音频,确保模块已稳定 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 延迟100ms后注册PCM音频") time.sleep(0.1) # 先延迟一小段时间 # 开始注册PCM音频 self._register_pcm_audio() # Voice call end elif "VOICE CALL: END:" in line: self.in_call = False self.call_connected = False match = re.search(r'VOICE CALL: END: (\d+)', line) duration = "0" if match: duration = match.group(1) print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Call ended, duration: {duration}") else: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Call ended, no duration info") # 记录详细日志,包括通话持续时间 call_minutes = int(duration) // 60 call_seconds = int(duration) % 60 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通话结束,持续时间: {call_minutes}分{call_seconds}秒") # 首先取消PCM音频注册,然后才发送通话结束信号 # 这样可以确保PCM音频在通话结束信号处理前已经被取消 if self._ensure_pcm_audio_unregistered(): # 在成功取消注册后发送信号 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频已取消注册,发送通话结束信号") # 使用threading.Timer代替QTimer,避免线程问题 threading.Timer(0.2, lambda: self.call_ended.emit(duration)).start() else: # 即使取消注册失败,也要发送通话结束信号 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频取消注册失败,仍发送通话结束信号") # 使用threading.Timer代替QTimer,避免线程问题 threading.Timer(0.2, lambda: self.call_ended.emit(duration)).start() # Missed call elif "MISSED_CALL:" in line: match = re.search(r'MISSED_CALL: ([^\r\n]+)', line) if match: missed_info = match.group(1) self.status_changed.emit(f"Missed call: {missed_info}") # Extract phone number from missed call info # Format is typically "HH:MMAM/PM PHONENUMBER" parts = missed_info.strip().split() if len(parts) >= 2: missed_number = parts[-1] # Last part should be the phone number # Signal call ended to stop ringtone self.call_ended.emit("Missed") # Also emit missed call signal with the number self.call_number = missed_number self.status_changed.emit(f"Missed call from {missed_number}") # SMS received (direct content mode) elif line.startswith("+CMT:"): # 检查是否为长短信 if self._is_concatenated_sms(line): self._handle_concatenated_sms(line) else: # 处理普通短信 self._handle_regular_sms(line) elif self.waiting_for_sms_content: # This is the SMS content line self.waiting_for_sms_content = False message = line # Check if the content is in UCS2 format (hex string) if all(c in "0123456789ABCDEFabcdef" for c in line.replace(" ", "")): try: # 检查是否为长短信的一部分 if self._is_part_of_concatenated_sms(line): # 处理长短信部分 self._process_concatenated_sms_part(self.pending_sms_sender, self.pending_sms_timestamp, line) else: # 普通UCS2短信,直接解码 message = ucs2_to_text(line) self.status_changed.emit("Decoded UCS2 message") # 发送完整消息 self.sms_received.emit( self.pending_sms_sender, self.pending_sms_timestamp, message ) except Exception as e: self.status_changed.emit(f"Failed to decode UCS2: {str(e)}") # Keep original if decoding fails message = line # 发送原始消息 self.sms_received.emit( self.pending_sms_sender, self.pending_sms_timestamp, message ) else: # 非UCS2编码,直接发送 self.sms_received.emit( self.pending_sms_sender, self.pending_sms_timestamp, message ) # Clear pending SMS data self.pending_sms_sender = None self.pending_sms_timestamp = None # SMS received (index mode) elif line.startswith("+CMTI:"): match = re.search(r'\+CMTI: "([^"]+)",(\d+)', line) if match: storage, index = match.group(1), match.group(2) self.status_changed.emit(f"New SMS at index {index}") # Fetch SMS content self._fetch_sms(storage, index) # DTMF tone received elif "+RXDTMF:" in line: match = re.search(r'\+RXDTMF: (\d)', line) if match: tone = match.group(1) self.dtmf_received.emit(tone) # SMS full elif "+SMS FULL" in line: self.status_changed.emit("SMS storage full. Please delete some messages.") def _is_concatenated_sms(self, header_line): """检查是否为长短信""" # 检查是否包含长短信特征 # 对于UCS2编码的长短信,通常有特定的格式标识 if ",145," in header_line and ",0,8," in header_line: return True return False def _handle_regular_sms(self, header_line): """处理普通短信""" # Parse SMS header header_match = re.search(r'\+CMT: "([^"]*)",[^,]*,"([^"]*)"', header_line) if header_match: sender = header_match.group(1) timestamp = header_match.group(2) # Check if sender is in UCS2 format (starts with 00) if sender.startswith("00"): try: sender = ucs2_to_text(sender) except: pass # Keep original if decoding fails self.pending_sms_sender = sender self.pending_sms_timestamp = timestamp self.status_changed.emit(f"SMS received from {sender}") else: self.pending_sms_sender = "Unknown" self.pending_sms_timestamp = QDateTime.currentDateTime().toString("yy/MM/dd,hh:mm:ss") self.status_changed.emit("SMS received") # Next line will contain the SMS content self.waiting_for_sms_content = True def _handle_concatenated_sms(self, header_line): """处理长短信""" try: # 解析长短信头部 # 格式: +CMT: "sender","","timestamp",145,36,0,8,"service_center",145,64 parts = header_line.split(',') if len(parts) < 10: # 格式不符合预期,作为普通短信处理 self._handle_regular_sms(header_line) return # 提取发送者和时间戳 sender = parts[0].replace('+CMT: ', '').strip('"') timestamp = parts[2].strip('"') # 检查发送者是否为UCS2格式 if sender.startswith("00"): try: sender = ucs2_to_text(sender) except: pass # 解码失败时保留原始格式 # 设置等待内容标志 self.pending_sms_sender = sender self.pending_sms_timestamp = timestamp self.waiting_for_sms_content = True # 记录长短信信息,下一行将包含内容 self.status_changed.emit(f"Concatenated SMS part received from {sender}") except Exception as e: self.status_changed.emit(f"Error parsing concatenated SMS header: {str(e)}") # 出错时尝试作为普通短信处理 self._handle_regular_sms(header_line) def _process_concatenated_sms_part(self, sender, timestamp, content): """处理长短信的一部分""" try: # 移除空格 content = content.replace(" ", "") # 打印原始内容用于调试 self.status_changed.emit(f"长短信原始内容: {content[:50]}...") # 解码内容 try: decoded_content = ucs2_to_text(content) self.status_changed.emit(f"解码后内容: {decoded_content[:50]}...") except Exception as decode_error: self.status_changed.emit(f"UCS2解码错误: {str(decode_error)}") # 尝试不同的解码方法 try: # 尝试直接从十六进制转换为字节,然后解码 hex_bytes = binascii.unhexlify(content) decoded_content = hex_bytes.decode('utf-16-be', errors='replace') self.status_changed.emit(f"替代解码方法成功: {decoded_content[:50]}...") except Exception as alt_error: self.status_changed.emit(f"替代解码方法错误: {str(alt_error)}") # 如果解码失败,使用原始内容 decoded_content = content self.sms_received.emit( self.pending_sms_sender, self.pending_sms_timestamp, f"[解码失败] {content[:100]}..." ) return # 检查特殊格式,尝试直接提取有效负载 if "62117ED94F6053D14E86957F6587672C" in content: # 基于固定标记提取后面的有效内容 # 格式可能是:标记 + 消息前缀 + 003A(冒号) + URL编码 parts = content.split("003A", 1) # 003A是冒号的UCS2编码 if len(parts) > 1 and parts[1]: # 只解码URL部分 try: url_part = ucs2_to_text("003A" + parts[1]) # 加回冒号 self.status_changed.emit(f"提取的URL部分: {url_part}") # 直接提取URL url_match = re.search(r':(https?://[^\s]+)', url_part) url = url_match.group(1) if url_match else url_part # 使用前缀 + URL格式 prefix = "消息" # 默认前缀 # 使用发送者和时间戳的前10个字符作为唯一标识符 sms_id = f"{sender}_{timestamp[:10]}" # 如果是新的长短信,初始化存储 if sms_id not in self.concat_sms_parts: self.concat_sms_parts[sms_id] = { 'sender': sender, 'timestamp': timestamp, 'parts': [], 'urls': [], 'received_time': time.time(), 'prefix': prefix } # 存储这一部分 if url and url not in self.concat_sms_parts[sms_id]['urls']: self.concat_sms_parts[sms_id]['urls'].append(url) self.concat_sms_parts[sms_id]['parts'].append(url_part) # 更新接收时间 self.concat_sms_parts[sms_id]['received_time'] = time.time() # 记录日志 part_num = len(self.concat_sms_parts[sms_id]['parts']) self.status_changed.emit(f"接收到长短信的第{part_num}部分,来自{sender}") # 检查是否需要合并 QTimer.singleShot(2000, lambda: self._check_and_merge_sms(sms_id)) return except Exception as url_error: self.status_changed.emit(f"URL提取错误: {str(url_error)}") # 常规处理逻辑(如果特殊处理失败) # 提取URL部分 url_match = re.search(r'(https?://[^\s]+)', decoded_content) url = url_match.group(1) if url_match else "" # 提取消息前缀部分(冒号前的内容) prefix = "" if ":" in decoded_content: prefix = decoded_content.split(":", 1)[0].strip() else: prefix = "消息" # 默认前缀 # 使用发送者和时间戳的前10个字符作为唯一标识符 sms_id = f"{sender}_{timestamp[:10]}" # 如果是新的长短信,初始化存储 if sms_id not in self.concat_sms_parts: self.concat_sms_parts[sms_id] = { 'sender': sender, 'timestamp': timestamp, 'parts': [], 'urls': [], 'received_time': time.time(), 'prefix': prefix } # 存储这一部分 if url and url not in self.concat_sms_parts[sms_id]['urls']: self.concat_sms_parts[sms_id]['urls'].append(url) self.concat_sms_parts[sms_id]['parts'].append(decoded_content) # 更新接收时间 self.concat_sms_parts[sms_id]['received_time'] = time.time() # 记录日志 part_num = len(self.concat_sms_parts[sms_id]['parts']) self.status_changed.emit(f"接收到长短信的第{part_num}部分,来自{sender}") # 检查是否需要合并 QTimer.singleShot(2000, lambda: self._check_and_merge_sms(sms_id)) else: # 没有URL或已经存在的URL # 尝试直接发送完整内容 self.status_changed.emit(f"无法提取URL或URL重复,尝试直接发送内容") self.sms_received.emit( sender, timestamp, decoded_content ) except Exception as e: self.status_changed.emit(f"长短信处理错误: {str(e)}") # 出错时尝试作为普通短信处理 try: decoded = ucs2_to_text(content) if all(c in "0123456789ABCDEFabcdef" for c in content) else content self.sms_received.emit( self.pending_sms_sender, self.pending_sms_timestamp, decoded ) except: # 如果解码也失败,发送原始内容 self.sms_received.emit( self.pending_sms_sender, self.pending_sms_timestamp, content ) def _check_and_merge_sms(self, sms_id): """检查并合并长短信""" if sms_id not in self.concat_sms_parts: return sms_info = self.concat_sms_parts[sms_id] # 检查是否已经过了足够的时间 current_time = time.time() if current_time - sms_info['received_time'] < 2: # 2秒内收到的部分 # 还不到合并的时间,可能还有更多部分 return # 合并所有部分 merged_content = self._merge_sms_parts(sms_id) # 发送完整消息 self.sms_received.emit( sms_info['sender'], sms_info['timestamp'], merged_content ) # 清理已处理的长短信 del self.concat_sms_parts[sms_id] # 记录日志 self.status_changed.emit(f"Concatenated SMS fully received from {sms_info['sender']}") def _merge_sms_parts(self, sms_id): """合并长短信的所有部分""" if sms_id not in self.concat_sms_parts: return "" sms_info = self.concat_sms_parts[sms_id] # 如果只有一个部分,直接返回 if len(sms_info['parts']) == 1: return sms_info['parts'][0] # 合并所有部分 prefix = sms_info.get('prefix', '') urls = sms_info.get('urls', []) if urls: # 只返回URL列表,每行一个 if prefix: merged_content = f"{prefix}:\n" + "\n".join(urls) else: merged_content = "\n".join(urls) else: # 如果没有提取到URL,直接合并所有部分 merged_content = "\n".join(sms_info['parts']) return merged_content def _cleanup_old_sms_parts(self): """清理超时的长短信部分""" current_time = time.time() sms_ids_to_remove = [] for sms_id, sms_info in self.concat_sms_parts.items(): if current_time - sms_info['received_time'] > self.concat_sms_timeout: sms_ids_to_remove.append(sms_id) for sms_id in sms_ids_to_remove: del self.concat_sms_parts[sms_id] self.status_changed.emit(f"Removed timeout concatenated SMS {sms_id}") def _decode_pdu_message(self, pdu_str): """Decode PDU format message (including Chinese characters)""" try: # Remove spaces and convert to bytes pdu_str = pdu_str.replace(" ", "") # 检查是否为长短信的一部分 if self._is_part_of_concatenated_sms(pdu_str): # 可能是长短信的一部分,需要特殊处理 # 这里需要根据实际的PDU格式进行解析 pass # Try to decode using our utility function return ucs2_to_text(pdu_str) except Exception as e: print(f"PDU decode error: {str(e)}") # If decoding fails, return the original string return f"[Decode error: {pdu_str[:30]}...]" def _is_part_of_concatenated_sms(self, content): """检查内容是否为长短信的一部分""" # 移除空格 content = content.replace(" ", "") # 检查是否为UCS2编码 if not all(c in "0123456789ABCDEFabcdef" for c in content): return False # 检查内容长度是否足够 if len(content) < 10: return False # 根据用户提供的示例,检查是否包含特定的模式 # 示例中的长短信内容以"62117ED94F6053D14E86957F6587672C"开头 if content.startswith("62117ED94F6053D14E86957F6587672C"): return True return False def _initialize_module(self): """Initialize the LTE module with basic settings""" try: # 在日志中标记初始化模块的开始 self.log_at_command(False, "[INFO] 开始初始化LTE模块") # AT echo off self.send_at_command("ATE0") # Set SMS text mode self.send_at_command("AT+CMGF=1") # Set SMS character set to GSM self.send_at_command('AT+CSCS="GSM"') # Enable new SMS notification self.send_at_command('AT+CNMI=2,1,0,0,0') # Enable caller ID notification self.send_at_command("AT+CLIP=1") # Set call progress monitoring URC (Unsolicited Result Code) self.send_at_command("AT+XCALLSTAT=1") # Get IMEI self.send_at_command("AT+GSN") # 在日志中标记初始化模块的完成 self.log_at_command(False, "[INFO] LTE模块初始化完成") return True except Exception as e: self.status_changed.emit(f"Module initialization error: {str(e)}") # 记录初始化错误 self.log_at_command(False, f"[ERROR] 模块初始化错误: {str(e)}") return False def _get_module_info(self): """Get module information (internal method)""" if not self.connected: return {} # 尝试从缓存中获取 cached_value = self._get_cached_value('module_info') if cached_value is not None: return cached_value # 缓存中没有,需要重新获取 try: # 确保基本属性已被初始化 if not self.manufacturer: manufacturer_response = self.send_at_command("AT+CGMI") if "OK" in manufacturer_response: # 移除命令回显和OK响应,只保留实际内容 lines = [line.strip() for line in manufacturer_response.split('\n') if line.strip()] # 过滤掉AT命令回显和OK响应 content_lines = [line for line in lines if line != "OK" and not line.startswith("AT+")] self.manufacturer = '\n'.join(content_lines).strip() else: self.manufacturer = "Unknown" if not self.model: model_response = self.send_at_command("AT+CGMM") if "OK" in model_response: # 移除命令回显和OK响应,只保留实际内容 lines = [line.strip() for line in model_response.split('\n') if line.strip()] # 过滤掉AT命令回显和OK响应 content_lines = [line for line in lines if line != "OK" and not line.startswith("AT+")] self.model = '\n'.join(content_lines).strip() else: self.model = "Unknown" if not self.imei: imei_response = self.send_at_command("AT+CGSN") if "OK" in imei_response: # 移除命令回显和OK响应,只保留实际内容 lines = [line.strip() for line in imei_response.split('\n') if line.strip()] # 过滤掉AT命令回显和OK响应 content_lines = [line for line in lines if line != "OK" and not line.startswith("AT+")] self.imei = '\n'.join(content_lines).strip() else: self.imei = "Unknown" if not self.firmware: firmware_response = self.send_at_command("AT+CGMR") if "OK" in firmware_response: # 尝试提取特定格式的固件版本 match = re.search(r'\+CGMR: (.+)', firmware_response) if match: self.firmware = match.group(1) else: # 移除命令回显和OK响应,只保留实际内容 lines = [line.strip() for line in firmware_response.split('\n') if line.strip()] # 过滤掉AT命令回显和OK响应 content_lines = [line for line in lines if line != "OK" and not line.startswith("AT+")] self.firmware = '\n'.join(content_lines).strip() else: self.firmware = "Unknown" # 更新其他信息 if not self.phone_number: self._update_phone_number() if not self.carrier or not self.network_type: self._update_carrier_info() if not self.signal_strength: self._update_signal_strength() # 组装模块信息字典 module_info = { 'manufacturer': self.manufacturer, 'model': self.model, 'imei': self.imei, 'firmware': self.firmware, 'phone_number': self.phone_number, 'carrier': self.carrier, 'network_type': self.network_type, 'signal_strength': self.signal_strength } # 更新缓存 self._update_cache('module_info', module_info) return module_info except Exception as e: print(f"获取模块信息出错: {str(e)}") # 出错时返回带有默认值的字典 return { 'manufacturer': 'Error', 'model': 'Error', 'imei': 'Error', 'firmware': 'Error', 'phone_number': 'Error', 'carrier': 'Error', 'network_type': 'Error', 'signal_strength': 'Error' } def get_module_info(self): """Get module information""" if not self.connected: return {} return self._get_module_info() def get_carrier_info(self): """Get carrier information""" if not self.connected: return ("未连接", "未连接") try: self._update_carrier_info() # 确保carrier和network_type属性存在 if not hasattr(self, 'carrier') or not self.carrier: self.carrier = "未知" if not hasattr(self, 'network_type') or not self.network_type: self.network_type = "未知" return (self.carrier, self.network_type) except Exception as e: self.log_at_interaction(f"获取运营商信息失败: {str(e)}") print(f"获取运营商信息失败: {str(e)}") return ("获取出错", "获取出错") def get_phone_number(self): """Get phone number""" if not self.connected: return "未连接" try: self._update_phone_number() # 确保phone_number属性存在 if not hasattr(self, 'phone_number') or not self.phone_number: self.phone_number = "未知" return self.phone_number except Exception as e: self.log_at_interaction(f"获取电话号码失败: {str(e)}") print(f"获取电话号码失败: {str(e)}") return "获取出错" def _update_carrier_info(self): """Update carrier information""" try: # 初始化默认值 self.carrier = "未知" self.network_type = "未知" response = self.send_at_command("AT+COPS?") if response and "+COPS:" in response: # Try to parse carrier information match = re.search(r'\+COPS:\s*\d+,\d+,"([^"]+)"', response) if match: carrier_name = match.group(1) # Check if it's a numeric code like "46000" if re.match(r'^\d+$', carrier_name): # Map common Chinese carrier codes carriers = { "46000": "中国移动", "46001": "中国联通", "46002": "中国移动", "46003": "中国电信", "46004": "中国移动", "46005": "中国电信", "46006": "中国联通", "46007": "中国移动", "46008": "中国电信", "46009": "中国联通", "46011": "中国电信", } self.carrier = carriers.get(carrier_name, carrier_name) else: self.carrier = carrier_name # Try to get network type network_response = self.send_at_command("AT+CPSI?") if network_response and "+CPSI:" in network_response: parts = network_response.strip().split('\n')[0].replace("+CPSI: ", "").split(',') if len(parts) >= 2: self.network_type = f"{parts[0]}/{parts[1]}" # Add band info if available if len(parts) >= 7: self.network_type += f"/{parts[6]}" # 更新缓存 - 既缓存单个值,也缓存组合值以便不同的获取方式 self._update_cache('carrier', self.carrier) return self.carrier except Exception as e: self.log_at_command(False, f"[ERROR] 获取运营商信息失败: {str(e)}") print(f"获取运营商信息失败: {str(e)}") self.carrier = "获取出错" self.network_type = "获取出错" self._update_cache('carrier', self.carrier) return self.carrier def _update_signal_strength(self): """Update signal strength information""" try: response = self.send_at_command("AT+CSQ") if not response or "+CSQ:" not in response: self.signal_strength = ("未知", "无响应") return self.signal_strength match = re.search(r'\+CSQ:\s*(\d+),\s*(\d+)', response) if match: rssi = int(match.group(1)) ber = int(match.group(2)) # 计算信号百分比 if rssi == 99: percent = 0 desc = "无信号" else: # RSSI范围是0-31,对应-113到-51 dBm percent = min(100, int((rssi / 31.0) * 100)) # 根据信号强度确定描述 if rssi > 25: # > -67 dBm desc = "极好" elif rssi > 19: # > -79 dBm desc = "很好" elif rssi > 14: # > -89 dBm desc = "好" elif rssi > 9: # > -99 dBm desc = "一般" elif rssi > 4: # > -107 dBm desc = "差" else: desc = "很差" # 将信号强度保存为元组(信号文本, 描述) self.signal_strength = (f"{percent}%", desc) return self.signal_strength else: # 如果无法解析信号强度,返回默认值 self.signal_strength = ("未知", "格式错误") return self.signal_strength except Exception as e: self.log_at_interaction(f"解析信号强度失败: {str(e)}") print(f"解析信号强度失败: {str(e)}") self.signal_strength = ("错误", str(e)[:20]) return self.signal_strength def _fetch_sms(self, storage, index): """Fetch SMS content by index""" response = self.send_at_command(f'AT+CMGR={index}') if response and "+CMGR:" in response: # Parse SMS header header_match = re.search(r'\+CMGR: "[^"]*","([^"]*)",[^,]*,"([^"]*)"', response) if header_match: sender = header_match.group(1) timestamp = header_match.group(2) # Check if sender is in UCS2 format (starts with 00) if sender.startswith("00"): try: sender = ucs2_to_text(sender) except: pass # Keep original if decoding fails # Extract message content lines = response.split('\n') message = "" if len(lines) > 1: content_line = lines[1].strip() message = content_line # Check if the content is in UCS2 format (hex string) if all(c in "0123456789ABCDEFabcdef" for c in content_line.replace(" ", "")): try: # Try to decode as UCS2 message = ucs2_to_text(content_line) self.status_changed.emit("Decoded UCS2 message from storage") except Exception as e: self.status_changed.emit(f"Failed to decode UCS2 from storage: {str(e)}") # Keep original if decoding fails self.sms_received.emit(sender, timestamp, message) def _register_pcm_audio(self): """注册PCM音频(用于通话开始时) 按照文档要求,在VOICE CALL: BEGIN后执行AT+CPCMREG=1 """ if not self.connected or not self.at_serial: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注册失败:未连接") return False # 如果已经不在通话中了,跳过注册 if not self.in_call: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 不在通话中,跳过PCM音频注册") self.status_changed.emit("Not in call, PCM audio registration skipped") return False try: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 开始PCM音频注册过程") # 清除可能的额外数据 if self.at_serial.in_waiting > 0: self.at_serial.read(self.at_serial.in_waiting) print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 清除了缓冲区数据") # 设置PCM格式为8K采样率(如需要16K,可更改为AT+CPCMFRM=1) try: with self.command_lock: self.at_serial.write(b'AT+CPCMFRM=0\r') time.sleep(0.1) if self.at_serial.in_waiting > 0: resp = self.at_serial.read(self.at_serial.in_waiting).decode('utf-8', errors='ignore') print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM格式设置响应: {resp}") except Exception as e: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 设置PCM格式出错: {str(e)}") # 直接发送PCM音频注册命令,使用更短的超时 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送PCM音频注册命令") # 确保没有另一个命令在发送 with self.command_lock: self.at_serial.write(b'AT+CPCMREG=1\r') print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注册命令已发送") # 等待较短的时间以获取响应 time.sleep(0.1) # 试图读取响应 response = "" start_time = time.time() while time.time() - start_time < 0.5: # 最多等待0.5秒 if self.at_serial.in_waiting > 0: response += self.at_serial.read(self.at_serial.in_waiting).decode('utf-8', errors='ignore') if "OK" in response or "ERROR" in response: break time.sleep(0.05) print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注册响应: {response}") # 记录是否成功 success = "OK" in response # 根据响应结果发送状态更新 if success: self.status_changed.emit("PCM audio registered successfully") print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注册成功") else: self.status_changed.emit("PCM audio registration sent") print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注册状态未知") # 无论响应如何,发送激活信号,系统将尝试处理音频 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送PCM音频激活信号") self.pcm_audio_status.emit(True) # 添加调试记录 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注册流程完成") return True except Exception as e: self.status_changed.emit(f"PCM audio registration error: {str(e)}") print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注册出错: {str(e)}") # 错误发生时,仍然尝试激活音频,保持一致行为 self.pcm_audio_status.emit(True) return False def _unregister_pcm_audio(self): """取消注册PCM音频(用于通话结束时) 按照文档要求,在VOICE CALL: END后执行AT+CPCMREG=0 """ if not self.connected or not self.at_serial: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 取消PCM音频注册失败:未连接") # 即使未连接,也发送停止信号 self.pcm_audio_status.emit(False) return False try: # 确保没有另一个命令在发送 with self.command_lock: # 清除任何待处理的数据 if self.at_serial.in_waiting > 0: self.at_serial.read(self.at_serial.in_waiting) print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 清除了PCM音频注销前的缓冲区数据") # 使用直接写入代替send_at_command,避免阻塞 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送PCM音频注销命令") self.at_serial.write(b'AT+CPCMREG=0\r') print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销命令已发送") # 给一点时间让模块响应 time.sleep(0.1) # 尝试读取响应,但不等待过长时间 response = "" start_time = time.time() while time.time() - start_time < 0.3: # 等待最多0.3秒 if self.at_serial.in_waiting > 0: response += self.at_serial.read(self.at_serial.in_waiting).decode('utf-8', errors='ignore') if "OK" in response: break time.sleep(0.05) print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销响应: {response}") success = "OK" in response # 根据响应结果更新状态 if success: self.status_changed.emit("PCM audio unregistered successfully") print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销成功") else: self.status_changed.emit("PCM audio unregistration sent") print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销状态未知") # 无论命令是否成功,都发送停止信号 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送PCM音频停止信号") self.pcm_audio_status.emit(False) return True except Exception as e: self.status_changed.emit(f"PCM audio unregistration error: {str(e)}") print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销错误: {str(e)}") # 出错时也发送停止信号 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 注销出错,但仍发送停止信号") self.pcm_audio_status.emit(False) return False def _ensure_pcm_audio_unregistered(self): """确保PCM音频被取消注册""" print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 确保PCM音频已注销") # 首先确保通话状态正确 self.in_call = False # 强制设置为非通话状态,确保在所有情况下状态一致 # 直接取消注册PCM音频 result = self._unregister_pcm_audio() if result: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销成功完成") else: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销可能未完成,但已发送停止信号") # 返回实际的操作结果,以便调用者可以适当处理 return result def make_call(self, number): """Make a phone call (MO - Mobile Originated call)""" if not self.connected: return False # 如果已经在通话中,先结束当前通话 if self.in_call: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 已在通话中,先结束当前通话") self.end_call() # 使用循环检查通话状态,而不是固定等待时间 wait_start = time.time() while self.in_call and time.time() - wait_start < 3.0: # 最多等待3秒 time.sleep(0.1) if self.in_call: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 无法结束先前通话,放弃拨号") self.status_changed.emit("Failed to end previous call") return False # 确保PCM音频已经关闭 self._ensure_pcm_audio_unregistered() # 设置模块为语音模式 try: self.send_at_command("AT+FCLASS=8") # 设置为语音模式,确保正确处理语音呼叫 except Exception as e: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 设置语音模式出错: {str(e)}") # 发起拨号命令 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发起拨打电话到 {number}") response = self.send_at_command(f"ATD{number};") if "OK" in response: self.call_number = number self.status_changed.emit(f"Calling {number}") print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 正在拨打 {number}") # 注意:设置in_call=True应该在收到VOICE CALL: BEGIN之后 # 这里只记录目标号码,不立即设置呼叫状态 # 在收到VOICE CALL: BEGIN事件后,会自动设置in_call=True并注册PCM音频 return True else: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 拨打电话失败: {response}") self.status_changed.emit(f"Failed to call {number}") return False def answer_call(self): """Answer an incoming call.""" # 检查是否有来电 self.log_at_interaction("尝试接听电话...") call_status = self.get_call_status() has_incoming = False for call in call_status: if call.get('state') == 'incoming': has_incoming = True break if not has_incoming: self.log_at_interaction("没有检测到来电,无法接听") return False # 确保在接听前停止铃声 try: from sound_utils import stop_all_ringtones stop_all_ringtones() except Exception as e: self.log_at_interaction(f"停止铃声时出错: {str(e)}") # 先确保PCM音频被注销,避免冲突 self._ensure_pcm_audio_unregistered() # 发送接听命令 self.log_at_interaction("发送ATA命令接听电话") response = self.send_at_command("ATA", timeout=10) if "OK" in response or "CONNECT" in response: self.log_at_interaction("电话已接通,准备注册PCM音频") # 创建一个延时,给电话接通一点时间后再注册PCM音频 def register_pcm_with_delay(): time.sleep(1) # 等待1秒确保电话真正接通 self._ensure_pcm_audio_registered() self.log_at_interaction("PCM音频已注册,通话已建立") # 使用线程实现延时注册,避免主线程阻塞 threading.Thread(target=register_pcm_with_delay, daemon=True).start() # 再次检查呼叫状态,确认是否真正接通 time.sleep(2) # 等待足够的时间让状态更新 new_call_status = self.get_call_status() for call in new_call_status: if call.get('state') == 'active': self.log_at_interaction("确认电话已接通") return True self.log_at_interaction("电话可能未成功接通,请再次检查状态") return True # 即使状态检查不是active,仍然返回True因为命令执行成功 else: self.log_at_interaction(f"接听电话失败: {response}") return False def end_call(self): """结束当前通话,根据通话状态使用不同的挂断命令""" if not self.connected: return False # 获取当前通话状态 calls = self.get_call_status() print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 当前通话状态: {calls}") # 根据通话状态选择合适的挂断命令 if not calls: # 没有活动通话,但为安全起见仍发送挂断命令 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 无活动通话,但仍发送挂断命令") response = self.send_at_command("ATH") else: # 检查第一个通话的状态 call = calls[0] stat = call.get('stat', -1) if stat == 4: # 来电中(MT) # 来电振铃状态,使用 AT+CHUP 命令挂断 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 使用AT+CHUP挂断未接通的来电") response = self.send_at_command("AT+CHUP") else: # 其他状态使用 ATH 命令挂断 print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 使用ATH挂断通话,状态: {self.call_states.get(stat, '未知')}") response = self.send_at_command("ATH") if "OK" in response: self.in_call = False self.call_connected = False self.status_changed.emit("通话结束") print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通话已结束") # 通话结束后,立即取消PCM音频注册 self._ensure_pcm_audio_unregistered() return True print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 挂断通话失败,响应: {response}") return False def send_sms(self, number, message): """Send SMS message""" if not self.connected: return False # Format the phone number formatted_number = format_phone_number(number) # 清空响应缓冲区 with self.command_lock: self.response_buffer.clear() self.last_response_time = time.time() # Set text mode and wait for OK response response = self.send_at_command("AT+CMGF=1") if "OK" not in response: self.status_changed.emit("Failed to set SMS text mode") return False # Add debug message self.status_changed.emit(f"Sending SMS to {formatted_number}") try: # Check if message contains Chinese characters if is_chinese_text(message): # Set character set to UCS2 for Unicode support response = self.send_at_command('AT+CSCS="UCS2"') if "OK" not in response: self.status_changed.emit("Failed to set UCS2 character set") return False # Convert message to UCS2 hex string hex_message = text_to_ucs2(message) if not hex_message: self.status_changed.emit("Failed to encode message") return False # Convert phone number to UCS2 format hex_number = text_to_ucs2(formatted_number) if not hex_number: self.status_changed.emit("Failed to encode phone number") return False # 清空响应缓冲区 with self.command_lock: self.response_buffer.clear() # Send message command with UCS2 encoded phone number cmd = f'AT+CMGS="{hex_number}"' self.at_serial.write((cmd + '\r').encode()) time.sleep(0.5) # Wait for > prompt # Send message content and Ctrl+Z to end self.at_serial.write(hex_message.encode() + b'\x1A') self.status_changed.emit("Sending UCS2 encoded message...") else: # Set character set to GSM for ASCII support response = self.send_at_command('AT+CSCS="GSM"') if "OK" not in response: self.status_changed.emit("Failed to set GSM character set") return False # 清空响应缓冲区 with self.command_lock: self.response_buffer.clear() # Send message command cmd = f'AT+CMGS="{formatted_number}"' self.at_serial.write((cmd + '\r').encode()) time.sleep(0.5) # Wait for > prompt # Send message content and Ctrl+Z to end self.at_serial.write(message.encode() + b'\x1A') self.status_changed.emit("Sending ASCII message...") # Wait for response with longer timeout start_time = time.time() response_lines = [] while time.time() - start_time < 15.0: # Increased timeout to 15 seconds # 检查是否有新响应 if self.response_buffer: with self.command_lock: # 获取并移除第一个响应 line = self.response_buffer.pop(0) response_lines.append(line) self.status_changed.emit(f"SMS response: {line}") if "+CMGS:" in line: self.status_changed.emit(f"SMS sent to {formatted_number}") return True elif "ERROR" in line or "+CMS ERROR:" in line: self.status_changed.emit(f"SMS error: {line}") return False # 短暂等待新响应 time.sleep(0.1) # If we get here, we timed out waiting for a response last_response = response_lines[-1] if response_lines else 'None' self.status_changed.emit(f"SMS send timeout. Last response: {last_response}") return False except Exception as e: self.status_changed.emit(f"SMS send exception: {str(e)}") return False def delete_sms(self, index=None, delete_type=None): """Delete SMS messages delete_type: 0 - Delete message at index 1 - Delete all read messages 2 - Delete all read and sent messages 3 - Delete all read, sent and unsent messages 4 - Delete all messages """ if not self.connected: return False if index is not None and delete_type is not None: command = f"AT+CMGD={index},{delete_type}" elif index is not None: command = f"AT+CMGD={index}" elif delete_type is not None: command = f"AT+CMGD=1,{delete_type}" else: return False response = self.send_at_command(command) if "OK" in response: self.status_changed.emit("SMS deleted") return True return False def get_sms_list(self, status="ALL"): """Get list of SMS messages status: "REC UNREAD" - Unread messages "REC READ" - Read messages "STO UNSENT" - Stored unsent messages "STO SENT" - Stored sent messages "ALL" - All messages """ if not self.connected: return [] # Set text mode self.send_at_command("AT+CMGF=1") # Get messages if status == "ALL": response = self.send_at_command("AT+CMGL") else: response = self.send_at_command(f'AT+CMGL="{status}"') if not response or "OK" not in response: return [] messages = [] lines = response.split('\n') i = 0 while i < len(lines): line = lines[i] if line.startswith("+CMGL:"): # Parse header header_match = re.search(r'\+CMGL: (\d+),"([^"]*)",' r'"([^"]*)",[^,]*,"([^"]*)"', line) if header_match: index = header_match.group(1) msg_status = header_match.group(2) sender = header_match.group(3) timestamp = header_match.group(4) # Get message content if i + 1 < len(lines): content = lines[i + 1] # Check if PDU or text mode if any(c for c in content if not (c.isalnum() or c.isspace() or c in '+-,.;:!?')): # Likely PDU data, decode it content = self._decode_pdu_message(content) messages.append({ 'index': index, 'status': msg_status, 'sender': sender, 'timestamp': timestamp, 'content': content }) i += 2 # Skip content line else: i += 1 else: i += 1 else: i += 1 return messages def get_network_info(self): """获取网络信息(优先使用缓存)""" # 检查缓存 cached_value = self._get_cached_value('network') if cached_value is not None: return cached_value if not self.connected: return None # 缓存无效,重新获取网络信息 try: self._update_carrier_info() # 网络信息在_update_carrier_info中更新 # 更新缓存 self._update_cache('network', self.network_type) return self.network_type except Exception as e: print(f"获取网络信息出错: {str(e)}") return None def get_signal_strength(self): """Get signal strength""" if not self.connected: return "未连接" try: self._update_signal_strength() # 确保返回正确格式的信号强度信息 if hasattr(self, 'signal_strength') and self.signal_strength: if isinstance(self.signal_strength, tuple) and len(self.signal_strength) == 2: # 已经是(signal_text, signal_desc)格式 return self.signal_strength elif isinstance(self.signal_strength, str): # 如果是字符串,将其作为信号描述返回 return (self.signal_strength, "") else: # 不支持的格式,转换为字符串 return (str(self.signal_strength), "") else: # 没有信号强度信息 return ("未知", "信号数据不可用") except Exception as e: self.log_at_interaction(f"获取信号强度失败: {str(e)}") print(f"获取信号强度失败: {str(e)}") return ("错误", str(e)[:20]) def get_call_status(self): """Get current call status""" if not self.connected: return [] # 记录CLCC命令调用 static_var_name = "_clcc_call_count" if not hasattr(self, static_var_name): setattr(self, static_var_name, 0) # 增加计数器 current_count = getattr(self, static_var_name) + 1 setattr(self, static_var_name, current_count) # 记录到日志 self.log_at_command(True, f"AT+CLCC (第{current_count}次调用)") print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 调用AT+CLCC检查通话状态 (第{current_count}次)") try: # Send AT+CLCC command to get call status response = self.send_at_command("AT+CLCC") if not response or "OK" not in response: return [] calls = [] lines = response.split('\n') for line in lines: line = line.strip() if not line or line == "OK" or not line.startswith("+CLCC:"): continue # Parse +CLCC response # Format: +CLCC: ,,,,[,,[,]] parts = line.replace("+CLCC:", "").strip().split(",") if len(parts) < 5: continue call = { 'id': int(parts[0]), 'dir': int(parts[1]), # 0=MO, 1=MT 'stat': int(parts[2]), # 0=active, 1=held, 2=dialing, 3=alerting, 4=incoming, 5=waiting 'mode': int(parts[3]), # 0=voice, 1=data, 2=fax 'mpty': int(parts[4]) # 0=not multiparty, 1=multiparty } # Check if number is included if len(parts) >= 7: # Remove quotes from number number = parts[5].strip('"') call['number'] = number # Check if alpha is included if len(parts) >= 8: alpha = parts[7].strip('"') call['alpha'] = alpha calls.append(call) # 如果通话状态发生变化,记录日志 if not hasattr(self, '_last_call_status') or self._last_call_status != calls: self._last_call_status = calls call_desc = self._format_call_status_for_log(calls) self.log_at_command(False, f"CLCC响应解析: {call_desc}") return calls except Exception as e: print(f"Error getting call status: {str(e)}") return [] def _format_call_status_for_log(self, calls): """格式化通话状态用于日志记录""" if not calls: return "无通话" status_texts = [] for call in calls: direction = "呼出" if call.get('dir') == 0 else "呼入" status_code = call.get('stat', -1) if status_code == 0: status = "活动" elif status_code == 1: status = "保持" elif status_code == 2: status = "拨号中" elif status_code == 3: status = "振铃中" elif status_code == 4: status = "来电中" elif status_code == 5: status = "等待中" else: status = f"未知({status_code})" number = call.get('number', '未知号码') status_texts.append(f"{direction}/{status}/{number}") return ", ".join(status_texts) def get_call_state_text(self): """ 获取当前通话状态的文本描述 """ calls = self.get_call_status() if not calls: return "无通话" # 获取第一个通话的状态描述 call = calls[0] stat = call.get('stat', -1) state_text = self.call_states.get(stat, "未知状态") # 添加方向信息 direction = "呼出" if call.get('dir', 0) == 0 else "呼入" # 添加号码信息 number = call.get('number', '') number_text = f", 号码: {number}" if number else "" return f"{direction}通话, {state_text}{number_text}" def is_call_connected(self): """检查通话是否已接通(不仅仅是振铃状态)""" # 获取最新通话状态 calls = self.get_call_status() # 如果没有通话,则未接通 if not calls: return False # 检查第一个通话是否处于活动状态(stat=0) call = calls[0] return call.get('stat', -1) == 0 def _get_cached_value(self, cache_key): """获取缓存值,如果有效则返回缓存,否则返回None""" cache_item = self.info_cache.get(cache_key) if not cache_item: return None current_time = time.time() if cache_item['value'] is not None and (current_time - cache_item['timestamp']) < cache_item['valid_time']: return cache_item['value'] return None def _update_cache(self, cache_key, value): """更新缓存值""" if cache_key in self.info_cache: self.info_cache[cache_key]['value'] = value self.info_cache[cache_key]['timestamp'] = time.time() def _update_phone_number(self): """Update phone number information""" try: response = self.send_at_command("AT+CNUM") if not response: self.phone_number = "无法获取号码" self._update_cache('phone_number', self.phone_number) return self.phone_number if "+CNUM:" in response: match = re.search(r'\+CNUM: "[^"]*","([^"]+)"', response) if match: self.phone_number = match.group(1) # 保存到缓存 self._update_cache('phone_number', self.phone_number) return self.phone_number else: # 如果匹配失败,尝试其他格式 match = re.search(r'\+CNUM:.*?"([^"]*)"', response) if match: self.phone_number = match.group(1) # 保存到缓存 self._update_cache('phone_number', self.phone_number) return self.phone_number # 如果无法获取电话号码,尝试查询SIM卡状态 sim_response = self.send_at_command("AT+CPIN?") if sim_response and "READY" in sim_response: # 尝试获取SIM卡IMSI sim_info = self.send_at_command("AT+CIMI") if sim_info and "OK" in sim_info: self.phone_number = "SIM卡已就绪" # 保存到缓存 self._update_cache('phone_number', self.phone_number) return self.phone_number # 如果所有尝试都失败 self.phone_number = "无法获取号码" # 保存到缓存 self._update_cache('phone_number', self.phone_number) return self.phone_number except Exception as e: self.log_at_interaction(f"获取电话号码失败: {str(e)}") print(f"获取电话号码失败: {str(e)}") self.phone_number = "获取号码出错" # 保存到缓存 self._update_cache('phone_number', self.phone_number) return self.phone_number def _ensure_pcm_audio_registered(self): """确保PCM音频被注册(如果尚未注册)""" if not self.pcm_registered: print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 注册PCM音频") result = self._register_pcm_audio() self.pcm_registered = True return result return True def log_at_interaction(self, message): """记录AT交互日志(简化的接口,内部使用log_at_command)""" self.log_at_command(False, f"[INFO] {message}")