Files
LTE_manager/lte_manager.py
T

2756 lines
118 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import serial
import threading
import time
import re
import binascii
import queue
import os
from PyQt5.QtCore import QObject, pyqtSignal, QDateTime, QTimer
from sms_utils import text_to_ucs2, ucs2_to_text, is_chinese_text, format_phone_number
# Import serial.tools.list_ports for port detection
import serial.tools.list_ports
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.at_serial = None
self.nmea_serial = None
self.at_port = ""
self.nmea_port = ""
self.at_baudrate = 115200
self.nmea_baudrate = 9600
self.connected = False
self.running = False # Flag to control the read thread
self.read_thread = None
self.response_queue = queue.Queue()
self.lock = threading.Lock()
# Command cache
self.command_cache = {}
# Module information
self.imei = ""
self.imsi = ""
self.model = ""
self.manufacturer = ""
self.firmware = ""
self.phone_number = ""
self.carrier = ""
self.network_type = ""
self.signal_strength = ""
# Call status
self.in_call = False
self.call_connected = False # 标记通话是否已经接通(区分来电振铃和通话接通)
self.call_number = ""
self.call_notification_sent = False # Flag to track if we've already notified about this call
self.call_states = {
0: "正在进行", # active
1: "保持", # hold
2: "拨号中", # dialing (MO)
3: "振铃中", # alerting (MO)
4: "来电中", # incoming (MT)
5: "等待中" # waiting (MT)
}
# SMS handling
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.cleanup_timer = QTimer()
self.cleanup_timer.timeout.connect(self._cleanup_old_sms_parts)
self.cleanup_timer.start(10000) # 每10秒清理一次
# 添加AT命令日志文件路径
self.at_log_file = None
self._setup_at_log_file()
def _setup_at_log_file(self):
"""设置AT命令日志文件"""
try:
# 确保.LTE目录存在
home_dir = os.path.expanduser("~")
lte_dir = os.path.join(home_dir, ".LTE")
if not os.path.exists(lte_dir):
os.makedirs(lte_dir)
# 清理旧日志文件(保留最近7天的日志)
self._cleanup_old_log_files(lte_dir)
# 创建基于日期的日志文件 - 使用time模块获取当前日期
today = time.strftime("%Y-%m-%d")
log_file_path = os.path.join(lte_dir, f"at_commands_{today}.log")
# 以追加模式打开日志文件
self.at_log_file = open(log_file_path, "a", encoding="utf-8")
print(f"AT命令日志文件已创建: {log_file_path}")
# 记录会话开始 - 使用time模块获取当前时间
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
self.at_log_file.write(f"\n===== LTE管理器会话开始 {timestamp} =====\n")
self.at_log_file.flush()
return True
except Exception as e:
print(f"创建AT命令日志文件失败: {str(e)}")
self.at_log_file = None
return False
def _cleanup_old_log_files(self, log_dir, max_days=7):
"""清理旧的日志文件,只保留最近max_days天的日志"""
try:
# 获取当前时间戳
current_time = time.time()
# 计算max_days天前的时间戳
max_age = current_time - (max_days * 24 * 60 * 60)
# 遍历日志目录
for file in os.listdir(log_dir):
# 只处理AT命令日志文件
if file.startswith('at_commands_') and file.endswith('.log'):
file_path = os.path.join(log_dir, file)
# 获取文件修改时间
file_time = os.path.getmtime(file_path)
# 如果文件修改时间早于max_days天前,则删除
if file_time < max_age:
os.remove(file_path)
print(f"已删除旧日志文件: {file}")
except Exception as e:
print(f"清理旧日志文件时出错: {str(e)}")
def _log_at_interaction(self, command, response=None):
"""记录AT命令交互"""
try:
if self.at_log_file:
# 使用time模块获取时间戳
timestamp = time.strftime("%Y-%m-%d %H:%M:%S.") + f"{int(time.time() * 1000) % 1000:03d}"
if command is not None:
# 只记录发送的命令
self.at_log_file.write(f"{timestamp} >>> {command}\n")
self.at_log_file.flush()
except Exception as e:
print(f"记录AT命令时出错: {str(e)}")
def _log_response(self, command, response):
"""单独记录AT命令的响应,避免重复记录命令"""
try:
if self.at_log_file:
# 使用time模块获取时间戳
timestamp = time.strftime("%Y-%m-%d %H:%M:%S.") + f"{int(time.time() * 1000) % 1000:03d}"
if response:
self.at_log_file.write(f"{timestamp} <<< {response}\n")
self.at_log_file.flush()
except Exception as e:
print(f"记录AT命令响应时出错: {str(e)}")
def _log_unsolicited(self, response):
"""记录非请求的响应,使用独立的格式"""
try:
if self.at_log_file:
# 使用time模块获取时间戳
timestamp = time.strftime("%Y-%m-%d %H:%M:%S.") + f"{int(time.time() * 1000) % 1000:03d}"
self.at_log_file.write(f"{timestamp} <UNSOLICITED> {response}\n")
self.at_log_file.flush()
except Exception as e:
print(f"记录非请求响应时出错: {str(e)}")
def connect(self, port=None, baudrate=115200):
"""Connect to the LTE module"""
try:
# Check if already connected
if self.is_connected():
print("Already connected")
return True
# Initialize command cache
self.command_cache = {}
# 重置连接状态
self.connected = False
# Validate port
if not port:
# Try to auto-detect port
port = self._auto_detect_port()
if not port:
self.status_changed.emit("Error: No serial port detected")
return False
self.status_changed.emit(f"Connecting to {port}...")
print(f"尝试连接到端口: {port}, 波特率: {baudrate}")
# Make sure any previous connection is properly closed
try:
if hasattr(self, 'at_serial') and self.at_serial:
self.at_serial.close()
time.sleep(0.5) # Increased delay to give OS more time to release the port
print(f"已关闭之前的串口连接,等待500ms")
except Exception as e:
print(f"Warning: Error closing previous serial connection: {str(e)}")
# Connect to the serial port
try:
print(f"打开串口: {port}")
self.at_serial = serial.Serial(
port=port,
baudrate=baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=2.0, # Increased timeout
write_timeout=1.0,
xonxoff=False,
rtscts=False,
dsrdtr=False
)
print(f"串口已打开,初始化响应队列")
# Check if the port is open
if not self.at_serial.is_open:
print(f"错误: 串口未能打开")
return False
# Initialize the response queue
self.response_queue = queue.Queue()
# Clear any pending data
self.at_serial.reset_input_buffer()
self.at_serial.reset_output_buffer()
print(f"串口缓冲区已重置")
# Set the running flag before starting the thread
self.running = True
# Start the read thread
self.read_thread = threading.Thread(target=self._read_thread, daemon=True)
self.read_thread.start()
print(f"读取线程已启动")
# 确保日志文件已创建 (但不重复创建)
if not self.at_log_file:
print("创建AT命令日志文件...")
self._setup_at_log_file()
# Wait for the thread to start reading
time.sleep(0.2)
# 直接尝试写入一个空行到串口,测试是否可用
try:
self.at_serial.write(b'\r\n')
time.sleep(0.1)
print("发送空行成功")
except Exception as e:
print(f"发送空行失败: {str(e)}")
# Send AT command multiple times to ensure connection
print(f"发送AT测试命令...")
response = ""
for attempt in range(3):
try:
# 直接使用串口写入,而不是send_at_command
if attempt == 0:
self.at_serial.write(b'AT\r\n')
print("直接写入AT命令")
time.sleep(0.2)
# 读取响应
data = self.at_serial.read(self.at_serial.in_waiting or 100)
if data:
response = data.decode('utf-8', errors='replace')
print(f"直接读取响应: {response}")
if "OK" in response:
print("直接通信成功!")
self.connected = True
break
else:
# 使用正常方法发送命令
response = self.send_at_command("AT", timeout=2.0, retries=1)
print(f"AT命令尝试 {attempt+1}/3 响应: {response}")
if "OK" in response or self.connected:
break
except Exception as e:
print(f"AT命令尝试 {attempt+1} 失败: {str(e)}")
time.sleep(0.5)
if self.connected: # 使用self.connected标志,该标志在send_at_command中设置
self.status_changed.emit(f"Connected to {port}")
print(f"成功连接到 {port}")
self.port = port
self.baudrate = baudrate
# Configure the module
print(f"开始配置模块...")
self._configure_module()
return True
else:
self.running = False # Stop the read thread
self.status_changed.emit("Error: Module not responding")
print(f"错误: 模块未响应, 响应内容: {response}")
if hasattr(self, 'at_serial') and self.at_serial and self.at_serial.is_open:
self.at_serial.close()
return False
except Exception as e:
self.running = False # Make sure thread stops if an error occurs
self.status_changed.emit(f"Error connecting: {str(e)}")
print(f"连接错误: {str(e)}")
return False
except Exception as e:
self.status_changed.emit(f"Error in connect: {str(e)}")
print(f"连接过程中发生错误: {str(e)}")
return False
def disconnect(self):
"""Disconnect from the LTE module"""
try:
if self.is_connected():
# Stop the read thread
self.running = False
if self.read_thread and self.read_thread.is_alive():
try:
self.read_thread.join(1.0) # Wait for thread to finish, timeout after 1 second
except Exception as e:
print(f"Warning: Error waiting for read thread: {str(e)}")
# Log disconnection
if self.at_log_file:
# 使用time模块获取时间戳
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
self.at_log_file.write(f"\n===== LTE管理器会话结束 {timestamp} =====\n\n")
self.at_log_file.flush()
self.at_log_file.close()
print(f"AT命令日志文件已关闭: {self.at_log_file.name}")
self.at_log_file = None
# Close the serial port
if hasattr(self, 'at_serial') and self.at_serial:
self.at_serial.close()
self.connected = False
self.status_changed.emit("Disconnected")
return True
return False
except Exception as e:
self.status_changed.emit(f"Error disconnecting: {str(e)}")
return False
def is_connected(self):
"""Check if connected to the LTE module"""
return self.connected
def send_at_command(self, command, timeout=2.0, retries=2, use_cache=False):
"""发送AT命令并等待响应"""
# 如果使用缓存并且命令有缓存
if use_cache and command in self.command_cache:
cache_time, cache_result = self.command_cache[command]
# 检查缓存是否过期 (500ms)
if time.time() - cache_time < 0.5:
# 记录使用了缓存
print(f"使用缓存结果: {command}")
self._log_at_interaction(command, f"[CACHED] {cache_result}")
return cache_result
# 初始化重试计数器
retry_count = 0
print(f"发送AT命令: {command}, 超时: {timeout}秒, 最大重试次数: {retries}")
while retry_count < retries:
try:
# 检查串口是否已打开(不检查self.connected标志)
if not hasattr(self, 'at_serial') or not self.at_serial or not self.at_serial.is_open:
error_msg = "ERROR: Serial port not open"
print(f"命令发送失败: {error_msg}")
self._log_at_interaction(command, error_msg)
return error_msg
# 清空输入缓冲区
try:
self.at_serial.reset_input_buffer()
print(f"输入缓冲区已清空")
except Exception as e:
print(f"清空输入缓冲区失败: {str(e)}")
# 记录发送的AT命令
self._log_at_interaction(command, None)
# 发送命令
cmd = command + "\r\n"
bytes_written = self.at_serial.write(cmd.encode())
print(f"发送命令: {command},已写入 {bytes_written} 字节")
# 确保命令已发送
self.at_serial.flush()
print(f"命令已刷新到设备")
# 等待并读取响应
response = self._read_serial(timeout)
print(f"收到命令响应: {response}")
# 检查响应
if "ERROR" in response:
# 记录错误响应
self._log_response(command, response)
# 区分不同类型的错误
# 1. 查询命令返回ERROR - 这通常表示命令不支持,不需要重试
if command.endswith("=?") or command.endswith("?"):
print(f"命令不支持: {command} -> {response}")
# 将响应缓存并返回,不重试
self.command_cache[command] = (time.time(), response)
return response
# 2. CME ERROR或CMS ERROR - 这是带错误代码的特定错误,说明命令被识别但执行失败
if "+CME ERROR:" in response or "+CMS ERROR:" in response:
print(f"命令执行错误: {command} -> {response}")
# 如果是特定错误代码,不需要重试
self.command_cache[command] = (time.time(), response)
return response
# 3. 普通ERROR - 可能需要重试的通信问题
print(f"命令执行错误: {command} -> {response}, 重试 {retry_count+1}/{retries}")
retry_count += 1
time.sleep(0.5) # 出错时延迟后重试
continue
else:
# 命令成功,记录并缓存响应
self._log_response(command, response)
print(f"命令执行成功: {command}")
# 如果这是AT命令并且响应包含OK,则设置connected标志
if command == "AT" and "OK" in response:
self.connected = True
print("连接状态已设置为已连接")
# 缓存响应结果
self.command_cache[command] = (time.time(), response)
return response
except Exception as e:
print(f"命令 {command} 执行时出错: {str(e)}")
error_msg = f"ERROR: {str(e)}"
retry_count += 1
time.sleep(0.5) # 出错时延迟后重试
continue
# 达到最大重试次数后返回错误
print(f"命令 {command} 已达到最大重试次数 {retries},放弃执行")
return f"ERROR: Max retries ({retries}) exceeded"
def _read_thread(self):
"""Thread function to continuously read from serial port"""
buffer = ""
print("Serial read thread started")
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
if self.at_serial.in_waiting > 0:
data = self.at_serial.read(self.at_serial.in_waiting)
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
# Process unsolicited responses
self._process_unsolicited(line)
# Add to response queue for command responses
self.response_queue.put(line)
else:
# No data available, sleep briefly to reduce CPU usage
time.sleep(0.01)
except Exception as e:
print(f"Serial read error: {str(e)}")
time.sleep(0.1)
print("Serial read thread stopped")
def _read_serial(self, timeout=5.0):
"""读取串口响应,直到超时或收到完整响应"""
start_time = time.time()
response = []
command_echo_received = False
print(f"等待AT命令响应,最大超时时间: {timeout}秒")
# 等待响应,直到超时
while time.time() - start_time < timeout:
try:
# 使用queue.get()获取响应行
line = self.response_queue.get(timeout=0.2)
print(f"收到响应行: {line}")
# 检查是否是命令回显(某些模块会回显命令)
if not command_echo_received and line.startswith("AT"):
command_echo_received = True
# 可以选择跳过命令回显行,不添加到响应中
continue
response.append(line)
# 检查是否为完整响应的各种情况
# 1. 标准的OK结束
if line == "OK":
print("收到完整响应标识,结束等待")
break
# 2. ERROR结束
elif line == "ERROR" or "+CMS ERROR:" in line or "+CME ERROR:" in line:
print("收到完整响应标识,结束等待")
break
# 3. 没有正常的结束标记,但对于某些特殊响应需要特殊处理
# 例如,对于COPS?命令如果收到了+COPS:行,可能不需要等待OK
elif "+COPS:" in line and "OK" not in response:
# 继续等待OK,不直接退出
pass
# 4. 对于返回消息体的命令,等待足够长的时间确保收到完整消息
# 特别是短信内容、通话状态等
elif "+CMGR:" in line or "+CMGL:" in line or "+CLCC:" in line:
# 继续等待,这些命令通常有多行响应
pass
except queue.Empty:
# 如果队列为空,检查是否已有足够的响应内容
# 如果已经收到至少一行,且最后一行包含某些特定结束标记,也可以结束等待
if response:
# 检查最后一行是否是某种结束标记
last_line = response[-1]
if last_line in ["OK", "ERROR"] or "+CMS ERROR:" in last_line or "+CME ERROR:" in last_line:
print("在队列等待期间确认已收到完整响应")
break
# 对于某些命令,没有明确结束标记,但收到特定响应后短时间内没有更多响应,也可视为完成
# 例如,AT+CSQ后只有一行+CSQ:响应,但没有OK
if ("+CSQ:" in last_line or "+CREG:" in last_line or "+CGREG:" in last_line) and \
time.time() - start_time > 1.0: # 等待至少1秒以确保无更多响应
print("已收到关键响应行且无后续内容,视为完成")
break
# 如果没有收到任何响应,返回超时错误
if not response:
print("读取超时,未收到任何响应")
return "ERROR: Read timeout"
# 合并所有响应行并返回
full_response = "\n".join(response)
return full_response
def _process_unsolicited(self, line):
"""处理非请求响应"""
# 不把AT命令及其响应作为unsolicited response处理
if line.startswith("AT") or line == "OK" or line == "ERROR" or line.startswith("+CSQ") or line.startswith("+CREG") or line.startswith("+CGREG"):
# 跳过可能的命令回显或常见查询响应
return
# 对于真正的非请求通知,记录并处理
self._log_unsolicited(line)
# 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:"):
try:
# 提取发送者号码和时间戳,用于后续匹配和合并短信
sender_match = re.search(r'\+CMT: "([^"]*)",[^,]*,"([^"]*)"', line)
if sender_match:
sender = sender_match.group(1)
timestamp = sender_match.group(2)
# 解码发送者号码(如果是UCS2编码)
if sender.startswith("00"):
try:
sender = ucs2_to_text(sender)
except Exception as e:
print(f"解码发送者号码出错: {str(e)}")
# 保存短信头部信息,用于后续处理
self.pending_sms_sender = sender
self.pending_sms_timestamp = timestamp
# 生成SMS ID用于匹配多段短信
sms_id = f"{sender}_{timestamp[:10]}"
# 检查是否已有相同ID的短信正在处理中
is_continuation = False
if sms_id in self.concat_sms_parts:
# 这是已有短信的后续部分
is_continuation = True
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 检测到后续短信部分: {sms_id}")
self.status_changed.emit(f"检测到后续短信部分,来自 {sender}")
# 标记等待内容行
self.waiting_for_sms_content = True
# 保存当前短信ID,用于内容行处理
self.current_sms_id = sms_id
self.current_is_continuation = is_continuation
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 收到短信头部,发送者: {sender}, 时间: {timestamp}, ID: {sms_id}")
else:
# 无法解析发送者和时间,使用默认处理方式
if self._is_concatenated_sms(line):
self._handle_concatenated_sms(line)
else:
self._handle_regular_sms(line)
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 处理短信头部出错: {str(e)}")
# 错误时使用旧方法尝试处理
if self._is_concatenated_sms(line):
self._handle_concatenated_sms(line)
else:
self._handle_regular_sms(line)
elif self.waiting_for_sms_content:
# 这是短信内容行
self.waiting_for_sms_content = False
message = line
# 检查是否保存了短信ID信息
sms_id = getattr(self, 'current_sms_id', None)
is_continuation = getattr(self, 'current_is_continuation', False)
# 清除临时属性
if hasattr(self, 'current_sms_id'):
del self.current_sms_id
if hasattr(self, 'current_is_continuation'):
del self.current_is_continuation
try:
# 检查是否是UCS2编码
if all(c in "0123456789ABCDEFabcdef" for c in line.replace(" ", "")):
# 尝试解码UCS2内容
decoded_content = None
try:
decoded_content = ucs2_to_text(line)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - UCS2内容解码成功: {decoded_content[:50]}...")
except Exception as decode_error:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - UCS2解码错误: {str(decode_error)}")
# 尝试替代解码方法
try:
hex_bytes = binascii.unhexlify(line.replace(" ", ""))
decoded_content = hex_bytes.decode('utf-16-be', errors='replace')
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 替代解码成功: {decoded_content[:50]}...")
except Exception as alt_error:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 替代解码也失败: {str(alt_error)}")
# 如果是长短信的一部分(根据特定特征判断)
is_long_message_part = False
if "62117ED94F6053D14E86957F6587672C" in line:
is_long_message_part = True
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 检测到长短信特征")
elif decoded_content and "https://" in decoded_content:
is_long_message_part = True
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 检测到URL内容,视为长短信")
# 处理长短信
if is_long_message_part or is_continuation:
# 处理为长短信的一部分
self._process_long_message_part(self.pending_sms_sender, self.pending_sms_timestamp, line, decoded_content, sms_id)
else:
# 常规短信处理,直接发送解码后的内容
message = decoded_content if decoded_content else line
self.sms_received.emit(
self.pending_sms_sender,
self.pending_sms_timestamp,
message
)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送常规短信到UI")
else:
# 非UCS2编码,直接发送
self.sms_received.emit(
self.pending_sms_sender,
self.pending_sms_timestamp,
message
)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送纯文本短信到UI")
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 处理短信内容时出错: {str(e)}")
# 出错时尝试直接发送原始内容
self.sms_received.emit(
self.pending_sms_sender,
self.pending_sms_timestamp,
f"[解码错误] {message[:100]}..."
)
# 清除待处理短信数据
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 _process_long_message_part(self, sender, timestamp, content, decoded_content, sms_id):
"""处理长短信的一部分,支持追加入库功能"""
try:
# 移除空格
content = content.replace(" ", "")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 处理长短信部分,ID: {sms_id}, 内容长度: {len(content)}")
# 特殊格式检测
is_special_format = "62117ED94F6053D14E86957F6587672C" in content
# 如果没有解码后的内容,尝试解码
if not decoded_content:
try:
decoded_content = ucs2_to_text(content)
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 解码UCS2内容出错: {str(e)}")
try:
# 尝试替代解码方法
hex_bytes = binascii.unhexlify(content)
decoded_content = hex_bytes.decode('utf-16-be', errors='replace')
except Exception as alt_e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 替代解码方法失败: {str(alt_e)}")
decoded_content = f"[无法解码] {content[:50]}..."
# 提取URL (如果有)
url = None
if is_special_format and decoded_content:
# 尝试从特殊格式中提取URL
try:
url_match = re.search(r':(https?://[^\s]+)', decoded_content)
if url_match:
url = url_match.group(1)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 从特殊格式中提取URL: {url}")
except Exception as url_e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 从特殊格式提取URL失败: {str(url_e)}")
# 如果没有提取到URL但有解码后的内容,尝试从普通文本中提取
if not url and decoded_content:
url_match = re.search(r'(https?://[^\s]+)', decoded_content)
if url_match:
url = url_match.group(1)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 从文本中提取URL: {url}")
# 初始化或更新长短信记录
if sms_id not in self.concat_sms_parts:
# 提取消息前缀(如果有)
prefix = "消息"
if decoded_content and ":" in decoded_content:
prefix_match = re.match(r'^([^:]+):', decoded_content)
if prefix_match:
prefix = prefix_match.group(1).strip()
# 创建新的长短信记录
self.concat_sms_parts[sms_id] = {
'sender': sender,
'timestamp': timestamp,
'parts': [],
'urls': [],
'received_time': time.time(),
'prefix': prefix,
'is_processed': False # 标记是否已处理
}
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 创建新的长短信记录: {sms_id}")
# 更新长短信记录
sms_record = self.concat_sms_parts[sms_id]
# 添加解码后的内容到parts
if decoded_content and decoded_content not in sms_record['parts']:
sms_record['parts'].append(decoded_content)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 添加第 {len(sms_record['parts'])} 部分到长短信记录")
# 添加URL到urls列表(如果有且不重复)
if url and url not in sms_record['urls']:
sms_record['urls'].append(url)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 添加URL到长短信记录: {url}")
# 更新接收时间
sms_record['received_time'] = time.time()
# 使用定时器,延迟合并处理长短信(等待其他部分到达)
# 如果是分条短信,设置较短的延迟;如果是长短信,设置较长的延迟
delay = 1.5 if len(sms_record['parts']) > 1 else 3.0
threading.Timer(delay, lambda: self._check_and_merge_sms(sms_id)).start()
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 设置 {delay} 秒后合并长短信")
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 处理长短信部分时出错: {str(e)}")
# 出错时尝试直接发送当前部分
try:
message = decoded_content if decoded_content else content
self.sms_received.emit(sender, timestamp, f"[长短信处理错误] {message[:100]}...")
except Exception as send_e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送错误消息失败: {str(send_e)}")
def _check_and_merge_sms(self, sms_id):
"""检查并合并长短信,支持追加内容到已处理的长短信"""
if sms_id not in self.concat_sms_parts:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 无法找到长短信记录: {sms_id}")
return
sms_info = self.concat_sms_parts[sms_id]
# 检查是否已处理过
if sms_info.get('is_processed', False):
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 长短信 {sms_id} 已处理过,检查是否有新部分")
# 如果已处理过但有新内容(最近3秒内收到的),则追加处理
current_time = time.time()
if current_time - sms_info['received_time'] < 3:
# 有新部分,继续等待更多部分
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 检测到新内容,延迟后再次尝试合并")
threading.Timer(2.0, lambda: self._check_and_merge_sms(sms_id)).start()
return
# 有新内容需要追加,重新合并并发送更新
merged_content = self._merge_sms_parts(sms_id)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送更新的长短信内容: {merged_content[:50]}...")
# 发送信号,表示这是更新的内容
self.status_changed.emit(f"更新长短信内容,来自 {sms_info['sender']}")
self.sms_received.emit(
sms_info['sender'],
sms_info['timestamp'],
merged_content
)
# 记录处理状态
sms_info['is_processed'] = True
# 更新上次处理时间
sms_info['last_processed'] = current_time
return
# 检查是否有有效部分
if not sms_info.get('parts', []):
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 长短信 {sms_id} 没有有效部分,跳过合并")
return
# 检查是否收到后续部分的超时(通常1-3秒内应该收到所有部分)
current_time = time.time()
time_since_last_part = current_time - sms_info['received_time']
# 如果最近2秒内收到新部分,继续等待
if time_since_last_part < 2.0:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 最近才收到新部分 ({time_since_last_part:.1f}秒前),继续等待")
return
# 超过等待时间,进行合并处理
merged_content = self._merge_sms_parts(sms_id)
# 发送完整消息
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送合并后的长短信: {merged_content[:50]}...")
self.sms_received.emit(
sms_info['sender'],
sms_info['timestamp'],
merged_content
)
# 标记为已处理
sms_info['is_processed'] = True
sms_info['last_processed'] = current_time
# 记录日志
self.status_changed.emit(f"已接收完整长短信,来自 {sms_info['sender']}")
# 不删除记录,而是保留用于后续追加处理
# 长短信记录将在清理定时任务中处理
def _is_concatenated_sms(self, header_line):
"""检查是否为长短信"""
# 检查是否符合长短信格式
try:
# 长短信格式通常包含更多逗号分隔的字段
parts = header_line.split(',')
# 普通短信头部通常有3个字段,长短信可能有更多
if len(parts) >= 6:
return True
return False
except Exception as e:
print(f"检查长短信格式出错: {str(e)}")
return False
def _handle_regular_sms(self, header_line):
"""处理普通短信"""
try:
# 解析SMS头部,格式通常为: +CMT: "sender","","timestamp"
header_match = re.search(r'\+CMT: "([^"]*)",[^,]*,"([^"]*)"', header_line)
if header_match:
sender = header_match.group(1)
timestamp = header_match.group(2)
# 检查发送者是否为UCS2格式(通常以00开头)
if sender.startswith("00"):
try:
sender = ucs2_to_text(sender)
except Exception as e:
print(f"解码发送者号码失败: {str(e)}")
# 解码失败时保留原始格式
# 保存发送者和时间信息,等待下一行接收内容
self.pending_sms_sender = sender
self.pending_sms_timestamp = timestamp
self.waiting_for_sms_content = True
# 发送状态更新
self.status_changed.emit(f"收到来自 {sender} 的短信")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 收到来自 {sender} 的短信")
else:
# 如果头部格式不匹配,使用默认值
self.pending_sms_sender = "未知号码"
self.pending_sms_timestamp = time.strftime("%y/%m/%d,%H:%M:%S")
self.waiting_for_sms_content = True
# 发送状态更新
self.status_changed.emit("收到短信(无法识别发送者)")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 收到短信(头部格式异常:{header_line}")
except Exception as e:
print(f"处理短信头部出错: {str(e)}")
# 出错时使用默认值
self.pending_sms_sender = "错误"
self.pending_sms_timestamp = time.strftime("%y/%m/%d,%H:%M:%S")
self.waiting_for_sms_content = True
self.status_changed.emit(f"解析短信头部时出错: {str(e)}")
def _handle_concatenated_sms(self, header_line):
"""处理长短信的头部信息"""
try:
# 解析长短信头部,格式可能更复杂
parts = header_line.split(',')
if len(parts) < 3:
# 格式不符合预期,作为普通短信处理
self._handle_regular_sms(header_line)
return
# 尝试提取发送者和时间戳
sender_part = parts[0].replace('+CMT: ', '').strip('"')
# 时间戳位置可能在第3个位置
timestamp_part = parts[2].strip('"') if len(parts) > 2 else time.strftime("%y/%m/%d,%H:%M:%S")
# 检查发送者是否为UCS2格式
if sender_part.startswith("00"):
try:
sender_part = ucs2_to_text(sender_part)
except Exception as e:
print(f"解码长短信发送者出错: {str(e)}")
# 解码失败时保留原始格式
# 保存信息等待后续处理
self.pending_sms_sender = sender_part
self.pending_sms_timestamp = timestamp_part
self.waiting_for_sms_content = True
# 发送状态更新
self.status_changed.emit(f"收到来自 {sender_part} 的长短信部分")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 收到来自 {sender_part} 的长短信部分")
except Exception as e:
# 出错时尝试作为普通短信处理
print(f"处理长短信头部出错: {str(e)}")
self._handle_regular_sms(header_line)
def _process_concatenated_sms_part(self, sender, timestamp, content):
"""处理长短信的内容部分"""
try:
# 移除空格
content = content.replace(" ", "")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 处理长短信内容: {content[:50]}...")
# 检查是否为特定格式的长短信
is_special_format = "62117ED94F6053D14E86957F6587672C" in content
# 尝试解码内容
try:
decoded_content = ucs2_to_text(content)
print(f"解码内容: {decoded_content[:50]}...")
except Exception as e:
print(f"UCS2解码错误: {str(e)}")
# 如果解码失败,尝试不同方法或直接发送原始内容
try:
# 尝试直接从十六进制转换为字节,然后解码
hex_bytes = binascii.unhexlify(content)
decoded_content = hex_bytes.decode('utf-16-be', errors='replace')
print(f"替代解码成功: {decoded_content[:50]}...")
except Exception as alt_e:
print(f"替代解码失败: {str(alt_e)}")
# 如果所有解码方法都失败,发送原始内容
self.sms_received.emit(
sender,
timestamp,
f"[无法解码的消息] {content[:50]}..."
)
return
# 特殊格式处理 - 提取URL
url = None
if is_special_format:
# 尝试从特殊格式中提取URL
parts = content.split("003A", 1) # 003A是冒号的UCS2编码
if len(parts) > 1:
url_content = "003A" + parts[1] # 加回冒号
try:
url_text = ucs2_to_text(url_content)
url_match = re.search(r':(https?://[^\s]+)', url_text)
if url_match:
url = url_match.group(1)
print(f"提取URL: {url}")
except Exception as url_e:
print(f"URL提取错误: {str(url_e)}")
# 如果没有找到URL,尝试从普通文本中提取
if not url:
url_match = re.search(r'(https?://[^\s]+)', decoded_content)
if url_match:
url = url_match.group(1)
print(f"从普通文本提取URL: {url}")
# 创建或更新长短信记录
sms_id = f"{sender}_{timestamp[:10]}"
if sms_id not in self.concat_sms_parts:
prefix = "消息"
if ":" in decoded_content:
prefix = decoded_content.split(":", 1)[0].strip()
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)
if decoded_content not in self.concat_sms_parts[sms_id]['parts']:
self.concat_sms_parts[sms_id]['parts'].append(decoded_content)
# 更新接收时间
self.concat_sms_parts[sms_id]['received_time'] = time.time()
# 使用定时器,3秒后尝试合并长短信
threading.Timer(3.0, lambda: self._check_and_merge_sms(sms_id)).start()
print(f"已保存长短信部分,将在3秒后尝试合并")
except Exception as e:
print(f"处理长短信内容部分出错: {str(e)}")
# 出错时直接发送解码后的内容
try:
decoded = ucs2_to_text(content) if all(c in "0123456789ABCDEFabcdef" for c in content) else content
self.sms_received.emit(
sender,
timestamp,
decoded
)
except Exception as final_e:
print(f"最终解码尝试失败: {str(final_e)}")
self.sms_received.emit(
sender,
timestamp,
f"[消息解码失败] {content[:50]}..."
)
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,优先使用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):
"""清理超时的长短信部分"""
try:
current_time = time.time()
sms_ids_to_remove = []
for sms_id, sms_info in self.concat_sms_parts.items():
# 检查是否已处理且超过保留时间(10分钟)
if sms_info.get('is_processed', False) and current_time - sms_info.get('last_processed', 0) > 600:
sms_ids_to_remove.append(sms_id)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 清理已处理的长短信: {sms_id}")
# 检查未处理但已超时的长短信(30秒)
elif not sms_info.get('is_processed', False) and current_time - sms_info.get('received_time', 0) > 30:
# 如果有内容但未处理(可能是因为只收到部分内容),尝试合并发送
if sms_info.get('parts', []):
try:
# 合并可用部分并发送
merged_content = self._merge_sms_parts(sms_id)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送超时但未处理的长短信: {merged_content[:50]}...")
self.sms_received.emit(
sms_info['sender'],
sms_info['timestamp'],
f"[部分内容] {merged_content}"
)
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 处理超时长短信时出错: {str(e)}")
sms_ids_to_remove.append(sms_id)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 清理超时未处理的长短信: {sms_id}")
# 移除标记的记录
for sms_id in sms_ids_to_remove:
del self.concat_sms_parts[sms_id]
self.status_changed.emit(f"清理长短信记录: {sms_id}")
# 打印当前缓存状态
if self.concat_sms_parts:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 当前有 {len(self.concat_sms_parts)} 条长短信记录在缓存中")
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 清理长短信部分时出错: {str(e)}")
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):
"""检查内容是否为长短信的一部分"""
try:
# 移除空格
content = content.replace(" ", "")
# 检查是否为UCS2编码
if not all(c in "0123456789ABCDEFabcdef" for c in content):
return False
# 检查内容长度是否足够
if len(content) < 10:
return False
# 检查是否包含特定模式
# 1. 检查特定前缀,这是已知的长短信特征
if content.startswith("62117ED94F6053D14E86957F6587672C"):
print(f"检测到长短信特定前缀: 62117ED94F6053D14E86957F6587672C")
return True
# 2. 检查内容是否包含URL的UCS2编码
# https的UCS2编码前缀: 00680074007400700073
if "00680074007400700073" in content:
print(f"检测到UCS2编码的HTTPS URL")
return True
# 3. 检查内容长度是否超过标准短信长度限制
# UCS2编码的短信最多支持70个字符,即140个字节,对应280个十六进制字符
if len(content) > 280:
print(f"内容长度({len(content)})超过标准短信限制")
return True
# 4. 尝试解码并检查是否包含特定内容标记
try:
decoded = ucs2_to_text(content)
# 检查解码后内容是否包含URL
if re.search(r'https?://', decoded):
print(f"检测到包含URL的内容")
return True
except:
pass
return False
except Exception as e:
print(f"检查长短信内容部分时出错: {str(e)}")
return False
def _initialize_module(self):
"""初始化LTE模块
设置基本配置,包括来电提醒、短信格式等
并获取设备信息
"""
try:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 初始化LTE模块")
# 每个命令间添加30毫秒延迟
# 检查并注销PCM音频,确保通话音频正确处理
self._stop_pcm_audio()
time.sleep(0.03) # 30毫秒延迟
# 检查当前CLIP状态
clip_status = self.send_at_command("AT+CLIP?")
time.sleep(0.03) # 30毫秒延迟
# 只有当CLIP不是1时才启用
if not clip_status or "+CLIP: 1" not in clip_status:
# 启用来电显示功能 (呼叫线路标识显示)
clip_response = self.send_at_command("AT+CLIP=1")
if "OK" in clip_response:
self.status_changed.emit("来电显示功能已启用")
else:
self.status_changed.emit("启用来电显示功能失败")
time.sleep(0.03) # 30毫秒延迟
# 检查当前SMS格式状态
sms_format = self.send_at_command("AT+CMGF?")
time.sleep(0.03) # 30毫秒延迟
# 只有当不是文本模式时才设置
if not sms_format or "+CMGF: 1" not in sms_format:
# 设置短信为文本模式
cmgf_response = self.send_at_command("AT+CMGF=1")
if "OK" in cmgf_response:
self.status_changed.emit("短信文本模式已启用")
else:
self.status_changed.emit("启用短信文本模式失败")
time.sleep(0.03) # 30毫秒延迟
# 检查新消息指示配置
cnmi_status = self.send_at_command("AT+CNMI?")
time.sleep(0.03) # 30毫秒延迟
# 只有当不是2,2,0,0,0时才设置
if not cnmi_status or "+CNMI: 2,2,0,0,0" not in cnmi_status:
# 设置新消息指示
cnmi_response = self.send_at_command("AT+CNMI=2,2,0,0,0")
if "OK" in cnmi_response:
self.status_changed.emit("短信通知已启用")
else:
self.status_changed.emit("启用短信通知失败")
time.sleep(0.03) # 30毫秒延迟
# 获取模块信息 (厂商、型号、IMEI等)
self._get_module_info()
time.sleep(0.03) # 30毫秒延迟
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - LTE模块初始化完成")
except Exception as e:
self.status_changed.emit(f"初始化模块失败: {str(e)}")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 初始化模块失败: {str(e)}")
def _get_module_info(self):
"""获取模块信息(初始化时调用一次)"""
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 获取设备基本信息")
# 记录上次更新时间
self.last_info_update = time.time()
# 获取制造商信息
response = self.send_at_command("AT+CGMI")
if response and "OK" in response:
# 移除命令回显和OK响应,只保留实际内容
lines = [line.strip() for line in 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()
# 获取模块型号
response = self.send_at_command("AT+CGMM")
if response and "OK" in response:
# 移除命令回显和OK响应,只保留实际内容
lines = [line.strip() for line in 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()
# 获取IMEI号码
response = self.send_at_command("AT+CGSN")
if response and "OK" in response:
# 移除命令回显和OK响应,只保留实际内容
lines = [line.strip() for line in 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()
# 获取固件版本
response = self.send_at_command("AT+CGMR")
if response and "OK" in response:
match = re.search(r'\+CGMR: (.+)', response)
if match:
self.firmware = match.group(1)
else:
# 移除命令回显和OK响应,只保留实际内容
lines = [line.strip() for line in 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()
# 获取电话号码、运营商和信号强度信息
self._update_phone_number()
self._update_carrier_info()
self._update_signal_strength()
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 设备基本信息获取完成")
def _update_phone_number(self):
"""更新电话号码信息(缓存30分钟)"""
# 添加缓存检查,减少AT命令交互
current_time = time.time()
if hasattr(self, 'last_phone_update') and current_time - self.last_phone_update < 1800: # 30分钟缓存
# 使用缓存的值
return self.phone_number
response = self.send_at_command("AT+CNUM")
if response and "+CNUM:" in response:
match = re.search(r'\+CNUM: "[^"]*","([^"]+)"', response)
if match:
self.phone_number = match.group(1)
self.last_phone_update = current_time # 记录更新时间
return self.phone_number
# 如果没有找到号码但有之前的值,保留之前的值
return self.phone_number
def _update_carrier_info(self):
"""更新运营商信息(缓存10分钟)"""
# 添加缓存检查,减少AT命令交互
current_time = time.time()
if hasattr(self, 'last_carrier_update') and current_time - self.last_carrier_update < 600: # 10分钟缓存
# 使用缓存的值
return (self.carrier, self.network_type)
# 更新运营商信息
carrier_updated = False
response = self.send_at_command("AT+COPS?")
if response and "+COPS:" in response:
match = re.search(r'\+COPS: \d+,\d+,"([^"]+)"', response)
if match:
self.carrier = match.group(1)
carrier_updated = True
# 更新网络类型
network_updated = False
response = self.send_at_command("AT+CPSI?")
if response and "+CPSI:" in response:
# 移除命令回显,只保留+CPSI:部分
match = re.search(r'\+CPSI:(.+)', response)
if match:
parts = match.group(1).split(',')
if len(parts) > 1:
self.network_type = parts[0].strip()
network_updated = True
else:
parts = response.split(',')
if len(parts) > 1:
self.network_type = parts[0].replace("+CPSI:", "").strip()
network_updated = True
# 如果有更新,记录更新时间
if carrier_updated or network_updated:
self.last_carrier_update = current_time
return (self.carrier, self.network_type)
def _update_signal_strength(self):
"""更新信号强度信息(无缓存,需要实时监控)"""
response = self.send_at_command("AT+CSQ")
if response and "+CSQ:" in response:
match = re.search(r'\+CSQ: (\d+),', response)
if match:
rssi = int(match.group(1))
if rssi == 99:
self.signal_strength = "Unknown"
else:
# Convert to dBm (-113 to -51 dBm)
dbm = -113 + (2 * rssi)
self.signal_strength = f"{dbm} dBm ({rssi}/31)"
return self.signal_strength
def get_carrier_info(self):
"""获取运营商信息(使用缓存机制)"""
if not self.connected:
return None
# 使用缓存的值,不每次都发送AT命令
if hasattr(self, 'carrier') and self.carrier:
# 只有在超过缓存时间时才更新
current_time = time.time()
if not hasattr(self, 'last_carrier_update') or current_time - self.last_carrier_update >= 600: # 10分钟缓存
self._update_carrier_info()
else:
# 第一次请求,直接更新
self._update_carrier_info()
return self.carrier
def get_phone_number(self):
"""获取电话号码(使用缓存机制)"""
if not self.connected:
return None
# 使用缓存的值,不每次都发送AT命令
if hasattr(self, 'phone_number') and self.phone_number:
# 只有在超过缓存时间时才更新
current_time = time.time()
if not hasattr(self, 'last_phone_update') or current_time - self.last_phone_update >= 1800: # 30分钟缓存
self._update_phone_number()
else:
# 第一次请求,直接更新
self._update_phone_number()
return self.phone_number
def get_network_info(self):
"""获取网络信息(使用缓存机制)"""
if not self.connected:
return None
# 使用缓存的值,不每次都发送AT命令
if hasattr(self, 'network_type') and self.network_type:
# 只有在超过缓存时间时才更新(与运营商信息共享缓存时间)
current_time = time.time()
if not hasattr(self, 'last_carrier_update') or current_time - self.last_carrier_update >= 600: # 10分钟缓存
self._update_carrier_info() # 这里会同时更新network_type
else:
# 第一次请求,直接更新
self._update_carrier_info()
return self.network_type
def get_signal_strength(self):
"""获取信号强度(实时更新)"""
if not self.connected:
return None
# 信号强度需要实时更新,其他信息则使用缓存
self._update_signal_strength()
return self.signal_strength
def get_module_info(self):
"""获取模块信息(使用缓存机制)"""
if not self.connected:
return {}
# 检查是否需要刷新模块信息(默认每小时更新一次)
current_time = time.time()
if not hasattr(self, 'last_info_update') or current_time - self.last_info_update >= 3600: # 1小时缓存
self._get_module_info()
else:
# 仅更新可能变化的信息:信号强度
self._update_signal_strength()
# 适当更新运营商信息(如果缓存过期)
if not hasattr(self, 'last_carrier_update') or current_time - self.last_carrier_update >= 600:
self._update_carrier_info()
return {
'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
}
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.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.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.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):
"""接听来电"""
try:
# 首先检查是否真的有来电
calls = self.get_call_status()
has_incoming_call = False
for call in calls:
if call.get('stat') == 4 and call.get('dir') == 1: # 来电中(MT)
has_incoming_call = True
break
if not has_incoming_call:
self.status_changed.emit("当前没有待接听的来电")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 尝试接听来电失败:当前无待接听来电")
return False
# 停止所有铃声
self._stop_all_ringtones()
time.sleep(0.03) # 30毫秒延迟
# 确保PCM音频已注册
self._ensure_pcm_audio_registered()
time.sleep(0.03) # 30毫秒延迟
# 发送接听命令
response = self.send_at_command("ATA")
if "OK" in response or "CONNECT" in response:
# 标记已接通
self.call_connected = True
self.in_call = True
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通话已接通")
return True
else:
# 即使命令返回失败,仍检查通话是否已建立(有时模块会接通但返回错误)
time.sleep(0.5) # 等待一小段时间让通话建立
# 再次检查通话状态,确认是否已接通
calls_after = self.get_call_status()
call_established = False
for call in calls_after:
if call.get('stat') in [0, 1] and call.get('dir') == 1: # 活动或保持状态的呼入通话
call_established = True
self.in_call = True
self.call_connected = True
break
if call_established:
self.status_changed.emit("通话已接通")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通话接听成功")
return True
else:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 接听失败: {response}")
return False
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 接听电话时出错: {str(e)}")
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")
# 检查响应中是否包含OK
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
else:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 挂断通话失败,响应: {response}")
return False
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")
# 对于AT+CHUP命令,检查特殊成功标志
if "OK" in response or "NO CARRIER" in response or "VOICE CALL: END" 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
# 检查响应中是否包含+CLCC行,且状态为6(终止)
# 如: +CLCC: 1,1,6,0,0,"18571797477",129,""
if "+CLCC:" in response and ",6," in response:
self.in_call = False
self.call_connected = False
self.status_changed.emit("来电已拒绝")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通过CLCC状态确认来电已拒绝")
# 通话结束后,立即取消PCM音频注册
self._ensure_pcm_audio_unregistered()
return True
# 发送CHUP后立即检查通话状态
time.sleep(0.5) # 短暂延迟,等待模块处理
after_calls = self.get_call_status()
if not after_calls:
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
else:
# 其他状态使用 ATH 命令挂断
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 使用ATH挂断通话,状态: {self.call_states.get(stat, '未知')}")
response = self.send_at_command("ATH")
# 检查响应中是否包含OK或其他成功标志
if "OK" in response or "NO CARRIER" in response or "VOICE CALL: END" 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
# 发送ATH后立即检查通话状态
time.sleep(0.5) # 短暂延迟,等待模块处理
after_calls = self.get_call_status()
if not after_calls:
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)
# Clear any pending responses
while not self.response_queue.empty():
self.response_queue.get()
# 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
# 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
# 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 = []
while time.time() - start_time < 15.0: # Increased timeout to 15 seconds
try:
line = self.response_queue.get(timeout=0.5)
response.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
except queue.Empty:
continue
# If we get here, we timed out waiting for a response
self.status_changed.emit(f"SMS send timeout. Last response: {response[-1] if response else 'None'}")
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_call_status(self):
"""
获取当前所有通话状态
返回值为通话列表,每个通话包含:
- id: 通话ID
- dir: 方向(0=MO, 1=MT
- stat: 状态(0=活动, 1=保持, 2=拨号, 3=振铃, 4=来电, 5=等待)
- mode: 模式(0=语音)
- mpty: 多方通话(0=非多方通话)
- number: 电话号码
"""
if not self.is_connected():
return []
# 检查是否有缓存且在短时间内(500毫秒内)
current_time = time.time()
if hasattr(self, 'last_call_status_check') and hasattr(self, 'cached_call_status'):
if current_time - self.last_call_status_check < 0.5: # 500毫秒内直接使用缓存结果
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 使用缓存的通话状态 ({int((current_time - self.last_call_status_check) * 1000)}ms)")
return self.cached_call_status
try:
# 记录本次查询时间
self.last_call_status_check = current_time
# 发送AT+CLCC查询通话状态命令
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送AT+CLCC查询通话状态")
response = self.send_at_command("AT+CLCC")
calls = []
# 检查响应是否有效
if not response:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - AT+CLCC无响应")
self.cached_call_status = []
return []
# 检查是否有错误响应
if "ERROR" in response:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - AT+CLCC返回错误: {response}")
self.cached_call_status = []
return []
# 检查响应中是否只有OK(无通话)
if "+CLCC:" not in response:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 无活动通话")
self.cached_call_status = []
return []
# 解析响应
lines = response.strip().split('\n')
for line in lines:
line = line.strip()
if not line.startswith('+CLCC:'):
continue
try:
# +CLCC: <id>,<dir>,<stat>,<mode>,<mpty>[,<number>,<type>[,<alpha>]]
parts = line[7:].strip().split(',')
# 验证是否有足够的字段
if len(parts) < 5:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - CLCC响应格式不完整: {line}")
continue
# 尝试解析通话信息
call = {
'id': int(parts[0].strip()),
'dir': int(parts[1].strip()),
'stat': int(parts[2].strip()),
'mode': int(parts[3].strip()),
'mpty': int(parts[4].strip())
}
# 判断是否有电话号码字段
if len(parts) > 5:
number = parts[5].strip()
if number.startswith('"') and number.endswith('"'):
number = number[1:-1] # 移除引号
call['number'] = number
# 记录该通话状态的文本描述(用于日志)
state_text = self.call_states.get(call['stat'], "未知状态")
direction = "呼出" if call['dir'] == 0 else "呼入"
number_info = f", 号码: {call.get('number', '未知')}" if 'number' in call else ""
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 检测到{direction}通话: {state_text}{number_info}")
calls.append(call)
except Exception as parse_error:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 解析CLCC响应行错误: {str(parse_error)}, 行: {line}")
continue
# 保存缓存结果
self.cached_call_status = calls
# 输出通话状态摘要
if calls:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 当前有 {len(calls)} 个活动通话")
else:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 没有活动通话")
# 通话状态变化时的特殊处理
if calls and not self.in_call:
# 之前不在通话,现在有通话 - 进入通话状态
self.in_call = True
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 检测到新通话,共 {len(calls)} 个通话")
# 获取最高优先级的通话状态
highest_priority_call = None
for call in calls:
# 优先级: 活动 > 来电 > 拨号 > 其他
if highest_priority_call is None or call['stat'] < highest_priority_call['stat']:
highest_priority_call = call
# 记录主叫号码或被叫号码
if highest_priority_call and 'number' in highest_priority_call:
self.call_number = highest_priority_call['number']
else:
self.call_number = ""
# 如果是状态为0的通话(活动通话),则更新通话已接通标志和时间
if highest_priority_call and highest_priority_call['stat'] == 0:
self.call_connected = True
self.call_connect_time = time.time()
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通话已接通,记录开始时间")
elif not calls and self.in_call:
# 之前在通话,现在没有通话 - 退出通话状态
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 所有通话已结束")
self.in_call = False
# 备份通话状态,然后清除
was_connected = self.call_connected
self.call_connected = False
# 如果有记录通话号码,生成结束通知
if self.call_number:
duration = "Missed" # 默认为未接
# 根据通话是否曾经接通决定如何计算时长
if was_connected:
# 计算通话时长(秒)
if hasattr(self, 'call_connect_time'):
call_duration = round(time.time() - self.call_connect_time)
duration = str(call_duration)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通话结束,持续时间: {call_duration}秒")
# 发出通话结束信号
self.call_ended.emit(duration)
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通话结束,号码: {self.call_number},持续时间: {duration}")
# 清除通话号码记录
self.call_number = ""
# 清除连接时间记录
if hasattr(self, 'call_connect_time'):
del self.call_connect_time
return calls
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 获取通话状态出错: {str(e)}")
# 出错时返回空列表,并缓存空列表
self.cached_call_status = []
return []
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 _ensure_pcm_audio_registered(self):
"""确保PCM音频已注册,用于通话音频处理"""
try:
# 检查当前PCM音频注册状态
pcm_status = self.send_at_command("AT+CPCMREG?")
time.sleep(0.03) # 30毫秒延迟
# 如果PCM没有注册,则进行注册
if not pcm_status or "+CPCMREG: 1" not in pcm_status:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 注册PCM音频")
reg_response = self.send_at_command("AT+CPCMREG=1")
if "OK" in reg_response:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注册成功")
self.pcm_audio_status.emit(True)
# 设置PCM音频格式
time.sleep(0.03) # 30毫秒延迟
frm_response = self.send_at_command("AT+CPCMFRM=1")
if "OK" in frm_response:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频格式设置成功")
else:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频格式设置失败")
else:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注册失败")
self.pcm_audio_status.emit(False)
else:
# 已经注册,发出信号
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频已注册")
self.pcm_audio_status.emit(True)
return True
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 确保PCM音频注册出错: {str(e)}")
self.pcm_audio_status.emit(False)
return False
def _stop_pcm_audio(self):
"""停止PCM音频注册"""
try:
# 检查当前PCM音频注册状态
pcm_status = self.send_at_command("AT+CPCMREG?")
# 如果已注册,则注销
if pcm_status and "+CPCMREG: 1" in pcm_status:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送PCM音频注销命令")
response = self.send_at_command("AT+CPCMREG=0")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销命令已发送")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销响应: {response}")
if "OK" in response:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销成功")
else:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频注销状态未知")
else:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频未注册或读取状态失败")
# 无论如何,都发送PCM音频已停止信号
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送PCM音频停止信号")
self.pcm_audio_status.emit(False)
return True
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 停止PCM音频注册出错: {str(e)}")
return False
def _stop_all_ringtones(self):
"""停止所有铃声和声音"""
try:
# 这里不应该发送AT+CLIP=0,那会禁用来电显示
# 而是通知上层应用停止铃声播放
self.status_changed.emit("STOP_RINGTONES")
# 清理任何可能的铃声缓存
if hasattr(self, 'ringtone_thread') and self.ringtone_thread:
try:
self.ringtone_thread.stop()
self.ringtone_thread = None
except:
pass
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 停止铃声信号已发送")
return True
except Exception as e:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 停止铃声出错: {str(e)}")
return False
def _auto_detect_port(self):
"""尝试自动检测LTE模块连接的串口"""
try:
# 获取所有可用串口
available_ports = [port.device for port in serial.tools.list_ports.comports()]
print(f"检测到可用串口: {available_ports}")
if not available_ports:
self.status_changed.emit("未检测到任何串口设备")
print("未检测到任何串口设备")
return None
# 如果只有一个串口,直接返回
if len(available_ports) == 1:
print(f"只有一个串口可用,直接使用: {available_ports[0]}")
return available_ports[0]
# 如果有多个串口,尝试连接每个串口并发送AT命令
print("检测到多个串口,尝试查找LTE模块...")
for port in available_ports:
try:
print(f"尝试在串口 {port} 上查找LTE模块...")
# 尝试打开串口
test_serial = serial.Serial(
port=port,
baudrate=115200,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1,
write_timeout=1
)
# 清空缓冲区
test_serial.reset_input_buffer()
test_serial.reset_output_buffer()
# 发送AT命令并等待响应
print(f"向 {port} 发送AT命令")
test_serial.write(b'AT\r\n')
time.sleep(0.5)
# 读取响应
response = test_serial.read(test_serial.in_waiting or 100).decode('utf-8', errors='replace')
print(f"从 {port} 收到响应: {response}")
# 关闭测试连接
test_serial.close()
# 检查响应是否包含OK
if 'OK' in response:
self.status_changed.emit(f"自动检测到LTE模块连接在 {port}")
print(f"在 {port} 上找到LTE模块")
return port
except Exception as e:
print(f"测试 {port} 时出错: {str(e)}")
try:
# 确保串口已关闭
if 'test_serial' in locals() and test_serial.is_open:
test_serial.close()
except:
pass
continue
# 如果没有找到匹配的串口,返回COM6作为默认(如果存在)
if 'COM6' in available_ports:
self.status_changed.emit(f"未能确定LTE模块连接的串口,尝试使用COM6")
print(f"未能确定LTE模块连接的串口,尝试使用COM6")
return 'COM6'
# 否则返回第一个可用串口
self.status_changed.emit(f"未能确定LTE模块连接的串口,使用第一个可用串口 {available_ports[0]}")
print(f"未能确定LTE模块连接的串口,使用第一个可用串口 {available_ports[0]}")
return available_ports[0]
except Exception as e:
self.status_changed.emit(f"串口自动检测出错: {str(e)}")
return None
def _configure_module(self):
"""配置LTE模块的初始设置"""
try:
# 日志记录配置开始
self.status_changed.emit("初始化LTE模块")
# 禁用回显 - 可选,取决于模块和应用需求
# self.send_at_command("ATE0")
# 设置SMS文本模式
self.send_at_command("AT+CMGF=1")
# 设置SMS字符集
self.send_at_command('AT+CSCS="UCS2"')
# 设置来电显示
self.send_at_command("AT+CLIP=1")
# 设置新消息指示
self.send_at_command('AT+CNMI=2,2,0,0,0')
# 查询是否有PCM音频注册
pcm_status = self.send_at_command("AT+ECPCMREG?")
if "+ECPCMREG: 1" in pcm_status:
# PCM已注册,先取消注册
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频已注册,取消注册")
self._unregister_pcm_audio()
else:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频未注册或读取状态失败")
# 确保PCM音频处于未注册状态
self.pcm_audio_status.emit(False)
# 获取基本信息
self._update_device_info()
self.status_changed.emit("LTE模块初始化完成")
return True
except Exception as e:
self.status_changed.emit(f"LTE模块配置失败: {str(e)}")
return False
def _update_device_info(self):
"""获取设备基本信息"""
try:
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 获取设备基本信息")
# 获取IMEI
response = self.send_at_command("AT+GSN")
if response and "ERROR" not in response:
self.imei = response.strip().split("\n")[0]
# 获取制造商
response = self.send_at_command("AT+GMI")
if response and "ERROR" not in response:
self.manufacturer = response.strip().split("\n")[0]
# 获取型号
response = self.send_at_command("AT+GMM")
if response and "ERROR" not in response:
self.model = response.strip().split("\n")[0]
# 获取固件版本
response = self.send_at_command("AT+GMR")
if response and "ERROR" not in response:
self.firmware = response.strip().split("\n")[0]
# 获取电话号码
response = self.send_at_command("AT+CNUM")
if response and "+CNUM:" in response:
match = re.search(r'\+CNUM: "([^"]*)",("?[^"]*"?),(\d+)', response)
if match:
self.phone_number = match.group(2).strip('"')
print(f"电话号码: {self.phone_number}")
# 获取运营商信息
response = self.send_at_command("AT+COPS?")
if response and "+COPS:" in response:
match = re.search(r'\+COPS: (\d+),(\d+),"([^"]*)"', response)
if match:
self.carrier = match.group(3)
print(f"运营商: {self.carrier}")
# 检查网络类型值,可能是第4个项目
if len(match.groups()) >= 4:
# 值映射: 0=GSM, 2=UTRAN, 7=LTE, 13=NR
net_type_map = {
'0': '2G (GSM)',
'2': '3G (UMTS)',
'7': '4G (LTE)',
'13': '5G (NR)'
}
net_type = match.group(4) if len(match.groups()) >= 4 else '7' # 默认LTE
self.network_type = net_type_map.get(net_type, f'Unknown ({net_type})')
else:
# 从response中提取网络类型
self._update_network_type()
else:
# 如果COPS查询失败,尝试单独更新网络类型
self._update_network_type()
# 获取信号强度
response = self.send_at_command("AT+CSQ")
if response and "+CSQ:" in response:
match = re.search(r'\+CSQ: (\d+),(\d+)', response)
if match:
rssi = int(match.group(1))
# 转换RSSI为信号格数和dBm值
if rssi == 99:
self.signal_strength = "无信号"
else:
# RSSI值0-31,对应-113dBm到-51dBm
dbm = -113 + (2 * rssi)
# 信号格数(0-4格)
if rssi >= 16: # >= -81dBm
bars = 4
elif rssi >= 12: # >= -89dBm
bars = 3
elif rssi >= 8: # >= -97dBm
bars = 2
elif rssi >= 4: # >= -105dBm
bars = 1
else:
bars = 0
self.signal_strength = f"{bars}格 ({dbm}dBm)"
print(f"信号强度: {self.signal_strength}")
# 发送初始化完成的信号
self.status_changed.emit(f"设备信息已更新: {self.model} {self.carrier}")
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 设备基本信息获取完成")
return True
except Exception as e:
self.status_changed.emit(f"获取设备信息失败: {str(e)}")
return False
def _update_network_type(self):
"""单独更新网络类型信息"""
try:
# 尝试使用AT+CEREG?命令获取LTE网络注册状态
cereg_response = self.send_at_command("AT+CEREG?")
if "CEREG: " in cereg_response:
# 检查是否有网络注册
match = re.search(r'\+CEREG: \d+,(\d+)', cereg_response)
if match and match.group(1) in ['1', '5']: # 1=已注册,本地网络; 5=已注册,漫游
self.network_type = "4G (LTE)"
return
# 尝试使用AT+CREG?命令获取GSM/UMTS网络注册状态
creg_response = self.send_at_command("AT+CREG?")
if "CREG: " in creg_response:
match = re.search(r'\+CREG: \d+,(\d+)', creg_response)
if match and match.group(1) in ['1', '5']:
# 进一步检查是2G还是3G
cgreg_response = self.send_at_command("AT+CGREG?")
if "CGREG: " in cgreg_response and re.search(r'\+CGREG: \d+,[15]', cgreg_response):
self.network_type = "3G (UMTS)"
else:
self.network_type = "2G (GSM)"
return
# 如果以上都失败,使用默认值
self.network_type = "未知"
except Exception as e:
print(f"更新网络类型失败: {str(e)}")
self.network_type = "更新失败"