chore: import R0nY3n/LTE_manager main snapshot
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
+849
@@ -0,0 +1,849 @@
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import sounddevice as sd
|
||||
import numpy as np
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
import re
|
||||
import sys
|
||||
import logging
|
||||
import struct
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QTimer
|
||||
|
||||
# 配置日志记录
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger("PCM_Audio")
|
||||
|
||||
# PCM 音频参数 (SIM7600CE-T 使用的标准 PCM 格式)
|
||||
# 按照文档说明: "USB audio PCM data format is 8K sample rate, 16 bit linear"
|
||||
# 可以通过AT+CPCMFRM=1设置为16K采样率,但请确保模块和音频处理使用相同的采样率
|
||||
PCM_SAMPLE_RATE = 8000 # 8kHz (默认模式,可通过AT+CPCMFRM=1设置为16kHz)
|
||||
PCM_CHANNELS = 1 # 单声道
|
||||
PCM_DTYPE = np.int16 # 16-bit 线性PCM
|
||||
CHUNK_SIZE = 160 # 每次读取的样本数 (20ms @ 8kHz,更小的块大小可降低延迟)
|
||||
BUFFER_SIZE = 8 # 增加缓冲区大小,提高音频稳定性
|
||||
|
||||
class PCMAudio(QObject):
|
||||
status_changed = pyqtSignal(str) # 状态变化信号
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.audio_port = None
|
||||
self.audio_thread = None
|
||||
self.play_thread = None
|
||||
self.record_thread = None
|
||||
self.terminating = False # 新增终止标志
|
||||
self.is_running = False
|
||||
self.call_active = False
|
||||
self.port_name = None # 存储当前使用的端口名称
|
||||
self.shutdown_requested = False # 替代QTimer的关闭请求标志
|
||||
|
||||
# 音频数据队列
|
||||
self.play_queue = queue.Queue(maxsize=BUFFER_SIZE) # 播放队列
|
||||
self.record_queue = queue.Queue(maxsize=BUFFER_SIZE) # 录音队列
|
||||
|
||||
# 音频流
|
||||
self.output_stream = None
|
||||
self.input_stream = None
|
||||
|
||||
def find_audio_port(self):
|
||||
"""查找SIM7600CE的Audio端口 (通常是Audio 9001端口)"""
|
||||
logger.info("正在查找SIM7600CE Audio端口...")
|
||||
self.status_changed.emit("正在查找音频端口...")
|
||||
|
||||
ports = list(serial.tools.list_ports.comports())
|
||||
for port in ports:
|
||||
# 检查描述或设备ID中是否包含"Audio"和"9001"
|
||||
if ('audio' in port.description.lower() or
|
||||
'audio' in port.device.lower() or
|
||||
'9001' in port.description):
|
||||
logger.info(f"找到疑似音频端口: {port.device} - {port.description}")
|
||||
self.status_changed.emit(f"找到音频端口: {port.device}")
|
||||
return port.device
|
||||
|
||||
logger.warning("未找到SIM7600CE音频端口! 请确保设备已连接且驱动已安装。")
|
||||
self.status_changed.emit("未找到音频端口, 通话将没有音频")
|
||||
return None
|
||||
|
||||
def open_audio_port(self, port=None):
|
||||
"""打开SIM7600CE的Audio端口"""
|
||||
# 重置终止标志
|
||||
self.terminating = False
|
||||
|
||||
# 先关闭之前可能打开的端口
|
||||
if self.audio_port and self.audio_port.is_open:
|
||||
try:
|
||||
self.audio_port.close()
|
||||
logger.info("关闭先前打开的音频端口")
|
||||
except Exception as e:
|
||||
logger.error(f"关闭先前端口时出错: {str(e)}")
|
||||
self.audio_port = None
|
||||
|
||||
if port:
|
||||
audio_port_name = port
|
||||
else:
|
||||
audio_port_name = self.find_audio_port()
|
||||
|
||||
if not audio_port_name:
|
||||
logger.error("无法打开音频端口: 未找到端口")
|
||||
self.status_changed.emit("无法打开音频端口")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 使用更高的波特率921600以确保音频数据传输顺畅
|
||||
self.audio_port = serial.Serial(
|
||||
port=audio_port_name,
|
||||
baudrate=921600, # 提高波特率到921600
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=0.1, # 非阻塞读取
|
||||
rtscts=True, # 启用硬件流控制
|
||||
write_timeout=0.5 # 设置写入超时
|
||||
)
|
||||
self.port_name = audio_port_name # 存储端口名称
|
||||
logger.info(f"成功打开音频端口: {audio_port_name}, 波特率: 921600")
|
||||
self.status_changed.emit(f"音频端口已打开: {audio_port_name}")
|
||||
|
||||
# 清空可能已有的数据
|
||||
self.audio_port.reset_input_buffer()
|
||||
self.audio_port.reset_output_buffer()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"打开音频端口失败: {str(e)}")
|
||||
self.status_changed.emit(f"打开音频端口失败: {str(e)[:50]}")
|
||||
return False
|
||||
|
||||
def start_audio_processing(self):
|
||||
"""启动音频处理"""
|
||||
if not self.audio_port:
|
||||
logger.error("未打开音频端口,无法启动音频处理")
|
||||
self.status_changed.emit("未打开音频端口,无法启动音频处理")
|
||||
return False
|
||||
|
||||
if self.is_running:
|
||||
logger.warning("音频处理已经在运行")
|
||||
return True
|
||||
|
||||
# 重置终止标志
|
||||
self.terminating = False
|
||||
|
||||
# 初始化音频设备
|
||||
try:
|
||||
# 获取默认设备信息
|
||||
devices = sd.query_devices()
|
||||
default_output = sd.default.device[1]
|
||||
default_input = sd.default.device[0]
|
||||
|
||||
logger.info(f"使用默认输出设备: {devices[default_output]['name']}")
|
||||
logger.info(f"使用默认输入设备: {devices[default_input]['name']}")
|
||||
self.status_changed.emit(f"使用音频设备: {devices[default_output]['name']}")
|
||||
|
||||
# 清空旧数据
|
||||
if self.audio_port and self.audio_port.is_open:
|
||||
self.audio_port.reset_input_buffer()
|
||||
self.audio_port.reset_output_buffer()
|
||||
|
||||
# 清空队列
|
||||
self._clear_audio_queues()
|
||||
|
||||
# 打开音频流
|
||||
self.output_stream = sd.OutputStream(
|
||||
samplerate=PCM_SAMPLE_RATE,
|
||||
channels=PCM_CHANNELS,
|
||||
dtype=PCM_DTYPE,
|
||||
callback=self._audio_output_callback,
|
||||
blocksize=CHUNK_SIZE,
|
||||
latency='low' # 设置低延迟
|
||||
)
|
||||
|
||||
self.input_stream = sd.InputStream(
|
||||
samplerate=PCM_SAMPLE_RATE,
|
||||
channels=PCM_CHANNELS,
|
||||
dtype=PCM_DTYPE,
|
||||
callback=self._audio_input_callback,
|
||||
blocksize=CHUNK_SIZE,
|
||||
latency='low' # 设置低延迟
|
||||
)
|
||||
|
||||
# 启动音频流
|
||||
self.output_stream.start()
|
||||
self.input_stream.start()
|
||||
|
||||
# 设置运行标志
|
||||
self.is_running = True
|
||||
|
||||
# 启动处理线程
|
||||
self.audio_thread = threading.Thread(target=self._audio_port_thread, daemon=True)
|
||||
self.audio_thread.name = "PCMAudioPortThread"
|
||||
self.audio_thread.start()
|
||||
|
||||
# 启动播放线程
|
||||
self.play_thread = threading.Thread(target=self._play_thread, daemon=True)
|
||||
self.play_thread.name = "PCMAudioPlayThread"
|
||||
self.play_thread.start()
|
||||
|
||||
# 启动录音线程
|
||||
self.record_thread = threading.Thread(target=self._record_thread, daemon=True)
|
||||
self.record_thread.name = "PCMAudioRecordThread"
|
||||
self.record_thread.start()
|
||||
|
||||
logger.info("音频处理已启动")
|
||||
self.status_changed.emit("音频处理已启动")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动音频处理失败: {str(e)}")
|
||||
self.status_changed.emit(f"启动音频处理失败: {str(e)[:50]}")
|
||||
self._cleanup_resources()
|
||||
return False
|
||||
|
||||
def _clear_audio_queues(self):
|
||||
"""清空音频队列"""
|
||||
# 清空播放队列
|
||||
while not self.play_queue.empty():
|
||||
try:
|
||||
self.play_queue.get_nowait()
|
||||
except Exception as e:
|
||||
logger.error(f"清空播放队列出错: {str(e)}")
|
||||
break
|
||||
|
||||
# 清空录音队列
|
||||
while not self.record_queue.empty():
|
||||
try:
|
||||
self.record_queue.get_nowait()
|
||||
except Exception as e:
|
||||
logger.error(f"清空录音队列出错: {str(e)}")
|
||||
break
|
||||
|
||||
logger.info("已清空音频队列")
|
||||
|
||||
def _cleanup_resources(self):
|
||||
"""清理所有资源(在关闭或错误时调用)"""
|
||||
# 停止和关闭音频流
|
||||
if self.output_stream:
|
||||
try:
|
||||
self.output_stream.stop()
|
||||
self.output_stream.close()
|
||||
except Exception as e:
|
||||
logger.error(f"关闭输出流出错: {str(e)}")
|
||||
self.output_stream = None
|
||||
|
||||
if self.input_stream:
|
||||
try:
|
||||
self.input_stream.stop()
|
||||
self.input_stream.close()
|
||||
except Exception as e:
|
||||
logger.error(f"关闭输入流出错: {str(e)}")
|
||||
self.input_stream = None
|
||||
|
||||
# 清空队列
|
||||
self._clear_audio_queues()
|
||||
|
||||
# 关闭音频端口
|
||||
if self.audio_port and self.audio_port.is_open:
|
||||
try:
|
||||
self.audio_port.reset_input_buffer()
|
||||
self.audio_port.reset_output_buffer()
|
||||
self.audio_port.close()
|
||||
logger.info(f"已关闭音频端口: {self.port_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"关闭音频端口出错: {str(e)}")
|
||||
self.audio_port = None
|
||||
|
||||
# 重置状态
|
||||
self.is_running = False
|
||||
self.call_active = False
|
||||
|
||||
def stop_audio_processing(self):
|
||||
"""停止音频处理"""
|
||||
if not self.is_running:
|
||||
logger.info("音频处理已经停止,无需再次停止")
|
||||
return
|
||||
|
||||
logger.info("正在停止音频处理...")
|
||||
self.status_changed.emit("正在停止音频处理...")
|
||||
|
||||
# 设置终止标志,通知所有线程停止
|
||||
self.terminating = True
|
||||
self.call_active = False
|
||||
self.is_running = False
|
||||
self.shutdown_requested = False # 取消可能的关闭请求
|
||||
|
||||
# 立即清理所有资源,不再等待线程正常结束
|
||||
self._cleanup_resources()
|
||||
|
||||
# 只在资源清理后尝试关闭线程
|
||||
# 等待线程结束 - 使用更短的超时以防止阻塞
|
||||
threads_to_wait = []
|
||||
|
||||
if self.audio_thread and self.audio_thread.is_alive():
|
||||
logger.info("等待音频端口线程结束...")
|
||||
threads_to_wait.append(('音频端口线程', self.audio_thread))
|
||||
|
||||
if self.play_thread and self.play_thread.is_alive():
|
||||
logger.info("等待播放线程结束...")
|
||||
threads_to_wait.append(('播放线程', self.play_thread))
|
||||
|
||||
if self.record_thread and self.record_thread.is_alive():
|
||||
logger.info("等待录音线程结束...")
|
||||
threads_to_wait.append(('录音线程', self.record_thread))
|
||||
|
||||
# 等待所有线程结束,每个线程最多等待0.5秒
|
||||
for thread_name, thread in threads_to_wait:
|
||||
thread.join(timeout=0.5)
|
||||
if thread.is_alive():
|
||||
logger.warning(f"{thread_name}未能正常结束")
|
||||
|
||||
# 重置线程变量
|
||||
self.audio_thread = None
|
||||
self.play_thread = None
|
||||
self.record_thread = None
|
||||
|
||||
logger.info("音频处理已停止")
|
||||
self.status_changed.emit("音频处理已停止")
|
||||
|
||||
def set_call_active(self, active):
|
||||
"""设置通话状态"""
|
||||
prev_state = self.call_active
|
||||
self.call_active = active
|
||||
|
||||
if prev_state != active: # 只有状态改变时才记录和通知
|
||||
logger.info(f"通话状态: {'活动' if active else '非活动'}")
|
||||
self.status_changed.emit(f"通话音频状态: {'活动' if active else '非活动'}")
|
||||
|
||||
if active:
|
||||
# 当状态从非活动变为活动时,清空缓冲区
|
||||
if self.audio_port and self.audio_port.is_open:
|
||||
try:
|
||||
self.audio_port.reset_input_buffer()
|
||||
self.audio_port.reset_output_buffer()
|
||||
except Exception as e:
|
||||
logger.error(f"重置音频缓冲区出错: {str(e)}")
|
||||
|
||||
# 清空音频队列
|
||||
self._clear_audio_queues()
|
||||
else:
|
||||
# 状态从活动变为非活动时,开始直接关闭处理,不使用延迟机制
|
||||
logger.info("通话状态变为非活动,准备关闭音频处理")
|
||||
self.shutdown_requested = True # 设置关闭请求标志,用于代替QTimer
|
||||
|
||||
# 启动单独的关闭线程,避免在当前线程中执行可能阻塞的操作
|
||||
shutdown_thread = threading.Thread(target=self._delayed_shutdown_thread, daemon=True)
|
||||
shutdown_thread.start()
|
||||
|
||||
def _delayed_shutdown_thread(self):
|
||||
"""在单独线程中执行延迟关闭,避免阻塞主线程"""
|
||||
try:
|
||||
# 等待短暂时间,确保所有挂起的操作都有时间完成
|
||||
time.sleep(0.5)
|
||||
|
||||
# 检查是否仍然需要关闭
|
||||
if not self.call_active and self.shutdown_requested:
|
||||
logger.info("执行延迟关闭音频处理")
|
||||
# 执行停止处理,但不在线程中调用self.stop_audio_processing,而是发送信号
|
||||
self.status_changed.emit("音频处理关闭中...")
|
||||
|
||||
# 设置所有状态标志
|
||||
self.terminating = True
|
||||
|
||||
# 等待安全时间后强制清理资源
|
||||
time.sleep(0.5)
|
||||
logger.info("关闭音频流和端口")
|
||||
|
||||
# 关闭和清理资源
|
||||
if self.output_stream:
|
||||
try:
|
||||
self.output_stream.stop()
|
||||
self.output_stream.close()
|
||||
self.output_stream = None
|
||||
except Exception as e:
|
||||
logger.error(f"关闭输出流出错: {str(e)}")
|
||||
|
||||
if self.input_stream:
|
||||
try:
|
||||
self.input_stream.stop()
|
||||
self.input_stream.close()
|
||||
self.input_stream = None
|
||||
except Exception as e:
|
||||
logger.error(f"关闭输入流出错: {str(e)}")
|
||||
|
||||
# 重置状态
|
||||
self.is_running = False
|
||||
self.shutdown_requested = False
|
||||
logger.info("延迟关闭完成")
|
||||
self.status_changed.emit("音频处理已关闭")
|
||||
except Exception as e:
|
||||
logger.error(f"延迟关闭线程出错: {str(e)}")
|
||||
|
||||
def _audio_output_callback(self, outdata, frames, time, status):
|
||||
"""音频输出回调(从队列获取PCM数据并输出到扬声器)"""
|
||||
if status:
|
||||
logger.warning(f"音频输出状态: {status}")
|
||||
|
||||
if not self.call_active or self.terminating:
|
||||
# 如果没有通话或正在终止,输出静音
|
||||
outdata.fill(0)
|
||||
return
|
||||
|
||||
try:
|
||||
if not self.play_queue.empty():
|
||||
# 从队列获取PCM数据
|
||||
data = self.play_queue.get_nowait()
|
||||
# 确保数据长度匹配
|
||||
if len(data) < frames:
|
||||
# 数据不足,补零
|
||||
padding = np.zeros((frames - len(data), PCM_CHANNELS), dtype=PCM_DTYPE)
|
||||
data = np.vstack((data, padding))
|
||||
elif len(data) > frames:
|
||||
# 数据过多,截断
|
||||
data = data[:frames]
|
||||
# 复制到输出缓冲区
|
||||
outdata[:] = data
|
||||
else:
|
||||
# 队列为空,输出静音
|
||||
outdata.fill(0)
|
||||
except Exception as e:
|
||||
logger.error(f"音频输出错误: {str(e)}")
|
||||
outdata.fill(0)
|
||||
|
||||
def _audio_input_callback(self, indata, frames, time, status):
|
||||
"""音频输入回调(从麦克风获取PCM数据并发送到队列)"""
|
||||
if status:
|
||||
logger.warning(f"音频输入状态: {status}")
|
||||
|
||||
if not self.call_active or self.terminating:
|
||||
# 如果没有通话或正在终止,不处理输入
|
||||
return
|
||||
|
||||
try:
|
||||
# 将麦克风数据放入录音队列
|
||||
if not self.record_queue.full():
|
||||
self.record_queue.put_nowait(indata.copy())
|
||||
except Exception as e:
|
||||
logger.error(f"音频输入错误: {str(e)}")
|
||||
|
||||
def _audio_port_thread(self):
|
||||
"""音频端口处理线程(读取PCM数据 - 模块到扬声器)"""
|
||||
# PCM数据解析缓冲区
|
||||
buffer = bytearray()
|
||||
bytes_per_frame = CHUNK_SIZE * PCM_CHANNELS * 2 # 16-bit = 2 bytes
|
||||
last_log_time = 0
|
||||
frame_count = 0
|
||||
last_buffer_check_time = 0
|
||||
processed_frames_total = 0
|
||||
last_data_received_time = time.time()
|
||||
silent_frames_count = 0
|
||||
frame_sync_attempts = 0
|
||||
recovered_frames = 0
|
||||
|
||||
# 设置调试计数器
|
||||
debug_frame_counter = 0
|
||||
debug_signal_detection = False
|
||||
signal_level_history = []
|
||||
max_signal_level = 0
|
||||
|
||||
# 设置基准音量和增益值 - 增加接收增益确保清晰听到对方声音
|
||||
base_gain = 5.0 # 更高的基准增益,确保足够听到对方声音
|
||||
noise_threshold = 30 # 降低噪声阈值以确保不会过滤掉有效信号
|
||||
|
||||
# 设置静音帧阈值,超过该数量未收到有效数据帧时发出警告
|
||||
SILENT_FRAMES_THRESHOLD = 50
|
||||
|
||||
logger.info("音频端口处理线程已启动")
|
||||
logger.info(f"PCM参数: 采样率={PCM_SAMPLE_RATE}Hz, 通道数={PCM_CHANNELS}, 每帧字节数={bytes_per_frame}")
|
||||
logger.info(f"音频输出设置: 基准增益={base_gain}x,噪声阈值={noise_threshold}")
|
||||
|
||||
# 发送模式测试数据
|
||||
try:
|
||||
# 向模块发送一些测试数据,验证发送通道
|
||||
if self.audio_port and self.audio_port.is_open:
|
||||
test_data = np.zeros((CHUNK_SIZE, PCM_CHANNELS), dtype=np.int16)
|
||||
test_data[:10, 0] = 16000 # 前10个样本设置为16000,形成短脉冲
|
||||
test_bytes = test_data.tobytes()
|
||||
self.audio_port.write(test_bytes)
|
||||
logger.info(f"已发送测试音频数据: {len(test_bytes)}字节")
|
||||
except Exception as e:
|
||||
logger.error(f"发送测试数据出错: {str(e)}")
|
||||
|
||||
while self.is_running and not self.terminating:
|
||||
try:
|
||||
if not self.audio_port or not self.audio_port.is_open:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# 如果不在通话状态,快速检查并继续循环
|
||||
if not self.call_active:
|
||||
# 清空缓冲区并睡眠
|
||||
if buffer:
|
||||
buffer = bytearray()
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# 读取串口数据
|
||||
try:
|
||||
available = self.audio_port.in_waiting
|
||||
if available > 0:
|
||||
# 读取所有可用数据
|
||||
data = self.audio_port.read(available)
|
||||
if data:
|
||||
# 更新最后接收数据时间
|
||||
last_data_received_time = time.time()
|
||||
silent_frames_count = 0 # 重置静音帧计数
|
||||
|
||||
# 添加到缓冲区
|
||||
buffer.extend(data)
|
||||
|
||||
# 每1000帧记录一次调试信息
|
||||
debug_frame_counter += 1
|
||||
if debug_frame_counter >= 1000:
|
||||
# 记录详细状态信息
|
||||
logger.info(f"[读取] 音频缓冲区: {len(buffer)}字节, 可用数据: {available}字节")
|
||||
logger.info(f"[读取] 已处理总帧数: {processed_frames_total}, 缓冲区状态: {len(buffer)/bytes_per_frame:.1f}帧")
|
||||
if signal_level_history:
|
||||
avg_level = sum(signal_level_history) / len(signal_level_history)
|
||||
logger.info(f"[读取] 平均信号电平: {avg_level:.2f}, 最大信号电平: {max_signal_level:.2f}")
|
||||
if avg_level > 0:
|
||||
logger.info(f"[读取] 检测到音频信号,增益设置为{base_gain}x")
|
||||
debug_frame_counter = 0
|
||||
else:
|
||||
# 长时间未收到数据,可能需要检查通信状态
|
||||
current_time = time.time()
|
||||
if current_time - last_data_received_time > 0.5: # 半秒未收到数据
|
||||
silent_frames_count += 1
|
||||
if silent_frames_count > SILENT_FRAMES_THRESHOLD and self.call_active:
|
||||
logger.warning("[读取] 长时间未收到音频数据,检查通信状态")
|
||||
silent_frames_count = 0 # 重置计数,避免重复警告
|
||||
|
||||
# 尝试重置串口缓冲区,重新启动数据流
|
||||
if self.audio_port and self.audio_port.is_open:
|
||||
try:
|
||||
# 先发送一些数据,可能帮助触发接收
|
||||
test_data = np.zeros((CHUNK_SIZE, PCM_CHANNELS), dtype=np.int16)
|
||||
test_data[:10, 0] = 16000 # 前10个样本设置为16000
|
||||
test_bytes = test_data.tobytes()
|
||||
self.audio_port.write(test_bytes)
|
||||
|
||||
# 重置输入缓冲区
|
||||
self.audio_port.reset_input_buffer()
|
||||
logger.info("[读取] 已重置音频输入缓冲区并发送测试数据")
|
||||
except Exception as e:
|
||||
logger.error(f"[读取] 重置音频缓冲区出错: {str(e)}")
|
||||
|
||||
# 定期检查缓冲区大小,避免缓冲区无限增长
|
||||
current_time = time.time()
|
||||
if current_time - last_buffer_check_time > 1.0: # 每秒检查一次
|
||||
# 检查缓冲区大小
|
||||
buffer_size = len(buffer)
|
||||
|
||||
# 如果缓冲区不是帧大小的整数倍,尝试帧同步
|
||||
remainder = buffer_size % bytes_per_frame
|
||||
if remainder != 0 and buffer_size > bytes_per_frame:
|
||||
# 尝试通过查找头部模式同步帧
|
||||
frame_sync_attempts += 1
|
||||
if frame_sync_attempts % 10 == 0: # 每10次尝试记录一次
|
||||
logger.warning(f"[读取] 帧同步尝试: {frame_sync_attempts}次, 缓冲区大小: {buffer_size}字节, 余数: {remainder}字节")
|
||||
|
||||
# 丢弃余数字节或补齐帧
|
||||
if remainder < bytes_per_frame / 2:
|
||||
# 余数小于半帧,丢弃余数
|
||||
buffer = buffer[:-remainder]
|
||||
else:
|
||||
# 余数大于半帧,补齐为完整帧(用0填充)
|
||||
padding_size = bytes_per_frame - remainder
|
||||
buffer.extend(bytes(padding_size))
|
||||
recovered_frames += 1
|
||||
|
||||
buffer_frames = len(buffer) / bytes_per_frame
|
||||
if buffer_size > bytes_per_frame * 30: # 如果缓冲区积累太多数据
|
||||
logger.warning(f"[读取] 缓冲区积累过多数据 ({buffer_size} 字节, {buffer_frames:.1f} 帧), 保留最后10帧数据")
|
||||
# 只保留最后部分数据
|
||||
buffer = buffer[-bytes_per_frame * 10:]
|
||||
|
||||
# 如果音频缓冲区长时间为空并且通话活动,记录警告
|
||||
if buffer_size == 0 and self.call_active and processed_frames_total > 0:
|
||||
logger.warning("[读取] 音频缓冲区为空但通话仍在进行,可能缺少音频数据")
|
||||
|
||||
last_buffer_check_time = current_time
|
||||
|
||||
# 当缓冲区数据足够时处理
|
||||
processed = 0
|
||||
while len(buffer) >= bytes_per_frame and self.call_active and not self.terminating:
|
||||
# 提取一帧数据
|
||||
frame_data = buffer[:bytes_per_frame]
|
||||
buffer = buffer[bytes_per_frame:]
|
||||
processed += 1
|
||||
processed_frames_total += 1
|
||||
|
||||
try:
|
||||
# 将SIM7600CE的PCM数据转换为音频数据
|
||||
pcm_data = np.frombuffer(frame_data, dtype=np.int16).reshape(-1, PCM_CHANNELS)
|
||||
|
||||
# 计算信号电平用于自动增益和检测有效信号
|
||||
signal_level = np.abs(pcm_data).mean()
|
||||
|
||||
# 在首次接收到高于阈值的信号时记录
|
||||
if signal_level > noise_threshold and not signal_level_history:
|
||||
logger.info(f"[读取] 首次检测到信号: 电平={signal_level:.2f}")
|
||||
|
||||
# 过滤掉异常值,确保数据有效
|
||||
if signal_level < 32000: # 有效PCM数据不应超过此值
|
||||
# 更新信号历史
|
||||
signal_level_history.append(signal_level)
|
||||
if len(signal_level_history) > 50: # 保留50帧的历史
|
||||
signal_level_history.pop(0)
|
||||
|
||||
# 记录最大信号电平(用于调试)
|
||||
if signal_level > max_signal_level:
|
||||
max_signal_level = signal_level
|
||||
if not debug_signal_detection and signal_level > 100:
|
||||
logger.info(f"[读取] 检测到新的最大信号电平: {max_signal_level:.2f}")
|
||||
debug_signal_detection = True
|
||||
|
||||
# 噪声消除 - 如果信号电平低于噪声阈值,视为噪声
|
||||
if signal_level < noise_threshold:
|
||||
# 对于非常低的信号(噪声),应用非常小的增益
|
||||
# 但仍保留一部分,以保持连续性
|
||||
pcm_data = pcm_data * 0.05 # 保留5%的信号
|
||||
else:
|
||||
# 为了确保足够的音量,使用较高的基准增益
|
||||
# 让对方的声音更加清晰
|
||||
pcm_data = np.clip(pcm_data * base_gain, -32700, 32700).astype(np.int16)
|
||||
|
||||
# 放入播放队列
|
||||
if not self.play_queue.full() and not self.terminating:
|
||||
self.play_queue.put_nowait(pcm_data)
|
||||
frame_count += 1
|
||||
else:
|
||||
# 信号电平异常,可能是帧同步问题
|
||||
logger.warning(f"[读取] 异常信号电平: {signal_level}, 可能帧同步问题")
|
||||
|
||||
# 每隔一段时间记录一次性能日志
|
||||
current_time = time.time()
|
||||
if current_time - last_log_time > 5.0: # 每5秒记录一次
|
||||
avg_signal = np.mean(signal_level_history) if signal_level_history else 0
|
||||
logger.info(f"[读取] 已处理 {frame_count} 帧PCM数据,平均信号电平: {avg_signal:.2f}")
|
||||
last_log_time = current_time
|
||||
frame_count = 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[读取] 处理PCM数据帧错误: {str(e)}")
|
||||
# 出错时清空缓冲区,避免继续处理错误数据
|
||||
buffer = bytearray()
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"[读取] 读取音频端口数据出错: {str(e)}")
|
||||
time.sleep(0.1)
|
||||
|
||||
# 如果当前没有更多数据可读,短暂休眠避免CPU占用
|
||||
if available == 0:
|
||||
time.sleep(0.01) # 10ms延迟,提供更好的响应性
|
||||
else:
|
||||
# 有数据处理时使用更短的延迟
|
||||
time.sleep(0.001)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[读取] 音频端口处理错误: {str(e)}")
|
||||
time.sleep(0.1)
|
||||
|
||||
# 线程结束前清空缓冲区及统计数据
|
||||
buffer = bytearray()
|
||||
signal_level_history = []
|
||||
logger.info(f"[读取] 音频端口处理线程已退出,总处理帧数: {processed_frames_total}, 恢复帧: {recovered_frames}")
|
||||
|
||||
def _play_thread(self):
|
||||
"""播放线程(处理PCM数据队列)"""
|
||||
logger.info("播放线程已启动")
|
||||
|
||||
while self.is_running and not self.terminating:
|
||||
try:
|
||||
# 线程主要工作在回调中完成,这里只需要保持线程运行
|
||||
time.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"播放线程错误: {str(e)}")
|
||||
time.sleep(0.1)
|
||||
|
||||
logger.info("播放线程已退出")
|
||||
|
||||
def _record_thread(self):
|
||||
"""录音线程(发送PCM数据到串口 - 麦克风到模块)"""
|
||||
logger.info("[发送] 录音线程已启动")
|
||||
|
||||
# 记录最近的数据包大小,用于调试
|
||||
packet_sizes = []
|
||||
last_log_time = 0
|
||||
sent_packets_count = 0
|
||||
total_bytes_sent = 0
|
||||
last_packet_sent_time = time.time()
|
||||
|
||||
# 引入随机数生成器用于加入测试音频
|
||||
np.random.seed()
|
||||
|
||||
# 采样率和块大小
|
||||
sample_rate = PCM_SAMPLE_RATE # 8kHz
|
||||
chunk_size = CHUNK_SIZE # 160个样本,即20ms@8kHz
|
||||
|
||||
# 初始化降噪参数
|
||||
noise_floor = 80 # 噪声阈值 - 降低以确保捕获更多人声
|
||||
voice_gain = 4.0 # 人声增益 - 增加以确保声音传输清晰
|
||||
noise_gate_enabled = True # 启用噪声门控
|
||||
|
||||
logger.info(f"[发送] 麦克风设置: 启用噪声门控={noise_gate_enabled}, 噪声阈值={noise_floor}, 人声增益={voice_gain}x")
|
||||
|
||||
# 创建测试音频信号(1kHz正弦波)用于向模块发送
|
||||
test_audio_enabled = False # 默认关闭测试音频
|
||||
test_tone_freq = 1000 # 1kHz
|
||||
test_tone_samples = np.arange(chunk_size)
|
||||
test_tone = (16000 * np.sin(2 * np.pi * test_tone_freq * test_tone_samples / sample_rate)).astype(np.int16)
|
||||
test_tone = test_tone.reshape(-1, PCM_CHANNELS)
|
||||
|
||||
# 强制发送计时器,确保即使麦克风无输入,仍定期发送数据包
|
||||
force_send_interval = 0.020 # 20ms,确保平滑音频
|
||||
zero_frame = np.zeros((chunk_size, PCM_CHANNELS), dtype=np.int16)
|
||||
|
||||
# 加入启动时的初始测试音频
|
||||
try:
|
||||
if self.audio_port and self.audio_port.is_open:
|
||||
# 发送测试音频波形序列
|
||||
for i in range(5): # 发送5帧测试音频
|
||||
self.audio_port.write(test_tone.tobytes())
|
||||
sent_packets_count += 1
|
||||
time.sleep(0.02) # 20ms间隔
|
||||
logger.info(f"[发送] 已发送初始测试音频: 5帧")
|
||||
except Exception as e:
|
||||
logger.error(f"[发送] 发送初始测试音频出错: {str(e)}")
|
||||
|
||||
while self.is_running and not self.terminating:
|
||||
try:
|
||||
if not self.call_active or not self.audio_port or not self.audio_port.is_open or self.terminating:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
# 是否应该强制发送(超过定期发送间隔)
|
||||
should_force_send = (current_time - last_packet_sent_time) > force_send_interval
|
||||
|
||||
# 从录音队列获取数据
|
||||
try:
|
||||
# 使用短超时,避免长时间阻塞
|
||||
pcm_data = None
|
||||
try:
|
||||
pcm_data = self.record_queue.get(timeout=0.01)
|
||||
except queue.Empty:
|
||||
# 队列为空,如果需要强制发送则生成静音帧
|
||||
if should_force_send:
|
||||
if test_audio_enabled:
|
||||
# 使用测试音频而不是静音
|
||||
pcm_data = test_tone.copy()
|
||||
logger.debug("[发送] 生成测试音频帧发送")
|
||||
else:
|
||||
# 使用静音帧
|
||||
pcm_data = zero_frame.copy()
|
||||
logger.debug("[发送] 生成静音帧发送")
|
||||
else:
|
||||
continue
|
||||
|
||||
# 如果还没有数据,跳过当前循环
|
||||
if pcm_data is None:
|
||||
continue
|
||||
|
||||
# 计算当前音量级别
|
||||
volume_level = np.abs(pcm_data).mean()
|
||||
|
||||
# 偶尔发送测试音频以确保通信通道开放
|
||||
if sent_packets_count % 1000 == 0: # 每1000个包发送一次测试音频
|
||||
# 临时替换为测试音频
|
||||
pcm_data = test_tone.copy()
|
||||
logger.info(f"[发送] 发送测试音频帧: #{sent_packets_count}")
|
||||
|
||||
# 应用噪声门控和增益处理
|
||||
if noise_gate_enabled:
|
||||
if volume_level < noise_floor:
|
||||
# 低于阈值的信号视为背景噪音,强烈抑制但不完全消除
|
||||
# 这有助于减少背景噪音传输到对方
|
||||
pcm_data = pcm_data * 0.02 # 只保留2%原始信号
|
||||
else:
|
||||
# 高于阈值的信号应用更高增益提升人声清晰度
|
||||
# 确保声音传输到对方足够清晰
|
||||
pcm_data = np.clip(pcm_data * voice_gain, -32700, 32700).astype(np.int16)
|
||||
else:
|
||||
# 如果不启用噪声门控,仍然应用增益
|
||||
pcm_data = np.clip(pcm_data * voice_gain, -32700, 32700).astype(np.int16)
|
||||
|
||||
# 将PCM数据转换为字节发送到串口(确保使用小端字节序)
|
||||
bytes_data = pcm_data.astype(np.int16).tobytes()
|
||||
|
||||
# 更新发送计时
|
||||
last_packet_sent_time = current_time
|
||||
|
||||
# 记录数据包大小用于调试
|
||||
packet_sizes.append(len(bytes_data))
|
||||
if len(packet_sizes) > 20:
|
||||
packet_sizes.pop(0)
|
||||
|
||||
# 每5秒记录一次发送数据统计
|
||||
if current_time - last_log_time > 5.0:
|
||||
if packet_sizes:
|
||||
avg_size = sum(packet_sizes) / len(packet_sizes)
|
||||
logger.info(f"[发送] 音频发送: 平均数据包大小 {avg_size:.2f} 字节, 已发送 {sent_packets_count} 个数据包 ({total_bytes_sent/1024:.2f} KB)")
|
||||
last_log_time = current_time
|
||||
|
||||
# 检查连接和终止状态
|
||||
if self.audio_port and self.audio_port.is_open and not self.terminating:
|
||||
# 确保立即发送数据
|
||||
bytes_sent = self.audio_port.write(bytes_data)
|
||||
sent_packets_count += 1
|
||||
total_bytes_sent += bytes_sent
|
||||
|
||||
# 调试:检查发送的字节数
|
||||
if bytes_sent != len(bytes_data):
|
||||
logger.warning(f"[发送] 音频数据发送不完整: {bytes_sent}/{len(bytes_data)}字节")
|
||||
|
||||
# 确保数据立即发送
|
||||
self.audio_port.flush()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[发送] 发送PCM数据错误: {str(e)}")
|
||||
time.sleep(0.01)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[发送] 录音线程错误: {str(e)}")
|
||||
time.sleep(0.1)
|
||||
|
||||
# 清理记录数据
|
||||
packet_sizes = []
|
||||
logger.info(f"[发送] 录音线程已退出,总发送数据包: {sent_packets_count}, 总发送字节: {total_bytes_sent/1024:.2f} KB")
|
||||
|
||||
# 单独测试功能
|
||||
if __name__ == "__main__":
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 测试音频功能
|
||||
audio = PCMAudio()
|
||||
|
||||
port = audio.find_audio_port()
|
||||
if port:
|
||||
print(f"找到音频端口: {port}")
|
||||
if audio.open_audio_port(port):
|
||||
print("成功打开音频端口")
|
||||
audio.start_audio_processing()
|
||||
print("按Enter键模拟通话开始...")
|
||||
input()
|
||||
audio.set_call_active(True)
|
||||
print("通话已开始,现在可以说话...按Enter键结束通话")
|
||||
input()
|
||||
audio.set_call_active(False)
|
||||
print("通话已结束")
|
||||
# 等待延迟关闭完成
|
||||
time.sleep(4)
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("未找到音频端口")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,226 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
import glob
|
||||
import time
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
def build_executable():
|
||||
"""Build executable with PyInstaller"""
|
||||
print("Building LTE Manager executable...")
|
||||
|
||||
# 尝试终止可能正在运行的LTE_Manager.exe进程
|
||||
try:
|
||||
print("尝试终止可能正在运行的LTE_Manager.exe进程...")
|
||||
subprocess.call("taskkill /F /IM LTE_Manager.exe", shell=True)
|
||||
# 等待进程完全终止
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
print(f"终止进程时出错 (这可能是正常的,如果进程不存在): {str(e)}")
|
||||
|
||||
# 保存数据库文件
|
||||
db_backup_dir = "_db_backup_temp"
|
||||
db_files = []
|
||||
|
||||
# 查找用户主目录下的数据库文件
|
||||
user_home = os.path.expanduser('~')
|
||||
lte_db_dir = os.path.join(user_home, '.LTE')
|
||||
user_db_path = os.path.join(lte_db_dir, 'lte_data.db')
|
||||
|
||||
if os.path.exists("dist") or os.path.exists(user_db_path):
|
||||
print("备份数据库文件...")
|
||||
# 创建临时备份目录
|
||||
if not os.path.exists(db_backup_dir):
|
||||
os.makedirs(db_backup_dir)
|
||||
|
||||
# 查找所有SQLite数据库文件
|
||||
dist_db_files = glob.glob("dist/*.db") if os.path.exists("dist") else []
|
||||
|
||||
# 备份找到的dist目录中的文件
|
||||
for db_file in dist_db_files:
|
||||
db_filename = os.path.basename(db_file)
|
||||
backup_path = os.path.join(db_backup_dir, db_filename)
|
||||
print(f"备份数据库 (旧版位置): {db_filename}")
|
||||
shutil.copy2(db_file, backup_path)
|
||||
|
||||
# 备份用户主目录中的数据库
|
||||
if os.path.exists(user_db_path):
|
||||
backup_path = os.path.join(db_backup_dir, 'lte_data.db')
|
||||
print(f"备份数据库 (用户目录): {user_db_path}")
|
||||
shutil.copy2(user_db_path, backup_path)
|
||||
|
||||
# Check if PyInstaller is installed
|
||||
try:
|
||||
import PyInstaller
|
||||
except ImportError:
|
||||
print("PyInstaller not found. Installing...")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "pyinstaller"])
|
||||
|
||||
# Create spec file
|
||||
spec_content = """
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('default.png', '.'),
|
||||
('running.png', '.'),
|
||||
('error.png', '.'),
|
||||
('README.md', '.'),
|
||||
('incoming_call.py', '.'),
|
||||
('audio.py', '.')
|
||||
],
|
||||
hiddenimports=['PyQt5.sip', 'sounddevice', 'numpy'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='LTE_Manager',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon='default.png',
|
||||
)
|
||||
"""
|
||||
|
||||
with open("lte_manager.spec", "w") as f:
|
||||
f.write(spec_content)
|
||||
|
||||
# Run PyInstaller
|
||||
print("Running PyInstaller...")
|
||||
try:
|
||||
# 等待一秒确保所有进程都释放了文件
|
||||
time.sleep(1)
|
||||
|
||||
# 先检查目标EXE文件是否存在并可以被删除
|
||||
target_exe = os.path.join("dist", "LTE_Manager.exe")
|
||||
if os.path.exists(target_exe):
|
||||
try:
|
||||
# 尝试先重命名文件,这是检查文件是否被锁定的一种方法
|
||||
temp_name = os.path.join("dist", f"LTE_Manager_old_{int(time.time())}.exe")
|
||||
os.rename(target_exe, temp_name)
|
||||
# 然后删除重命名后的文件
|
||||
os.remove(temp_name)
|
||||
print("成功删除旧的可执行文件")
|
||||
except Exception as rename_error:
|
||||
print(f"无法删除旧的可执行文件: {str(rename_error)}")
|
||||
print("尝试使用Python 3.10来构建...")
|
||||
# 尝试使用Python 3.10,如果可用
|
||||
python310_path = r"G:\Python310\python.exe"
|
||||
if os.path.exists(python310_path):
|
||||
subprocess.check_call([
|
||||
python310_path,
|
||||
"-m",
|
||||
"PyInstaller",
|
||||
"lte_manager.spec",
|
||||
"--clean"
|
||||
])
|
||||
print("使用Python 3.10构建完成")
|
||||
build_success = True
|
||||
return
|
||||
|
||||
# 运行PyInstaller
|
||||
subprocess.check_call([
|
||||
sys.executable,
|
||||
"-m",
|
||||
"PyInstaller",
|
||||
"lte_manager.spec",
|
||||
"--clean"
|
||||
])
|
||||
|
||||
print("PyInstaller执行完成")
|
||||
build_success = True
|
||||
except Exception as e:
|
||||
print(f"构建过程中出错: {str(e)}")
|
||||
build_success = False
|
||||
|
||||
# 恢复数据库文件
|
||||
if os.path.exists(db_backup_dir):
|
||||
print("恢复数据库文件...")
|
||||
backups = glob.glob(os.path.join(db_backup_dir, "*.db"))
|
||||
|
||||
for backup_file in backups:
|
||||
db_filename = os.path.basename(backup_file)
|
||||
|
||||
# 确保用户目录存在
|
||||
if not os.path.exists(lte_db_dir):
|
||||
os.makedirs(lte_db_dir)
|
||||
|
||||
# 恢复到用户主目录
|
||||
user_db_path = os.path.join(lte_db_dir, db_filename)
|
||||
print(f"恢复数据库到用户目录: {user_db_path}")
|
||||
shutil.copy2(backup_file, user_db_path)
|
||||
|
||||
# 为了向后兼容,也恢复到dist目录
|
||||
if os.path.exists("dist"):
|
||||
dist_path = os.path.join("dist", db_filename)
|
||||
print(f"恢复数据库到dist目录: {dist_path}")
|
||||
shutil.copy2(backup_file, dist_path)
|
||||
|
||||
# 删除临时备份目录
|
||||
shutil.rmtree(db_backup_dir)
|
||||
|
||||
# Create a simple README file (if it doesn't exist)
|
||||
if not os.path.exists("README.md"):
|
||||
with open("README.md", "w") as f:
|
||||
f.write("""# LTE Manager
|
||||
|
||||
一个用于管理LTE模块的应用程序,支持电话和短信功能。
|
||||
|
||||
## 功能
|
||||
|
||||
- 电话呼叫管理(拨打/接听电话)
|
||||
- 短信管理(发送/接收/解码短信,包括中文)
|
||||
- 模块配置和状态监控
|
||||
- 串口配置
|
||||
- 来电接听对话框
|
||||
- PCM音频通话支持
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 连接LTE模块到计算机
|
||||
2. 在设置选项卡中配置串口设置
|
||||
3. 连接到模块
|
||||
4. 使用电话和短信功能
|
||||
|
||||
## 系统托盘
|
||||
|
||||
应用程序可以最小化到系统托盘。双击托盘图标可以显示/隐藏主窗口。
|
||||
""")
|
||||
|
||||
if build_success:
|
||||
print("Executable built successfully!")
|
||||
print("You can find it in the 'dist' folder.")
|
||||
else:
|
||||
print("构建过程未完成,请检查错误信息。")
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_executable()
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
class LTEDatabase:
|
||||
def __init__(self, db_path=None):
|
||||
"""初始化数据库连接
|
||||
|
||||
Args:
|
||||
db_path: 数据库文件路径,如未指定则使用用户目录下的.LTE/lte_data.db
|
||||
"""
|
||||
if db_path is None:
|
||||
# 默认使用用户主目录下的.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)
|
||||
self.db_path = os.path.join(lte_dir, 'lte_data.db')
|
||||
else:
|
||||
self.db_path = db_path
|
||||
|
||||
print(f"使用数据库: {self.db_path}")
|
||||
self.conn = None
|
||||
self.cursor = None
|
||||
self.connect()
|
||||
self.create_tables()
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the database"""
|
||||
try:
|
||||
# Create database directory if it doesn't exist
|
||||
db_dir = os.path.dirname(self.db_path)
|
||||
if db_dir and not os.path.exists(db_dir):
|
||||
os.makedirs(db_dir)
|
||||
|
||||
# Connect to database
|
||||
self.conn = sqlite3.connect(self.db_path)
|
||||
self.cursor = self.conn.cursor()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Database connection error: {str(e)}")
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
|
||||
def create_tables(self):
|
||||
"""Create necessary tables if they don't exist"""
|
||||
try:
|
||||
# Call history table
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS call_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phone_number TEXT NOT NULL,
|
||||
call_type TEXT NOT NULL,
|
||||
duration INTEGER DEFAULT 0,
|
||||
timestamp TEXT NOT NULL,
|
||||
notes TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
# SMS history table
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS sms_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phone_number TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
sms_type TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'sent'
|
||||
)
|
||||
''')
|
||||
|
||||
self.conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Table creation error: {str(e)}")
|
||||
return False
|
||||
|
||||
def add_call(self, phone_number, call_type, duration=0, notes=None):
|
||||
"""Add call record to database
|
||||
|
||||
call_type: 'incoming', 'outgoing', 'missed'
|
||||
"""
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"添加通话记录: {phone_number}, 类型: {call_type}, 持续时间: {duration}秒, 备注: {notes}")
|
||||
self.cursor.execute(
|
||||
"INSERT INTO call_history (phone_number, call_type, duration, timestamp, notes) VALUES (?, ?, ?, ?, ?)",
|
||||
(phone_number, call_type, duration, timestamp, notes)
|
||||
)
|
||||
self.conn.commit()
|
||||
return self.cursor.lastrowid
|
||||
except Exception as e:
|
||||
print(f"添加通话记录出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def add_sms(self, phone_number, message, sms_type, status='sent'):
|
||||
"""Add SMS record to database
|
||||
|
||||
sms_type: 'incoming', 'outgoing'
|
||||
status: 'sent', 'failed', 'received', 'read'
|
||||
"""
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.cursor.execute(
|
||||
"INSERT INTO sms_history (phone_number, message, sms_type, timestamp, status) VALUES (?, ?, ?, ?, ?)",
|
||||
(phone_number, message, sms_type, timestamp, status)
|
||||
)
|
||||
self.conn.commit()
|
||||
return self.cursor.lastrowid
|
||||
except Exception as e:
|
||||
print(f"Add SMS error: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_call_history(self, limit=50, offset=0, phone_number=None):
|
||||
"""Get call history from database"""
|
||||
try:
|
||||
query = "SELECT * FROM call_history"
|
||||
params = []
|
||||
|
||||
if phone_number:
|
||||
query += " WHERE phone_number = ?"
|
||||
params.append(phone_number)
|
||||
|
||||
query += " ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
||||
params.extend([limit, offset])
|
||||
|
||||
self.cursor.execute(query, params)
|
||||
return self.cursor.fetchall()
|
||||
except Exception as e:
|
||||
print(f"Get call history error: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_sms_history(self, limit=50, offset=0, phone_number=None, sms_type=None):
|
||||
"""Get SMS history from database"""
|
||||
try:
|
||||
query = "SELECT * FROM sms_history"
|
||||
params = []
|
||||
|
||||
conditions = []
|
||||
if phone_number:
|
||||
conditions.append("phone_number = ?")
|
||||
params.append(phone_number)
|
||||
|
||||
if sms_type:
|
||||
conditions.append("sms_type = ?")
|
||||
params.append(sms_type)
|
||||
|
||||
if conditions:
|
||||
query += " WHERE " + " AND ".join(conditions)
|
||||
|
||||
query += " ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
||||
params.extend([limit, offset])
|
||||
|
||||
self.cursor.execute(query, params)
|
||||
return self.cursor.fetchall()
|
||||
except Exception as e:
|
||||
print(f"Get SMS history error: {str(e)}")
|
||||
return []
|
||||
|
||||
def update_sms_status(self, sms_id, status):
|
||||
"""Update SMS status"""
|
||||
try:
|
||||
self.cursor.execute(
|
||||
"UPDATE sms_history SET status = ? WHERE id = ?",
|
||||
(status, sms_id)
|
||||
)
|
||||
self.conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Update SMS status error: {str(e)}")
|
||||
return False
|
||||
|
||||
def delete_call(self, call_id):
|
||||
"""Delete call record"""
|
||||
try:
|
||||
self.cursor.execute("DELETE FROM call_history WHERE id = ?", (call_id,))
|
||||
self.conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Delete call error: {str(e)}")
|
||||
return False
|
||||
|
||||
def delete_sms(self, sms_id):
|
||||
"""Delete SMS record"""
|
||||
try:
|
||||
self.cursor.execute("DELETE FROM sms_history WHERE id = ?", (sms_id,))
|
||||
self.conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Delete SMS error: {str(e)}")
|
||||
return False
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,524 @@
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import logging
|
||||
import tempfile
|
||||
from PyQt5.QtCore import QObject, pyqtSignal
|
||||
|
||||
# 配置日志记录
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger("FFmpeg_Audio")
|
||||
|
||||
# PCM 音频参数
|
||||
PCM_SAMPLE_RATE = 8000 # 8kHz (默认模式,可通过AT+CPCMFRM=1设置为16kHz)
|
||||
PCM_CHANNELS = 1 # 单声道
|
||||
FFMPEG_PATH = "D:\\ffmpeg\\ffmpeg.exe" # FFmpeg可执行文件路径
|
||||
FFPLAY_PATH = "D:\\ffmpeg\\ffplay.exe" # FFPlay可执行文件路径
|
||||
|
||||
class FFmpegAudio(QObject):
|
||||
status_changed = pyqtSignal(str) # 状态变化信号
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.audio_port = None
|
||||
self.port_name = None
|
||||
self.is_running = False
|
||||
self.call_active = False
|
||||
self.terminating = False
|
||||
|
||||
# FFmpeg进程
|
||||
self.ffmpeg_input_process = None # 从串口读取到扬声器
|
||||
self.ffmpeg_output_process = None # 从麦克风到串口
|
||||
|
||||
# 管理线程
|
||||
self.monitor_thread = None
|
||||
|
||||
# 临时文件
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
logger.info(f"创建临时目录: {self.temp_dir}")
|
||||
|
||||
def find_audio_port(self):
|
||||
"""查找SIM7600CE的Audio端口 (通常是Audio 9001端口)"""
|
||||
logger.info("正在查找SIM7600CE Audio端口...")
|
||||
self.status_changed.emit("正在查找音频端口...")
|
||||
|
||||
ports = list(serial.tools.list_ports.comports())
|
||||
for port in ports:
|
||||
# 检查描述或设备ID中是否包含"Audio"和"9001"
|
||||
if ('audio' in port.description.lower() or
|
||||
'audio' in port.device.lower() or
|
||||
'9001' in port.description):
|
||||
logger.info(f"找到疑似音频端口: {port.device} - {port.description}")
|
||||
self.status_changed.emit(f"找到音频端口: {port.device}")
|
||||
return port.device
|
||||
|
||||
logger.warning("未找到SIM7600CE音频端口! 请确保设备已连接且驱动已安装。")
|
||||
self.status_changed.emit("未找到音频端口, 通话将没有音频")
|
||||
return None
|
||||
|
||||
def open_audio_port(self, port=None):
|
||||
"""打开SIM7600CE的Audio端口"""
|
||||
# 重置终止标志
|
||||
self.terminating = False
|
||||
|
||||
# 先关闭之前可能打开的端口
|
||||
if self.audio_port and self.audio_port.is_open:
|
||||
try:
|
||||
self.audio_port.close()
|
||||
logger.info("关闭先前打开的音频端口")
|
||||
except Exception as e:
|
||||
logger.error(f"关闭先前端口时出错: {str(e)}")
|
||||
self.audio_port = None
|
||||
|
||||
if port:
|
||||
audio_port_name = port
|
||||
else:
|
||||
audio_port_name = self.find_audio_port()
|
||||
|
||||
if not audio_port_name:
|
||||
logger.error("无法打开音频端口: 未找到端口")
|
||||
self.status_changed.emit("无法打开音频端口")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 使用更高的波特率921600以确保音频数据传输顺畅
|
||||
self.audio_port = serial.Serial(
|
||||
port=audio_port_name,
|
||||
baudrate=921600, # 提高波特率到921600
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=0.1, # 非阻塞读取
|
||||
rtscts=True, # 启用硬件流控制
|
||||
write_timeout=0.5 # 设置写入超时
|
||||
)
|
||||
self.port_name = audio_port_name # 存储端口名称
|
||||
logger.info(f"成功打开音频端口: {audio_port_name}, 波特率: 921600")
|
||||
self.status_changed.emit(f"音频端口已打开: {audio_port_name}")
|
||||
|
||||
# 清空可能已有的数据
|
||||
self.audio_port.reset_input_buffer()
|
||||
self.audio_port.reset_output_buffer()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"打开音频端口失败: {str(e)}")
|
||||
self.status_changed.emit(f"打开音频端口失败: {str(e)[:50]}")
|
||||
return False
|
||||
|
||||
def _cleanup_resources(self):
|
||||
"""清理所有资源(在关闭或错误时调用)"""
|
||||
logger.info("清理音频资源...")
|
||||
|
||||
# 停止FFmpeg进程
|
||||
try:
|
||||
if self.ffmpeg_input_process:
|
||||
logger.info("正在停止FFmpeg输入进程...")
|
||||
self.ffmpeg_input_process.terminate()
|
||||
self.ffmpeg_input_process.wait(timeout=1)
|
||||
self.ffmpeg_input_process = None
|
||||
except Exception as e:
|
||||
logger.error(f"停止FFmpeg输入进程出错: {str(e)}")
|
||||
|
||||
try:
|
||||
if self.ffmpeg_output_process:
|
||||
logger.info("正在停止FFmpeg输出进程...")
|
||||
self.ffmpeg_output_process.terminate()
|
||||
self.ffmpeg_output_process.wait(timeout=1)
|
||||
self.ffmpeg_output_process = None
|
||||
except Exception as e:
|
||||
logger.error(f"停止FFmpeg输出进程出错: {str(e)}")
|
||||
|
||||
# 关闭音频端口
|
||||
if self.audio_port and self.audio_port.is_open:
|
||||
try:
|
||||
self.audio_port.reset_input_buffer()
|
||||
self.audio_port.reset_output_buffer()
|
||||
self.audio_port.close()
|
||||
logger.info(f"已关闭音频端口: {self.port_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"关闭音频端口出错: {str(e)}")
|
||||
self.audio_port = None
|
||||
|
||||
# 重置状态
|
||||
self.is_running = False
|
||||
self.call_active = False
|
||||
logger.info("音频资源清理完成")
|
||||
|
||||
def stop_audio_processing(self):
|
||||
"""停止音频处理"""
|
||||
if not self.is_running:
|
||||
logger.info("音频处理已经停止,无需再次停止")
|
||||
return
|
||||
|
||||
logger.info("正在停止音频处理...")
|
||||
self.status_changed.emit("正在停止音频处理...")
|
||||
|
||||
# 设置终止标志
|
||||
self.terminating = True
|
||||
self.call_active = False
|
||||
self.is_running = False
|
||||
|
||||
# 清理资源
|
||||
self._cleanup_resources()
|
||||
|
||||
# 等待监控线程结束
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
self.monitor_thread.join(timeout=1)
|
||||
|
||||
self.monitor_thread = None
|
||||
|
||||
logger.info("音频处理已停止")
|
||||
self.status_changed.emit("音频处理已停止")
|
||||
|
||||
def set_call_active(self, active):
|
||||
"""设置通话状态"""
|
||||
prev_state = self.call_active
|
||||
self.call_active = active
|
||||
|
||||
if prev_state != active: # 只有状态改变时才记录和通知
|
||||
logger.info(f"通话状态: {'活动' if active else '非活动'}")
|
||||
self.status_changed.emit(f"通话音频状态: {'活动' if active else '非活动'}")
|
||||
|
||||
if active:
|
||||
# 当状态从非活动变为活动时,确保FFmpeg进程在运行
|
||||
self._ensure_ffmpeg_running()
|
||||
else:
|
||||
# 状态从活动变为非活动时,停止FFmpeg进程
|
||||
logger.info("通话状态变为非活动,准备关闭音频处理")
|
||||
# 启动单独的关闭线程,避免在当前线程中执行可能阻塞的操作
|
||||
threading.Thread(target=self._delayed_shutdown, daemon=True).start()
|
||||
|
||||
def _delayed_shutdown(self):
|
||||
"""延迟关闭处理"""
|
||||
try:
|
||||
# 等待短暂时间确保所有挂起的操作完成
|
||||
time.sleep(0.5)
|
||||
if not self.call_active and not self.terminating:
|
||||
logger.info("执行延迟关闭音频处理")
|
||||
self._cleanup_resources()
|
||||
except Exception as e:
|
||||
logger.error(f"延迟关闭出错: {str(e)}")
|
||||
|
||||
def _ensure_ffmpeg_running(self):
|
||||
"""确保FFmpeg进程在运行"""
|
||||
if not self.is_running or not self.audio_port or not self.audio_port.is_open:
|
||||
logger.warning("音频处理未启动或端口未打开,无法确保FFmpeg运行")
|
||||
return
|
||||
|
||||
try:
|
||||
# 检查并启动FFmpeg进程
|
||||
if not self.ffmpeg_input_process or self.ffmpeg_input_process.poll() is not None:
|
||||
self._start_ffmpeg_input()
|
||||
|
||||
if not self.ffmpeg_output_process or self.ffmpeg_output_process.poll() is not None:
|
||||
self._start_ffmpeg_output()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"确保FFmpeg运行时出错: {str(e)}")
|
||||
|
||||
def _start_ffmpeg_input(self):
|
||||
"""启动从串口到扬声器的FFmpeg进程"""
|
||||
if not self.audio_port or not self.audio_port.is_open:
|
||||
logger.error("音频端口未打开,无法启动FFmpeg输入进程")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 配置FFmpeg命令行
|
||||
# 从串口读取PCM数据并播放到扬声器
|
||||
input_pipe_path = os.path.join(self.temp_dir, "input_pipe.pcm")
|
||||
|
||||
# 检查并确保管道创建
|
||||
if os.path.exists(input_pipe_path):
|
||||
try:
|
||||
os.remove(input_pipe_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 创建命令行
|
||||
cmd = [
|
||||
FFPLAY_PATH,
|
||||
"-f", "s16le", # 16位有符号整数,小端序
|
||||
"-ar", str(PCM_SAMPLE_RATE), # 采样率
|
||||
"-ac", str(PCM_CHANNELS), # 通道数
|
||||
"-i", "pipe:0", # 从标准输入读取
|
||||
"-loglevel", "warning", # 只显示警告和错误
|
||||
"-af", "volume=5", # 增加音量
|
||||
"-nodisp" # 不显示视频窗口
|
||||
]
|
||||
|
||||
logger.info(f"启动FFmpeg输入进程: {' '.join(cmd)}")
|
||||
|
||||
# 启动FFmpeg进程
|
||||
self.ffmpeg_input_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
bufsize=0 # 无缓冲
|
||||
)
|
||||
|
||||
# 启动从串口读取数据的线程
|
||||
threading.Thread(
|
||||
target=self._read_from_serial_to_ffmpeg,
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
logger.info("FFmpeg输入进程启动成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"启动FFmpeg输入进程失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def _read_from_serial_to_ffmpeg(self):
|
||||
"""从串口读取数据并发送到FFmpeg"""
|
||||
buffer_size = 320 # 每次读取的字节数 (160个16位样本)
|
||||
|
||||
# 用于统计的变量
|
||||
bytes_read = 0
|
||||
last_log_time = time.time()
|
||||
frames_sent = 0
|
||||
|
||||
logger.info("[读取] 开始从串口读取PCM数据到FFmpeg")
|
||||
|
||||
while self.is_running and self.call_active and not self.terminating:
|
||||
try:
|
||||
if not self.audio_port or not self.audio_port.is_open:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
if not self.ffmpeg_input_process or self.ffmpeg_input_process.poll() is not None:
|
||||
logger.warning("[读取] FFmpeg输入进程已结束,停止读取")
|
||||
break
|
||||
|
||||
# 读取数据
|
||||
if self.audio_port.in_waiting > 0:
|
||||
data = self.audio_port.read(min(buffer_size, self.audio_port.in_waiting))
|
||||
if data:
|
||||
bytes_read += len(data)
|
||||
frames_sent += 1
|
||||
|
||||
# 发送到FFmpeg
|
||||
try:
|
||||
self.ffmpeg_input_process.stdin.write(data)
|
||||
self.ffmpeg_input_process.stdin.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"[读取] 发送数据到FFmpeg出错: {str(e)}")
|
||||
break
|
||||
|
||||
# 输出统计信息
|
||||
current_time = time.time()
|
||||
if current_time - last_log_time > 5.0: # 每5秒记录一次
|
||||
logger.info(f"[读取] 已读取 {bytes_read/1024:.2f} KB PCM数据,发送 {frames_sent} 帧")
|
||||
last_log_time = current_time
|
||||
|
||||
# 检查是否有长时间未收到数据
|
||||
if self.audio_port.in_waiting == 0:
|
||||
time.sleep(0.01) # 短暂休眠
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[读取] 从串口读取数据出错: {str(e)}")
|
||||
time.sleep(0.1)
|
||||
|
||||
# 关闭FFmpeg输入
|
||||
try:
|
||||
if self.ffmpeg_input_process and self.ffmpeg_input_process.stdin:
|
||||
self.ffmpeg_input_process.stdin.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.info(f"[读取] 停止从串口读取,总计读取 {bytes_read/1024:.2f} KB PCM数据")
|
||||
|
||||
def _start_ffmpeg_output(self):
|
||||
"""启动从麦克风到串口的FFmpeg进程"""
|
||||
if not self.audio_port or not self.audio_port.is_open:
|
||||
logger.error("音频端口未打开,无法启动FFmpeg输出进程")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 配置FFmpeg命令行
|
||||
# 从麦克风录制PCM数据并发送到串口
|
||||
cmd = [
|
||||
FFMPEG_PATH,
|
||||
"-f", "dshow", # DirectShow输入
|
||||
"-i", "audio=@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\\wave_{DFDF5B7D-7597-4E7C-84D6-CFF1F7379E35}", # 默认麦克风
|
||||
"-ar", str(PCM_SAMPLE_RATE), # 采样率
|
||||
"-ac", str(PCM_CHANNELS), # 通道数
|
||||
"-loglevel", "warning", # 只显示警告和错误
|
||||
"-af", "volume=4,highpass=f=200,lowpass=f=3000,compand=0.3:0.8:-90/-60:-60/-40:-40/-30:-20/-20:0/-10:0.2", # 音频处理
|
||||
"-f", "s16le", # 16位有符号整数,小端序
|
||||
"pipe:1" # 输出到标准输出
|
||||
]
|
||||
|
||||
logger.info(f"启动FFmpeg输出进程: {' '.join(cmd)}")
|
||||
|
||||
# 启动FFmpeg进程
|
||||
self.ffmpeg_output_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
bufsize=0 # 无缓冲
|
||||
)
|
||||
|
||||
# 启动将FFmpeg输出写入串口的线程
|
||||
threading.Thread(
|
||||
target=self._write_from_ffmpeg_to_serial,
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
logger.info("FFmpeg输出进程启动成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"启动FFmpeg输出进程失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def _write_from_ffmpeg_to_serial(self):
|
||||
"""从FFmpeg读取数据并写入串口"""
|
||||
buffer_size = 320 # 每次读取/写入的字节数
|
||||
|
||||
# 用于统计的变量
|
||||
bytes_written = 0
|
||||
last_log_time = time.time()
|
||||
frames_sent = 0
|
||||
|
||||
logger.info("[发送] 开始从FFmpeg发送PCM数据到串口")
|
||||
|
||||
while self.is_running and self.call_active and not self.terminating:
|
||||
try:
|
||||
if not self.audio_port or not self.audio_port.is_open:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
if not self.ffmpeg_output_process or self.ffmpeg_output_process.poll() is not None:
|
||||
logger.warning("[发送] FFmpeg输出进程已结束,停止写入")
|
||||
break
|
||||
|
||||
# 读取数据
|
||||
data = self.ffmpeg_output_process.stdout.read(buffer_size)
|
||||
if data:
|
||||
# 写入串口
|
||||
self.audio_port.write(data)
|
||||
self.audio_port.flush()
|
||||
bytes_written += len(data)
|
||||
frames_sent += 1
|
||||
|
||||
# 输出统计信息
|
||||
current_time = time.time()
|
||||
if current_time - last_log_time > 5.0: # 每5秒记录一次
|
||||
logger.info(f"[发送] 已发送 {bytes_written/1024:.2f} KB PCM数据,发送 {frames_sent} 帧")
|
||||
last_log_time = current_time
|
||||
|
||||
# 短暂休眠,避免CPU占用过高
|
||||
time.sleep(0.01)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[发送] 写入数据到串口出错: {str(e)}")
|
||||
time.sleep(0.1)
|
||||
|
||||
logger.info(f"[发送] 停止写入串口,总计发送 {bytes_written/1024:.2f} KB PCM数据")
|
||||
|
||||
def start_audio_processing(self):
|
||||
"""启动音频处理"""
|
||||
if not self.audio_port:
|
||||
logger.error("未打开音频端口,无法启动音频处理")
|
||||
self.status_changed.emit("未打开音频端口,无法启动音频处理")
|
||||
return False
|
||||
|
||||
if self.is_running:
|
||||
logger.warning("音频处理已经在运行")
|
||||
return True
|
||||
|
||||
# 重置终止标志
|
||||
self.terminating = False
|
||||
|
||||
try:
|
||||
# 检查FFmpeg是否存在
|
||||
if not os.path.exists(FFMPEG_PATH) or not os.path.exists(FFPLAY_PATH):
|
||||
logger.error(f"找不到FFmpeg可执行文件: {FFMPEG_PATH} 或 {FFPLAY_PATH}")
|
||||
self.status_changed.emit("找不到FFmpeg可执行文件")
|
||||
return False
|
||||
|
||||
# 设置运行标志
|
||||
self.is_running = True
|
||||
|
||||
# 启动监控线程
|
||||
self.monitor_thread = threading.Thread(target=self._monitor_thread, daemon=True)
|
||||
self.monitor_thread.name = "FFmpegMonitorThread"
|
||||
self.monitor_thread.start()
|
||||
|
||||
logger.info("音频处理已启动")
|
||||
self.status_changed.emit("音频处理已启动")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动音频处理失败: {str(e)}")
|
||||
self.status_changed.emit(f"启动音频处理失败: {str(e)[:50]}")
|
||||
self._cleanup_resources()
|
||||
return False
|
||||
|
||||
def _monitor_thread(self):
|
||||
"""监控线程,确保FFmpeg进程正常运行"""
|
||||
logger.info("启动FFmpeg监控线程")
|
||||
|
||||
while self.is_running and not self.terminating:
|
||||
try:
|
||||
# 如果通话活动且FFmpeg进程需要启动
|
||||
if self.call_active:
|
||||
self._ensure_ffmpeg_running()
|
||||
|
||||
# 检查进程状态
|
||||
if self.ffmpeg_input_process and self.ffmpeg_input_process.poll() is not None:
|
||||
logger.warning(f"FFmpeg输入进程已退出,状态码: {self.ffmpeg_input_process.poll()}")
|
||||
if self.call_active and not self.terminating:
|
||||
logger.info("尝试重启FFmpeg输入进程")
|
||||
self._start_ffmpeg_input()
|
||||
|
||||
if self.ffmpeg_output_process and self.ffmpeg_output_process.poll() is not None:
|
||||
logger.warning(f"FFmpeg输出进程已退出,状态码: {self.ffmpeg_output_process.poll()}")
|
||||
if self.call_active and not self.terminating:
|
||||
logger.info("尝试重启FFmpeg输出进程")
|
||||
self._start_ffmpeg_output()
|
||||
|
||||
# 短暂休眠
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"监控线程出错: {str(e)}")
|
||||
time.sleep(1.0)
|
||||
|
||||
logger.info("FFmpeg监控线程退出")
|
||||
|
||||
# 单独测试功能
|
||||
if __name__ == "__main__":
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 测试FFmpeg音频功能
|
||||
audio = FFmpegAudio()
|
||||
|
||||
port = audio.find_audio_port()
|
||||
if port:
|
||||
print(f"找到音频端口: {port}")
|
||||
if audio.open_audio_port(port):
|
||||
print("成功打开音频端口")
|
||||
audio.start_audio_processing()
|
||||
print("按Enter键模拟通话开始...")
|
||||
input()
|
||||
audio.set_call_active(True)
|
||||
print("通话已开始,现在可以说话...按Enter键结束通话")
|
||||
input()
|
||||
audio.set_call_active(False)
|
||||
print("通话已结束")
|
||||
# 等待延迟关闭完成
|
||||
time.sleep(4)
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("未找到音频端口")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,233 @@
|
||||
import sys
|
||||
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QSize
|
||||
from PyQt5.QtGui import QPixmap, QIcon
|
||||
import winsound
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
|
||||
class IncomingCallDialog(QDialog):
|
||||
# 定义信号
|
||||
answer_signal = pyqtSignal()
|
||||
reject_signal = pyqtSignal()
|
||||
|
||||
def __init__(self, phone_number, caller_name=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.phone_number = phone_number
|
||||
self.caller_name = caller_name or "未知联系人"
|
||||
self.display_name = caller_name or phone_number
|
||||
self.init_ui()
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||
|
||||
# 初始化时间计时器
|
||||
self.start_time = time.time()
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.update_time)
|
||||
self.timer.start(500) # 每500毫秒更新一次
|
||||
|
||||
# 15秒后自动关闭对话框(如果未接听),模拟未接来电
|
||||
self.auto_close_timer = QTimer(self)
|
||||
self.auto_close_timer.timeout.connect(self.auto_reject)
|
||||
self.auto_close_timer.setSingleShot(True)
|
||||
self.auto_close_timer.start(15000) # 15秒后自动关闭
|
||||
|
||||
# 设置窗口标志
|
||||
self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
|
||||
|
||||
# 记录对话框状态
|
||||
self.answer_clicked = False
|
||||
self.reject_clicked = False
|
||||
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 来电对话框已创建: {self.display_name}")
|
||||
|
||||
def init_ui(self):
|
||||
self.setWindowTitle("来电")
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #F5F5F5;
|
||||
border: 1px solid #CCCCCC;
|
||||
border-radius: 10px;
|
||||
}
|
||||
QLabel {
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton {
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
}
|
||||
QPushButton#answer {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
QPushButton#answer:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
QPushButton#reject {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
QPushButton#reject:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
""")
|
||||
|
||||
# 主布局
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# 联系人头像
|
||||
avatar_label = QLabel()
|
||||
avatar_path = os.path.join("assets", "user.png") # 默认头像
|
||||
if os.path.exists(avatar_path):
|
||||
pixmap = QPixmap(avatar_path)
|
||||
avatar_label.setPixmap(pixmap.scaled(80, 80, Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
||||
avatar_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(avatar_label)
|
||||
|
||||
# 显示标题(来电)
|
||||
title_label = QLabel("来电")
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #333333;")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# 显示来电号码或联系人名称
|
||||
self.name_label = QLabel(self.display_name)
|
||||
self.name_label.setAlignment(Qt.AlignCenter)
|
||||
self.name_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 10px;")
|
||||
layout.addWidget(self.name_label)
|
||||
|
||||
# 显示号码(如果有联系人名称)
|
||||
if self.caller_name:
|
||||
number_label = QLabel(self.phone_number)
|
||||
number_label.setAlignment(Qt.AlignCenter)
|
||||
number_label.setStyleSheet("font-size: 12px; color: #555555;")
|
||||
layout.addWidget(number_label)
|
||||
|
||||
# 显示振铃时间
|
||||
self.time_label = QLabel("正在响铃...")
|
||||
self.time_label.setAlignment(Qt.AlignCenter)
|
||||
self.time_label.setStyleSheet("font-size: 12px; color: #555555; margin-top: 5px;")
|
||||
layout.addWidget(self.time_label)
|
||||
|
||||
# 按钮布局
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
# 接听按钮
|
||||
self.answer_button = QPushButton()
|
||||
self.answer_button.setIcon(QIcon(os.path.join("assets", "answer.png")))
|
||||
self.answer_button.setIconSize(QSize(30, 30))
|
||||
self.answer_button.setObjectName("answer")
|
||||
self.answer_button.setFixedSize(60, 60)
|
||||
self.answer_button.clicked.connect(self.accept_call)
|
||||
button_layout.addWidget(self.answer_button)
|
||||
|
||||
# 拒绝按钮
|
||||
self.reject_button = QPushButton()
|
||||
self.reject_button.setIcon(QIcon(os.path.join("assets", "hangup.png")))
|
||||
self.reject_button.setIconSize(QSize(30, 30))
|
||||
self.reject_button.setObjectName("reject")
|
||||
self.reject_button.setFixedSize(60, 60)
|
||||
self.reject_button.clicked.connect(self.reject_call)
|
||||
button_layout.addWidget(self.reject_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
self.setLayout(layout)
|
||||
self.setFixedSize(300, 350)
|
||||
|
||||
# 将对话框移到屏幕中央
|
||||
screen_geometry = self.screen().availableGeometry()
|
||||
x = (screen_geometry.width() - self.width()) // 2
|
||||
y = (screen_geometry.height() - self.height()) // 2
|
||||
self.move(x, y)
|
||||
|
||||
def update_time(self):
|
||||
"""更新来电持续时间"""
|
||||
elapsed = int(time.time() - self.start_time)
|
||||
minutes, seconds = divmod(elapsed, 60)
|
||||
self.time_label.setText(f"已振铃 {minutes:02d}:{seconds:02d}")
|
||||
|
||||
def accept_call(self):
|
||||
"""处理接听按钮点击"""
|
||||
if self.answer_clicked: # 防止重复点击
|
||||
return
|
||||
|
||||
self.answer_clicked = True
|
||||
self.auto_close_timer.stop() # 停止自动拒绝计时器
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 用户点击接听按钮: {self.display_name}")
|
||||
|
||||
# 发送接听信号
|
||||
self.answer_signal.emit()
|
||||
self.timer.stop()
|
||||
self.accept() # 关闭对话框
|
||||
|
||||
def reject_call(self):
|
||||
"""处理拒绝按钮点击"""
|
||||
if self.reject_clicked: # 防止重复点击
|
||||
return
|
||||
|
||||
self.reject_clicked = True
|
||||
self.auto_close_timer.stop() # 停止自动拒绝计时器
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 用户点击拒绝按钮: {self.display_name}")
|
||||
|
||||
# 发送拒绝信号
|
||||
self.reject_signal.emit()
|
||||
self.timer.stop()
|
||||
self.reject() # 关闭对话框
|
||||
|
||||
def auto_reject(self):
|
||||
"""自动拒绝来电(未接听超时)"""
|
||||
if not self.answer_clicked and not self.reject_clicked:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 来电自动拒绝(超时): {self.display_name}")
|
||||
self.reject_clicked = True
|
||||
|
||||
# 发送拒绝信号
|
||||
self.reject_signal.emit()
|
||||
self.timer.stop()
|
||||
self.reject() # 关闭对话框
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""处理对话框关闭事件"""
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 来电对话框关闭: {self.display_name}")
|
||||
self.timer.stop()
|
||||
self.auto_close_timer.stop()
|
||||
|
||||
# 如果是通过"X"按钮关闭的,没有点击任何按钮,则视为拒绝
|
||||
if not self.answer_clicked and not self.reject_clicked:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 窗口被直接关闭,视为拒绝")
|
||||
self.reject_signal.emit()
|
||||
|
||||
super().closeEvent(event)
|
||||
|
||||
def show_incoming_call(caller_number):
|
||||
"""显示来电对话框并返回用户选择(True表示接听,False表示拒绝)"""
|
||||
app = QApplication.instance()
|
||||
if not app:
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 确保没有遗留的来电对话框
|
||||
for widget in QApplication.topLevelWidgets():
|
||||
if isinstance(widget, IncomingCallDialog):
|
||||
print(f"发现正在显示的来电对话框,关闭它: {widget.display_name}")
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
|
||||
# 创建并显示新的来电对话框
|
||||
dialog = IncomingCallDialog(caller_number)
|
||||
result = dialog.exec_()
|
||||
|
||||
# 记录用户选择
|
||||
user_choice = dialog.answer_clicked
|
||||
|
||||
# 确保对话框被释放
|
||||
dialog.deleteLater()
|
||||
|
||||
return user_choice
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试来电对话框
|
||||
result = show_incoming_call("+8613800138000")
|
||||
print(f"用户选择了{'接听' if result else '拒绝'}来电")
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
+916
@@ -0,0 +1,916 @@
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
import re
|
||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTabWidget, QWidget,
|
||||
QVBoxLayout, QHBoxLayout, QLabel, QStatusBar, QMessageBox,
|
||||
QSystemTrayIcon, QMenu, QAction)
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QColor
|
||||
import traceback
|
||||
|
||||
# 使用PyInstaller打包时的资源文件注意事项:
|
||||
# ----------------------------------
|
||||
# 1. 图标文件应在spec文件中添加为附加数据:
|
||||
# a = Analysis(...,
|
||||
# datas=[
|
||||
# ('default.png', '.'),
|
||||
# ('running.png', '.'),
|
||||
# ('error.png', '.')
|
||||
# ],
|
||||
# ...)
|
||||
#
|
||||
# 2. 数据库文件将自动保存在用户主目录的.LTE文件夹中
|
||||
#
|
||||
# 3. 其他资源文件也应通过datas参数添加,例如声音文件等
|
||||
|
||||
from phone_sms_tab import PhoneSmsTab
|
||||
from settings_tab import SettingsTab
|
||||
from lte_manager import LTEManager
|
||||
from database import LTEDatabase
|
||||
from sound_utils import SoundManager
|
||||
from audio import PCMAudio
|
||||
from ffmpeg_audio import FFmpegAudio # 导入新的FFmpeg音频处理类
|
||||
from incoming_call import show_incoming_call
|
||||
|
||||
class LTEToolApp(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("LTE Tool")
|
||||
self.resize(800, 600)
|
||||
|
||||
start_time = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f"{start_time} - LTE Tool 应用程序启动")
|
||||
|
||||
# 添加更新周期计数器,用于控制不同信息的更新频率
|
||||
# 初始化为1,确保首次连接时执行完整的信息获取
|
||||
self.update_counter = 1
|
||||
|
||||
# 添加应用退出标志,用于区分最小化到托盘和退出程序
|
||||
self.is_exiting = False
|
||||
|
||||
# 添加来电对话框标志,防止重复显示来电界面
|
||||
self.incoming_call_dialog_visible = False
|
||||
self.current_incoming_call_number = None
|
||||
self._incoming_call_dialog = None
|
||||
|
||||
# 加载图标文件
|
||||
self.load_icons()
|
||||
|
||||
# 设置应用图标
|
||||
self.setWindowIcon(self.default_icon)
|
||||
|
||||
# 创建系统托盘图标
|
||||
self.tray_icon = QSystemTrayIcon(self)
|
||||
self.tray_icon.setIcon(self.default_icon) # 初始使用默认图标
|
||||
self.tray_icon.setToolTip("LTE Tool - 未连接")
|
||||
|
||||
# 判断是否使用FFmpeg
|
||||
self.use_ffmpeg = False # 设置为False,禁用所有音频处理
|
||||
|
||||
# PCM音频处理器(已禁用)
|
||||
self.audio_processor = None
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 已禁用PCM音频处理")
|
||||
|
||||
# 设置系统托盘菜单和显示图标
|
||||
self.setup_tray_icon()
|
||||
|
||||
# 打印系统托盘状态信息
|
||||
print("系统托盘可用:", QSystemTrayIcon.isSystemTrayAvailable())
|
||||
print("托盘图标可见:", self.tray_icon.isVisible())
|
||||
|
||||
# 创建 LTE 管理器
|
||||
self.lte_manager = LTEManager()
|
||||
|
||||
# 创建数据库路径 - 使用用户主目录下的.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)
|
||||
db_path = os.path.join(lte_dir, 'lte_data.db')
|
||||
print(f"数据库路径: {db_path}")
|
||||
|
||||
# 创建数据库
|
||||
self.database = LTEDatabase(db_path=db_path)
|
||||
|
||||
# 创建声音管理器
|
||||
self.sound_manager = SoundManager()
|
||||
|
||||
# 创建主窗口部件和布局
|
||||
self.central_widget = QWidget()
|
||||
self.setCentralWidget(self.central_widget)
|
||||
self.main_layout = QVBoxLayout(self.central_widget)
|
||||
|
||||
# 创建标签页部件
|
||||
self.tab_widget = QTabWidget()
|
||||
self.main_layout.addWidget(self.tab_widget)
|
||||
|
||||
# 创建标签页
|
||||
self.phone_sms_tab = PhoneSmsTab(self.lte_manager, self.database, self.sound_manager)
|
||||
self.settings_tab = SettingsTab(self.lte_manager)
|
||||
|
||||
# 创建GitHub链接标签页
|
||||
self.github_tab = QWidget()
|
||||
github_layout = QVBoxLayout(self.github_tab)
|
||||
|
||||
# 添加GitHub链接标签
|
||||
github_label = QLabel("访问GitHub项目页面获取最新版本和更新:")
|
||||
github_label.setAlignment(Qt.AlignCenter)
|
||||
github_layout.addWidget(github_label)
|
||||
|
||||
# 添加GitHub链接按钮
|
||||
github_link = QLabel('<a href="https://github.com/R0nY3n/LTE_manager">https://github.com/R0nY3n/LTE_manager</a>')
|
||||
github_link.setAlignment(Qt.AlignCenter)
|
||||
github_link.setOpenExternalLinks(True) # 允许打开外部链接
|
||||
github_link.setTextInteractionFlags(Qt.TextBrowserInteraction) # 允许文本交互
|
||||
github_layout.addWidget(github_link)
|
||||
|
||||
# 添加说明文本
|
||||
info_label = QLabel("欢迎在GitHub上提交问题、建议或贡献代码!")
|
||||
info_label.setAlignment(Qt.AlignCenter)
|
||||
github_layout.addWidget(info_label)
|
||||
|
||||
# 添加空白区域
|
||||
github_layout.addStretch()
|
||||
|
||||
# 添加标签页
|
||||
self.tab_widget.addTab(self.phone_sms_tab, "电话和短信")
|
||||
self.tab_widget.addTab(self.settings_tab, "设置")
|
||||
self.tab_widget.addTab(self.github_tab, "GitHub")
|
||||
|
||||
# 状态栏部件
|
||||
self.status_carrier = QLabel("运营商: 未连接")
|
||||
self.status_phone = QLabel("电话: 不可用")
|
||||
self.status_network = QLabel("网络: 未连接")
|
||||
self.status_signal = QLabel("信号: 不可用")
|
||||
self.audio_status_label = QLabel("音频: 未初始化")
|
||||
self.call_status_label = QLabel("通话: 无通话") # 添加通话状态标签
|
||||
|
||||
# 添加部件到状态栏
|
||||
self.statusBar().addWidget(self.status_carrier)
|
||||
self.statusBar().addWidget(self.status_phone)
|
||||
self.statusBar().addWidget(self.status_network)
|
||||
self.statusBar().addWidget(self.status_signal)
|
||||
self.statusBar().addWidget(self.audio_status_label)
|
||||
self.statusBar().addWidget(self.call_status_label) # 添加到状态栏
|
||||
|
||||
# 现在可以安全地更新连接状态(初始为未连接)
|
||||
self.update_connection_status(False)
|
||||
|
||||
# 更新状态计时器 - 增加更长的更新间隔
|
||||
self.status_timer = QTimer()
|
||||
self.status_timer.timeout.connect(self.update_status_bar)
|
||||
self.status_timer.start(10000) # 增加到10秒更新一次(从5秒改为10秒)
|
||||
|
||||
# 通话状态检查计时器 - 修改为不定期检查模式
|
||||
self.call_status_timer = QTimer()
|
||||
self.call_status_timer.timeout.connect(self.check_call_status)
|
||||
# 不再固定间隔调用check_call_status
|
||||
# self.call_status_timer.start(1000) # 每秒检查一次通话状态
|
||||
|
||||
# 添加通话状态检查标志,用于控制何时检查通话状态
|
||||
self.should_check_call_status = False
|
||||
self.call_check_count = 0
|
||||
self.max_call_checks = 3 # 最多连续检查3次
|
||||
|
||||
# 连接信号
|
||||
self.lte_manager.status_changed.connect(self.on_status_changed)
|
||||
|
||||
# 连接短信接收信号以显示通知
|
||||
self.lte_manager.sms_received.connect(self.on_sms_received_notification)
|
||||
self.lte_manager.call_received.connect(self.on_call_received_notification)
|
||||
|
||||
# 连接通话结束信号
|
||||
self.lte_manager.call_ended.connect(self.on_call_ended)
|
||||
|
||||
# 连接PCM音频状态信号
|
||||
self.lte_manager.pcm_audio_status.connect(self.on_pcm_audio_status_changed)
|
||||
|
||||
# 尝试自动连接(如果启用)
|
||||
QTimer.singleShot(1000, self.try_auto_connect)
|
||||
|
||||
def initialize_audio_processor(self):
|
||||
"""初始化PCM音频处理器(已禁用实际处理)"""
|
||||
try:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 音频处理已禁用,仅创建空壳")
|
||||
# 不再实际初始化音频处理器,但保留接口兼容性
|
||||
self.audio_processor = None
|
||||
self.audio_status_label.setText("音频: 已禁用")
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 初始化音频处理器出错: {str(e)}")
|
||||
self.audio_processor = None
|
||||
# 确保异常处理中的状态更新也是安全的
|
||||
try:
|
||||
self.audio_status_label.setText("音频: 初始化失败")
|
||||
except:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 无法更新音频状态标签")
|
||||
|
||||
def on_pcm_audio_status_changed(self, registered):
|
||||
"""处理PCM音频注册状态变化"""
|
||||
try:
|
||||
# PCM音频已注册,只记录状态但不处理音频
|
||||
if registered:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频已注册,但不执行音频处理(已禁用)")
|
||||
self.audio_status_label.setText("音频: PCM已注册(处理已禁用)")
|
||||
else:
|
||||
# PCM音频已取消注册,只记录状态
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频已注销")
|
||||
self.audio_status_label.setText("音频: 非活动")
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - PCM音频状态变化处理出错: {str(e)}")
|
||||
try:
|
||||
self.audio_status_label.setText("音频: 错误")
|
||||
except:
|
||||
pass
|
||||
|
||||
def try_auto_connect(self):
|
||||
"""尝试自动连接到LTE模块"""
|
||||
# 调用设置标签页的自动连接方法
|
||||
if hasattr(self, 'settings_tab') and self.settings_tab:
|
||||
self.settings_tab.try_auto_connect()
|
||||
|
||||
def load_icons(self):
|
||||
"""加载应用图标和状态图标"""
|
||||
# 获取应用程序资源目录
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 如果是打包后的可执行文件
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
# 创建临时资源目录用于PyInstaller
|
||||
temp_dir = getattr(sys, '_MEIPASS', base_dir)
|
||||
resource_dir = temp_dir
|
||||
else:
|
||||
# 如果是开发环境
|
||||
resource_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 加载状态图标
|
||||
self.default_icon = None # 默认图标 - 未连接时使用
|
||||
self.running_icon = None # 运行图标 - 连接成功时使用
|
||||
self.error_icon = None # 错误图标 - 连接错误时使用
|
||||
|
||||
# 定义图标路径
|
||||
default_icon_path = os.path.join(resource_dir, "default.png")
|
||||
running_icon_path = os.path.join(resource_dir, "running.png")
|
||||
error_icon_path = os.path.join(resource_dir, "error.png")
|
||||
|
||||
# 加载默认图标 (default.png)
|
||||
if os.path.exists(default_icon_path):
|
||||
self.default_icon = QIcon(default_icon_path)
|
||||
self.app_icon = self.default_icon # 默认应用图标
|
||||
print(f"成功加载默认图标: {default_icon_path}")
|
||||
else:
|
||||
# 创建默认图标作为备用
|
||||
print(f"找不到默认图标文件: {default_icon_path},使用内置图标")
|
||||
default_pixmap = QPixmap(32, 32)
|
||||
default_pixmap.fill(QColor(100, 149, 237)) # 康乃馨蓝色
|
||||
self.default_icon = QIcon(default_pixmap)
|
||||
self.app_icon = self.default_icon
|
||||
|
||||
# 加载运行图标 (running.png)
|
||||
if os.path.exists(running_icon_path):
|
||||
self.running_icon = QIcon(running_icon_path)
|
||||
print(f"成功加载运行图标: {running_icon_path}")
|
||||
else:
|
||||
# 创建运行图标作为备用
|
||||
print(f"找不到运行图标文件: {running_icon_path},使用内置图标")
|
||||
running_pixmap = QPixmap(32, 32)
|
||||
running_pixmap.fill(QColor(60, 179, 113)) # 中等海洋绿
|
||||
self.running_icon = QIcon(running_pixmap)
|
||||
|
||||
# 加载错误图标 (error.png)
|
||||
if os.path.exists(error_icon_path):
|
||||
self.error_icon = QIcon(error_icon_path)
|
||||
print(f"成功加载错误图标: {error_icon_path}")
|
||||
else:
|
||||
# 创建错误图标作为备用
|
||||
print(f"找不到错误图标文件: {error_icon_path},使用内置图标")
|
||||
error_pixmap = QPixmap(32, 32)
|
||||
error_pixmap.fill(QColor(220, 20, 60)) # 猩红色
|
||||
self.error_icon = QIcon(error_pixmap)
|
||||
|
||||
def setup_tray_icon(self):
|
||||
"""设置系统托盘图标和菜单"""
|
||||
# 创建托盘菜单
|
||||
tray_menu = QMenu()
|
||||
|
||||
# 添加操作
|
||||
show_action = QAction("显示", self)
|
||||
show_action.triggered.connect(self.show)
|
||||
tray_menu.addAction(show_action)
|
||||
|
||||
hide_action = QAction("隐藏", self)
|
||||
hide_action.triggered.connect(self.hide)
|
||||
tray_menu.addAction(hide_action)
|
||||
|
||||
tray_menu.addSeparator()
|
||||
|
||||
# 添加音频已禁用的通知项
|
||||
audio_disabled_action = QAction("音频处理已禁用", self)
|
||||
audio_disabled_action.setEnabled(False) # 不可点击
|
||||
tray_menu.addAction(audio_disabled_action)
|
||||
|
||||
tray_menu.addSeparator()
|
||||
|
||||
# 连接状态操作(不可点击)
|
||||
self.connection_status_action = QAction("未连接", self)
|
||||
self.connection_status_action.setEnabled(False)
|
||||
tray_menu.addAction(self.connection_status_action)
|
||||
|
||||
tray_menu.addSeparator()
|
||||
|
||||
exit_action = QAction("退出", self)
|
||||
exit_action.triggered.connect(self._exit_application)
|
||||
tray_menu.addAction(exit_action)
|
||||
|
||||
# 设置托盘菜单
|
||||
self.tray_icon.setContextMenu(tray_menu)
|
||||
|
||||
# 连接信号
|
||||
self.tray_icon.activated.connect(self.on_tray_icon_activated)
|
||||
|
||||
# 显示托盘图标
|
||||
self.tray_icon.show()
|
||||
|
||||
def on_tray_icon_activated(self, reason):
|
||||
"""处理托盘图标激活"""
|
||||
if reason == QSystemTrayIcon.DoubleClick:
|
||||
if self.isVisible():
|
||||
self.hide()
|
||||
else:
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
|
||||
def on_sms_received_notification(self, sender, timestamp, message):
|
||||
"""收到短信时显示通知"""
|
||||
if self.tray_icon.isVisible():
|
||||
# 如果消息太长则截断
|
||||
display_message = message[:50] + "..." if len(message) > 50 else message
|
||||
self.tray_icon.showMessage(
|
||||
"新短信",
|
||||
f"发件人: {sender}\n{display_message}",
|
||||
QSystemTrayIcon.Information,
|
||||
5000 # 显示5秒
|
||||
)
|
||||
|
||||
def on_call_received_notification(self, caller_number):
|
||||
"""收到来电时显示通知和接听选项"""
|
||||
try:
|
||||
# 检查是否已有来电对话框正在显示,避免重复显示
|
||||
if self.incoming_call_dialog_visible:
|
||||
# 如果是同一个号码的来电,忽略此次通知
|
||||
if self.current_incoming_call_number == caller_number:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 已有来电对话框显示中,忽略重复通知: {caller_number}")
|
||||
return
|
||||
else:
|
||||
# 如果是新号码,可能是之前的通知没有正确清理
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 检测到新来电,但旧对话框未关闭,强制清理: {self.current_incoming_call_number} -> {caller_number}")
|
||||
# 继续处理新来电,旧对话框会在接听或拒绝时自动关闭
|
||||
|
||||
print(f"收到来电: {caller_number}")
|
||||
|
||||
# 设置当前来电号码和对话框状态
|
||||
self.current_incoming_call_number = caller_number
|
||||
self.incoming_call_dialog_visible = True
|
||||
|
||||
# 确保应用程序窗口可见
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
|
||||
# 播放来电铃声
|
||||
self.sound_manager.play_incoming_call()
|
||||
|
||||
# 显示系统通知
|
||||
if self.tray_icon.isVisible():
|
||||
self.tray_icon.showMessage(
|
||||
"来电",
|
||||
f"号码: {caller_number}",
|
||||
QSystemTrayIcon.Information,
|
||||
5000 # 显示5秒
|
||||
)
|
||||
|
||||
# 立即在数据库中记录来电
|
||||
self.database.add_call(caller_number, None, "未接来电", 0)
|
||||
|
||||
# 立即显示来电对话框 - 不再使用QTimer延迟
|
||||
self._show_incoming_call_dialog(caller_number)
|
||||
|
||||
# 设置应当检查通话状态的标志,并启动计时器
|
||||
self.should_check_call_status = True
|
||||
self.call_check_count = 0
|
||||
if not self.call_status_timer.isActive():
|
||||
self.call_status_timer.start(1000) # 开始每秒检查一次通话状态
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理来电通知时出错: {str(e)}")
|
||||
# 确保铃声停止
|
||||
self.sound_manager.stop_incoming_call()
|
||||
# 重置来电对话框状态
|
||||
self.incoming_call_dialog_visible = False
|
||||
self.current_incoming_call_number = None
|
||||
|
||||
def _show_incoming_call_dialog(self, phone_number, caller_name=None):
|
||||
"""显示来电对话框"""
|
||||
try:
|
||||
# 如果当前已经有来电对话框,先关闭它
|
||||
if self._incoming_call_dialog is not None and self._incoming_call_dialog.isVisible():
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 关闭已有的来电对话框")
|
||||
self._ensure_ringtone_stopped()
|
||||
self._incoming_call_dialog.close()
|
||||
self._incoming_call_dialog = None
|
||||
|
||||
# 检查通话状态,确保确实有来电
|
||||
calls = self.lte_manager.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:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 没有检测到来电,取消显示对话框")
|
||||
self._ensure_ringtone_stopped()
|
||||
return
|
||||
|
||||
# 查找联系人信息
|
||||
if caller_name is None:
|
||||
contact = self.phone_sms_tab.contacts_tab.find_contact_by_number(phone_number)
|
||||
caller_name = contact["name"] if contact else None
|
||||
|
||||
# 记录通话信息到数据库
|
||||
call_type = "未接来电" # 初始设置为未接,后续根据用户操作修改
|
||||
self.database.add_call(phone_number, caller_name, call_type, 0)
|
||||
|
||||
# 播放来电铃声
|
||||
self.sound_manager.play_incoming_call()
|
||||
|
||||
# 创建并显示对话框
|
||||
self._incoming_call_dialog = IncomingCallDialog(
|
||||
phone_number,
|
||||
caller_name,
|
||||
parent=self
|
||||
)
|
||||
|
||||
# 连接信号到槽
|
||||
self._incoming_call_dialog.answer_signal.connect(
|
||||
lambda: self._on_answer_call(phone_number, caller_name)
|
||||
)
|
||||
self._incoming_call_dialog.reject_signal.connect(
|
||||
lambda: self._on_reject_call(phone_number, caller_name)
|
||||
)
|
||||
|
||||
# 连接对话框关闭信号,确保铃声停止
|
||||
self._incoming_call_dialog.finished.connect(self._ensure_ringtone_stopped)
|
||||
|
||||
# 显示对话框
|
||||
self._incoming_call_dialog.show()
|
||||
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 显示来电对话框出错: {str(e)}")
|
||||
traceback.print_exc()
|
||||
# 确保在异常情况下也停止铃声
|
||||
self._ensure_ringtone_stopped()
|
||||
# 重置对话框状态
|
||||
self._incoming_call_dialog = None
|
||||
|
||||
def _on_answer_call(self, phone_number, caller_name=None):
|
||||
"""处理接听来电"""
|
||||
try:
|
||||
# 1. 立即停止铃声
|
||||
self._ensure_ringtone_stopped()
|
||||
|
||||
# 2. 尝试接听电话
|
||||
result = self.lte_manager.answer_call()
|
||||
|
||||
# 设置应当检查通话状态的标志,并启动计时器
|
||||
self.should_check_call_status = True
|
||||
self.call_check_count = 0
|
||||
if not self.call_status_timer.isActive():
|
||||
self.call_status_timer.start(1000) # 开始每秒检查一次通话状态
|
||||
|
||||
# 3. 检查通话状态,确认是否实际接通(即使API返回失败)
|
||||
time.sleep(0.5) # 给模块一点时间更新状态
|
||||
calls = self.lte_manager.get_call_status()
|
||||
call_connected = False
|
||||
|
||||
for call in calls:
|
||||
if call.get('stat') in [0, 1] and call.get('dir') == 1: # 活动或保持的呼入通话
|
||||
call_connected = True
|
||||
break
|
||||
|
||||
if result or call_connected:
|
||||
# 4. 修改数据库中的通话记录类型为"已接来电"
|
||||
self.database.update_call_type(phone_number, "已接来电")
|
||||
|
||||
# 5. 更新UI状态
|
||||
self.phone_sms_tab.add_to_call_log(f"已接听来电: {phone_number}")
|
||||
self.phone_sms_tab.refresh_call_log()
|
||||
|
||||
# 6. 再次检查通话状态,确认通话是否仍然活跃
|
||||
calls = self.lte_manager.get_call_status()
|
||||
is_call_active = False
|
||||
for call in calls:
|
||||
if call.get('stat') in [0, 1]: # 活动或保持状态
|
||||
is_call_active = True
|
||||
break
|
||||
|
||||
if not is_call_active:
|
||||
# 如果通话已结束,确保再次停止铃声
|
||||
self._ensure_ringtone_stopped()
|
||||
else:
|
||||
# 通知接听失败
|
||||
QMessageBox.warning(self, "通话错误", "接听来电失败")
|
||||
self.sound_manager.play_error()
|
||||
self._ensure_ringtone_stopped() # 再次确保铃声停止
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 接听来电出错: {str(e)}")
|
||||
traceback.print_exc()
|
||||
self._ensure_ringtone_stopped() # 确保在异常情况下也停止铃声
|
||||
|
||||
def _on_reject_call(self, phone_number, caller_name=None):
|
||||
"""处理拒接来电"""
|
||||
try:
|
||||
# 1. 立即停止铃声
|
||||
self._ensure_ringtone_stopped()
|
||||
|
||||
# 设置应当检查通话状态的标志,并启动计时器
|
||||
self.should_check_call_status = True
|
||||
self.call_check_count = 0
|
||||
if not self.call_status_timer.isActive():
|
||||
self.call_status_timer.start(1000) # 开始每秒检查一次通话状态
|
||||
|
||||
# 2. 尝试挂断电话
|
||||
if self.lte_manager.end_call():
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 已拒绝来电: {phone_number}")
|
||||
# 3. 数据库中的通话记录类型保持为"未接来电"
|
||||
|
||||
# 4. 更新UI状态
|
||||
self.phone_sms_tab.add_to_call_log(f"已拒绝来电: {phone_number}")
|
||||
self.phone_sms_tab.refresh_call_log()
|
||||
else:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 拒绝来电失败: {phone_number}")
|
||||
|
||||
# 5. 再次确保铃声停止
|
||||
self._ensure_ringtone_stopped()
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 拒绝来电出错: {str(e)}")
|
||||
traceback.print_exc()
|
||||
self._ensure_ringtone_stopped() # 确保在异常情况下也停止铃声
|
||||
|
||||
def _ensure_ringtone_stopped(self):
|
||||
"""确保所有铃声已停止"""
|
||||
try:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 确保所有铃声已停止")
|
||||
self.sound_manager.stop_ringtone()
|
||||
self.sound_manager.stop_incoming_call()
|
||||
|
||||
# 额外尝试停止系统声音
|
||||
try:
|
||||
import winsound
|
||||
winsound.PlaySound(None, winsound.SND_PURGE)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 如果还有声音线程在运行,给它们时间结束
|
||||
time.sleep(0.2)
|
||||
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 铃声停止过程完成")
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 停止铃声出错: {str(e)}")
|
||||
traceback.print_exc()
|
||||
|
||||
def on_call_ended(self, duration):
|
||||
"""处理通话结束事件"""
|
||||
# 确保来电对话框状态被重置
|
||||
self.incoming_call_dialog_visible = False
|
||||
self.current_incoming_call_number = None
|
||||
|
||||
# 记录通话结束信息
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 接收到通话结束信号,持续时间: {duration}")
|
||||
|
||||
# 设置应当检查通话状态的标志,并启动计时器确认通话已结束
|
||||
self.should_check_call_status = True
|
||||
self.call_check_count = 0
|
||||
if not self.call_status_timer.isActive():
|
||||
self.call_status_timer.start(1000) # 开始每秒检查一次通话状态
|
||||
|
||||
# 使用状态栏显示消息
|
||||
if duration.isdigit():
|
||||
# 格式化持续时间(秒 -> 分:秒)
|
||||
seconds = int(duration)
|
||||
minutes = seconds // 60
|
||||
remaining_seconds = seconds % 60
|
||||
formatted_duration = f"{minutes}:{remaining_seconds:02d}"
|
||||
self.statusBar().showMessage(f"通话结束,持续时间: {formatted_duration}", 5000)
|
||||
else:
|
||||
# 如果不是数字(例如"Call ended"或"Missed")
|
||||
self.statusBar().showMessage(f"通话结束: {duration}", 5000)
|
||||
|
||||
# 更新数据库中的通话记录
|
||||
if self.lte_manager.call_number:
|
||||
try:
|
||||
# 将持续时间转换为秒
|
||||
if duration.isdigit():
|
||||
duration_seconds = int(duration)
|
||||
else:
|
||||
duration_seconds = 0
|
||||
|
||||
# 查找最近的与此号码相关的通话记录
|
||||
calls = self.database.get_call_history(limit=1, phone_number=self.lte_manager.call_number)
|
||||
if calls:
|
||||
# 更新现有记录
|
||||
call_id = calls[0][0] # 第一列是ID
|
||||
# 更新持续时间和备注
|
||||
self.database.cursor.execute(
|
||||
"UPDATE call_history SET duration = ?, notes = NULL WHERE id = ?",
|
||||
(duration_seconds, call_id)
|
||||
)
|
||||
self.database.conn.commit()
|
||||
print(f"更新通话记录ID {call_id},持续时间 {duration_seconds}秒")
|
||||
else:
|
||||
# 如果找不到记录,添加一个新记录(这应该是不常见的情况)
|
||||
self.database.add_call(
|
||||
self.lte_manager.call_number,
|
||||
None,
|
||||
"未接来电" if duration == "Missed" or duration_seconds == 0 else "已接来电",
|
||||
duration_seconds
|
||||
)
|
||||
print(f"新增通话记录,号码 {self.lte_manager.call_number},持续时间 {duration_seconds}秒")
|
||||
except Exception as e:
|
||||
print(f"更新通话记录出错: {str(e)}")
|
||||
|
||||
def _reset_audio_processor_state(self):
|
||||
"""重置音频处理器状态(已简化为空操作)"""
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 重置音频处理器状态(空操作,处理已禁用)")
|
||||
|
||||
def _stop_audio_with_timeout(self):
|
||||
"""停止音频处理(已简化为空操作)"""
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 停止音频处理(空操作,处理已禁用)")
|
||||
|
||||
def _cleanup_audio_resources(self):
|
||||
"""清理所有音频相关资源(已简化为仅日志记录)"""
|
||||
# 更新状态
|
||||
try:
|
||||
self.audio_status_label.setText("音频: 非活动")
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 音频资源清理(空操作,处理已禁用)")
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 清理音频资源出错: {str(e)}")
|
||||
|
||||
# 确保停止任何正在播放的声音
|
||||
try:
|
||||
self.sound_manager.stop_ringtone()
|
||||
self.sound_manager.stop_incoming_call()
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 停止声音时出错: {str(e)}")
|
||||
|
||||
def check_call_status(self):
|
||||
"""定期检查通话状态并更新UI"""
|
||||
if not self.lte_manager.is_connected():
|
||||
# 如果未连接,停止检查
|
||||
self.call_status_timer.stop()
|
||||
self.should_check_call_status = False
|
||||
return
|
||||
|
||||
# 检查是否需要进行通话状态检查
|
||||
if not self.should_check_call_status:
|
||||
# 如果不需要继续检查,停止定时器
|
||||
self.call_status_timer.stop()
|
||||
return
|
||||
|
||||
# 增加检查计数
|
||||
self.call_check_count += 1
|
||||
|
||||
try:
|
||||
# 获取当前通话状态文本
|
||||
call_state = self.lte_manager.get_call_state_text()
|
||||
|
||||
# 更新状态栏
|
||||
self.call_status_label.setText(f"通话: {call_state}")
|
||||
|
||||
# 根据通话状态更新通话按钮状态
|
||||
calls = self.lte_manager.get_call_status()
|
||||
|
||||
# 检查是否有应该显示的来电提示
|
||||
if calls and not self.incoming_call_dialog_visible:
|
||||
for call in calls:
|
||||
if call.get('stat') == 4 and call.get('dir') == 1: # 来电中(MT)
|
||||
number = call.get('number', '未知号码')
|
||||
# 不在通知中直接显示来电对话框,因为呼叫信号会通过call_received正常触发
|
||||
break
|
||||
|
||||
# 更新UI以反映当前的通话状态
|
||||
self.phone_sms_tab.update_call_ui_state(bool(calls))
|
||||
|
||||
# 如果到达最大检查次数或没有活跃通话,停止定期检查
|
||||
if self.call_check_count >= self.max_call_checks or not calls:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 通话状态检查完成,停止定期检查")
|
||||
self.should_check_call_status = False
|
||||
self.call_status_timer.stop()
|
||||
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 检查通话状态出错: {str(e)}")
|
||||
# 出错时也停止检查
|
||||
self.should_check_call_status = False
|
||||
self.call_status_timer.stop()
|
||||
|
||||
# 在phone_sms_tab.py中调用dial_button和end_call_button点击时也需要手动触发通话状态检查
|
||||
|
||||
def update_status_bar(self):
|
||||
"""更新状态栏显示的信息"""
|
||||
if not self.lte_manager.is_connected():
|
||||
return
|
||||
|
||||
try:
|
||||
# 递增更新计数器,用于控制不同信息的更新频率
|
||||
self.update_counter += 1
|
||||
|
||||
# 每次都更新信号强度
|
||||
signal_info = self.lte_manager.get_signal_strength()
|
||||
if signal_info:
|
||||
try:
|
||||
if isinstance(signal_info, tuple) and len(signal_info) == 2:
|
||||
signal_text, signal_desc = signal_info
|
||||
if signal_desc:
|
||||
self.status_signal.setText(f"信号: {signal_text} ({signal_desc})")
|
||||
else:
|
||||
self.status_signal.setText(f"信号: {signal_text}")
|
||||
else:
|
||||
self.status_signal.setText(f"信号: {signal_info}")
|
||||
except Exception as e:
|
||||
print(f"处理信号强度信息出错: {str(e)}")
|
||||
self.status_signal.setText(f"信号: {signal_info}")
|
||||
|
||||
# 仅在首次连接或每10个周期更新一次运营商信息和电话号码
|
||||
if self.update_counter == 1 or self.update_counter % 10 == 0:
|
||||
# 更新运营商信息
|
||||
carrier_info = self.lte_manager.get_carrier_info()
|
||||
if carrier_info:
|
||||
try:
|
||||
if isinstance(carrier_info, tuple) and len(carrier_info) == 2:
|
||||
carrier, network_type = carrier_info
|
||||
self.status_carrier.setText(f"运营商: {carrier} ({network_type})")
|
||||
else:
|
||||
self.status_carrier.setText(f"运营商: {carrier_info}")
|
||||
except Exception as e:
|
||||
print(f"处理运营商信息出错: {str(e)}")
|
||||
self.status_carrier.setText(f"运营商: {carrier_info}")
|
||||
|
||||
# 更新电话号码
|
||||
phone_number = self.lte_manager.get_phone_number()
|
||||
if phone_number:
|
||||
self.status_phone.setText(f"电话: {phone_number}")
|
||||
|
||||
# 如果计数器达到30,重置它
|
||||
if self.update_counter >= 30:
|
||||
self.update_counter = 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"更新状态栏时出错: {str(e)}")
|
||||
traceback.print_exc()
|
||||
|
||||
def on_status_changed(self, status):
|
||||
"""处理状态变化"""
|
||||
try:
|
||||
# 在状态栏显示消息
|
||||
self.statusBar().showMessage(status, 5000)
|
||||
|
||||
# 更新托盘图标中的连接状态
|
||||
if "Connected to LTE module" in status:
|
||||
self.update_connection_status(True)
|
||||
# 连接成功后强制立即进行第一次状态更新(使用延时确保连接流程完成后再更新)
|
||||
self.update_counter = 0 # 重置计数器
|
||||
QTimer.singleShot(500, self.update_status_bar) # 0.5秒后更新状态栏
|
||||
elif "Disconnected from LTE module" in status:
|
||||
self.update_connection_status(False)
|
||||
# 立即更新状态栏为未连接状态
|
||||
self.status_carrier.setText("运营商: 未连接")
|
||||
self.status_phone.setText("电话: 不可用")
|
||||
self.status_network.setText("网络: 未连接")
|
||||
self.status_signal.setText("信号: 不可用")
|
||||
self.call_status_label.setText("通话: 无通话")
|
||||
elif "error" in status.lower() or "失败" in status or "failed" in status.lower():
|
||||
# 检测到错误状态
|
||||
self.show_error_status(status)
|
||||
# 出错时可能需要重新获取某些信息,强制下次执行完整更新
|
||||
self.update_counter = 9
|
||||
except Exception as e:
|
||||
print(f"状态更新出错: {str(e)}")
|
||||
self.show_error_status(f"状态更新出错: {str(e)}")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""处理应用关闭事件"""
|
||||
# 如果是通过退出菜单触发的关闭,直接关闭应用
|
||||
if self.is_exiting:
|
||||
self._cleanup_and_exit(event)
|
||||
return
|
||||
|
||||
# 显示确认对话框,询问是否退出或最小化到托盘
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
'关闭确认',
|
||||
'您希望退出程序还是最小化到系统托盘?\n\n点击"是"退出程序\n点击"否"最小化到系统托盘',
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
# 用户选择退出
|
||||
self._cleanup_and_exit(event)
|
||||
else:
|
||||
# 用户选择最小化到托盘
|
||||
event.ignore()
|
||||
self.hide()
|
||||
self.tray_icon.showMessage(
|
||||
"LTE Tool",
|
||||
"应用程序已最小化到系统托盘。双击图标可恢复窗口。",
|
||||
QSystemTrayIcon.Information,
|
||||
2000
|
||||
)
|
||||
|
||||
def _cleanup_and_exit(self, event):
|
||||
"""清理资源并退出应用"""
|
||||
# 停止所有声音
|
||||
self.sound_manager.stop_ringtone()
|
||||
self.sound_manager.stop_incoming_call()
|
||||
|
||||
# 关闭数据库连接
|
||||
self.database.close()
|
||||
|
||||
# 断开LTE模块连接
|
||||
if self.lte_manager.is_connected():
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 正在关闭LTE模块连接...")
|
||||
self.lte_manager.disconnect()
|
||||
|
||||
# 移除托盘图标
|
||||
if self.tray_icon.isVisible():
|
||||
self.tray_icon.hide()
|
||||
|
||||
# 接受关闭事件
|
||||
event.accept()
|
||||
|
||||
def _exit_application(self):
|
||||
"""退出应用程序"""
|
||||
if self.lte_manager and self.lte_manager.is_connected():
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 正在关闭LTE模块连接...")
|
||||
self.lte_manager.disconnect()
|
||||
|
||||
self.is_exiting = True
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - LTE Tool 应用程序退出")
|
||||
QApplication.quit()
|
||||
|
||||
def update_connection_status(self, connected):
|
||||
"""更新托盘图标中的连接状态"""
|
||||
try:
|
||||
if connected:
|
||||
# 使用运行图标表示连接成功
|
||||
self.tray_icon.setIcon(self.running_icon)
|
||||
self.tray_icon.setToolTip("LTE Tool - 已连接")
|
||||
self.connection_status_action.setText("已连接")
|
||||
# 在状态栏显示连接指示器
|
||||
self.statusBar().setStyleSheet("QStatusBar { background-color: rgba(60, 179, 113, 30); }")
|
||||
self.setWindowIcon(self.running_icon) # 更新窗口图标
|
||||
else:
|
||||
# 使用默认图标表示未连接状态
|
||||
self.tray_icon.setIcon(self.default_icon)
|
||||
self.tray_icon.setToolTip("LTE Tool - 未连接")
|
||||
self.connection_status_action.setText("未连接")
|
||||
# 在状态栏显示未连接指示器
|
||||
self.statusBar().setStyleSheet("QStatusBar { background-color: rgba(100, 149, 237, 30); }")
|
||||
self.setWindowIcon(self.default_icon) # 更新窗口图标
|
||||
except Exception as e:
|
||||
# 发生错误时使用错误图标
|
||||
print(f"更新连接状态出错: {str(e)}")
|
||||
try:
|
||||
self.tray_icon.setIcon(self.error_icon)
|
||||
self.tray_icon.setToolTip("LTE Tool - 连接错误")
|
||||
self.connection_status_action.setText("连接错误")
|
||||
self.statusBar().setStyleSheet("QStatusBar { background-color: rgba(220, 20, 60, 30); }")
|
||||
self.setWindowIcon(self.error_icon) # 更新窗口图标
|
||||
except:
|
||||
print("无法设置错误图标状态")
|
||||
|
||||
def show_error_status(self, error_message):
|
||||
"""显示错误状态并更新图标"""
|
||||
try:
|
||||
self.statusBar().showMessage(f"错误: {error_message}", 5000)
|
||||
self.tray_icon.setIcon(self.error_icon)
|
||||
self.tray_icon.setToolTip(f"LTE Tool - 错误: {error_message[:30]}")
|
||||
self.setWindowIcon(self.error_icon)
|
||||
|
||||
# 显示托盘通知
|
||||
self.tray_icon.showMessage(
|
||||
"LTE Tool 错误",
|
||||
error_message,
|
||||
QSystemTrayIcon.Warning,
|
||||
3000
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"显示错误状态时出错: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
# 不再设置QuitOnLastWindowClosed为False,让应用在窗口关闭时可以正常退出
|
||||
# app.setQuitOnLastWindowClosed(False)
|
||||
window = LTEToolApp()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,787 @@
|
||||
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QLineEdit, QTextEdit, QGroupBox, QTabWidget, QListWidget,
|
||||
QListWidgetItem, QMessageBox, QSplitter, QComboBox,
|
||||
QTableWidget, QTableWidgetItem, QHeaderView, QSizePolicy)
|
||||
from PyQt5.QtCore import Qt, pyqtSlot, QDateTime, QSize
|
||||
import time
|
||||
|
||||
class PhoneSmsTab(QWidget):
|
||||
def __init__(self, lte_manager, database, sound_manager):
|
||||
super().__init__()
|
||||
self.lte_manager = lte_manager
|
||||
self.database = database
|
||||
self.sound_manager = sound_manager
|
||||
|
||||
# Connect signals
|
||||
self.lte_manager.call_received.connect(self.on_call_received)
|
||||
self.lte_manager.call_ended.connect(self.on_call_ended)
|
||||
self.lte_manager.sms_received.connect(self.on_sms_received)
|
||||
self.lte_manager.dtmf_received.connect(self.on_dtmf_received)
|
||||
self.lte_manager.status_changed.connect(self.on_status_changed)
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
# Main layout
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
# Create inner tab widget for phone and SMS
|
||||
inner_tab_widget = QTabWidget()
|
||||
main_layout.addWidget(inner_tab_widget)
|
||||
|
||||
# Phone tab
|
||||
phone_widget = QWidget()
|
||||
phone_layout = QVBoxLayout(phone_widget)
|
||||
|
||||
# Create a splitter for phone controls and call history
|
||||
phone_splitter = QSplitter(Qt.Vertical)
|
||||
phone_layout.addWidget(phone_splitter)
|
||||
|
||||
# Top widget for phone controls
|
||||
phone_top_widget = QWidget()
|
||||
phone_top_layout = QVBoxLayout(phone_top_widget)
|
||||
|
||||
# Phone controls
|
||||
phone_group = QGroupBox("电话控制")
|
||||
phone_controls_layout = QVBoxLayout()
|
||||
|
||||
# 通话状态显示
|
||||
self.call_status_display = QLabel("通话状态: 无通话")
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #f0f0f0; border-radius: 3px;")
|
||||
self.call_status_display.setAlignment(Qt.AlignCenter)
|
||||
phone_controls_layout.addWidget(self.call_status_display)
|
||||
|
||||
# Number input
|
||||
number_layout = QHBoxLayout()
|
||||
number_layout.addWidget(QLabel("电话号码:"))
|
||||
self.phone_number_input = QLineEdit()
|
||||
self.phone_number_input.setPlaceholderText("输入电话号码")
|
||||
number_layout.addWidget(self.phone_number_input)
|
||||
phone_controls_layout.addLayout(number_layout)
|
||||
|
||||
# Call buttons
|
||||
call_buttons_layout = QHBoxLayout()
|
||||
self.call_button = QPushButton("拨号")
|
||||
self.call_button.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 6px; } QPushButton:disabled { background-color: #cccccc; }")
|
||||
self.call_button.clicked.connect(self.on_call_button_clicked)
|
||||
call_buttons_layout.addWidget(self.call_button)
|
||||
|
||||
self.answer_button = QPushButton("接听")
|
||||
self.answer_button.setStyleSheet("QPushButton { background-color: #2196F3; color: white; padding: 6px; } QPushButton:disabled { background-color: #cccccc; }")
|
||||
self.answer_button.clicked.connect(self.on_answer_button_clicked)
|
||||
self.answer_button.setEnabled(False)
|
||||
call_buttons_layout.addWidget(self.answer_button)
|
||||
|
||||
self.hangup_button = QPushButton("挂断")
|
||||
self.hangup_button.setStyleSheet("QPushButton { background-color: #f44336; color: white; padding: 6px; } QPushButton:disabled { background-color: #cccccc; }")
|
||||
self.hangup_button.clicked.connect(self.on_hangup_button_clicked)
|
||||
self.hangup_button.setEnabled(False)
|
||||
call_buttons_layout.addWidget(self.hangup_button)
|
||||
|
||||
phone_controls_layout.addLayout(call_buttons_layout)
|
||||
phone_group.setLayout(phone_controls_layout)
|
||||
phone_top_layout.addWidget(phone_group)
|
||||
|
||||
# DTMF tones received
|
||||
dtmf_group = QGroupBox("DTMF拨号音")
|
||||
dtmf_layout = QVBoxLayout()
|
||||
self.dtmf_display = QLineEdit()
|
||||
self.dtmf_display.setReadOnly(True)
|
||||
dtmf_layout.addWidget(self.dtmf_display)
|
||||
|
||||
# 添加DTMF拨号键盘
|
||||
dtmf_keyboard_layout = QVBoxLayout()
|
||||
|
||||
# 添加拨号键盘行
|
||||
dtmf_rows = [
|
||||
['1', '2', '3'],
|
||||
['4', '5', '6'],
|
||||
['7', '8', '9'],
|
||||
['*', '0', '#']
|
||||
]
|
||||
|
||||
for row in dtmf_rows:
|
||||
row_layout = QHBoxLayout()
|
||||
for key in row:
|
||||
btn = QPushButton(key)
|
||||
btn.setStyleSheet("QPushButton { font-size: 14px; padding: 10px; }")
|
||||
btn.clicked.connect(lambda checked, k=key: self.send_dtmf(k))
|
||||
row_layout.addWidget(btn)
|
||||
dtmf_keyboard_layout.addLayout(row_layout)
|
||||
|
||||
dtmf_layout.addLayout(dtmf_keyboard_layout)
|
||||
dtmf_group.setLayout(dtmf_layout)
|
||||
phone_top_layout.addWidget(dtmf_group)
|
||||
|
||||
# Add phone top widget to splitter
|
||||
phone_splitter.addWidget(phone_top_widget)
|
||||
|
||||
# Bottom widget for call history
|
||||
phone_bottom_widget = QWidget()
|
||||
phone_bottom_layout = QVBoxLayout(phone_bottom_widget)
|
||||
|
||||
# Call log
|
||||
call_log_group = QGroupBox("Call History")
|
||||
call_log_layout = QVBoxLayout()
|
||||
|
||||
# Call log controls
|
||||
call_log_controls = QHBoxLayout()
|
||||
self.refresh_call_log_button = QPushButton("Refresh")
|
||||
self.refresh_call_log_button.clicked.connect(self.refresh_call_log)
|
||||
call_log_controls.addWidget(self.refresh_call_log_button)
|
||||
|
||||
self.clear_call_log_button = QPushButton("Clear Selected")
|
||||
self.clear_call_log_button.clicked.connect(self.clear_selected_call)
|
||||
call_log_controls.addWidget(self.clear_call_log_button)
|
||||
|
||||
call_log_layout.addLayout(call_log_controls)
|
||||
|
||||
# Call log table
|
||||
self.call_log_table = QTableWidget()
|
||||
self.call_log_table.setColumnCount(4)
|
||||
self.call_log_table.setHorizontalHeaderLabels(["Time", "Number", "Type", "Duration"])
|
||||
self.call_log_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.call_log_table.setMinimumHeight(200) # Set minimum height
|
||||
call_log_layout.addWidget(self.call_log_table)
|
||||
|
||||
call_log_group.setLayout(call_log_layout)
|
||||
phone_bottom_layout.addWidget(call_log_group)
|
||||
|
||||
# Add phone bottom widget to splitter
|
||||
phone_splitter.addWidget(phone_bottom_widget)
|
||||
|
||||
# Set initial sizes for splitter
|
||||
phone_splitter.setSizes([200, 400])
|
||||
|
||||
# SMS tab
|
||||
sms_widget = QWidget()
|
||||
sms_layout = QVBoxLayout(sms_widget)
|
||||
|
||||
# Create a splitter for SMS tab
|
||||
sms_splitter = QSplitter(Qt.Vertical)
|
||||
sms_layout.addWidget(sms_splitter)
|
||||
|
||||
# Top widget for SMS sending
|
||||
sms_top_widget = QWidget()
|
||||
sms_top_layout = QVBoxLayout(sms_top_widget)
|
||||
|
||||
# SMS controls
|
||||
sms_group = QGroupBox("Send SMS")
|
||||
sms_controls_layout = QVBoxLayout()
|
||||
|
||||
# Number input
|
||||
sms_number_layout = QHBoxLayout()
|
||||
sms_number_layout.addWidget(QLabel("To:"))
|
||||
self.sms_number_input = QLineEdit()
|
||||
self.sms_number_input.setPlaceholderText("Enter recipient number")
|
||||
sms_number_layout.addWidget(self.sms_number_input)
|
||||
sms_controls_layout.addLayout(sms_number_layout)
|
||||
|
||||
# Message input
|
||||
sms_controls_layout.addWidget(QLabel("Message:"))
|
||||
self.sms_message_input = QTextEdit()
|
||||
self.sms_message_input.setPlaceholderText("Type your message here")
|
||||
self.sms_message_input.setMinimumHeight(100)
|
||||
sms_controls_layout.addWidget(self.sms_message_input)
|
||||
|
||||
# Send button
|
||||
self.send_sms_button = QPushButton("Send SMS")
|
||||
self.send_sms_button.clicked.connect(self.on_send_sms_button_clicked)
|
||||
sms_controls_layout.addWidget(self.send_sms_button)
|
||||
|
||||
sms_group.setLayout(sms_controls_layout)
|
||||
sms_top_layout.addWidget(sms_group)
|
||||
|
||||
# Add SMS top widget to splitter
|
||||
sms_splitter.addWidget(sms_top_widget)
|
||||
|
||||
# Middle widget for SMS inbox
|
||||
sms_middle_widget = QWidget()
|
||||
sms_middle_layout = QVBoxLayout(sms_middle_widget)
|
||||
|
||||
# SMS inbox
|
||||
sms_inbox_group = QGroupBox("SMS Messages")
|
||||
sms_inbox_layout = QVBoxLayout()
|
||||
|
||||
# SMS list and controls
|
||||
sms_list_controls = QHBoxLayout()
|
||||
self.sms_type_combo = QComboBox()
|
||||
self.sms_type_combo.addItems(["All", "Unread", "Read", "Sent", "Unsent"])
|
||||
sms_list_controls.addWidget(QLabel("Show:"))
|
||||
sms_list_controls.addWidget(self.sms_type_combo)
|
||||
|
||||
self.refresh_sms_button = QPushButton("Refresh")
|
||||
self.refresh_sms_button.clicked.connect(self.refresh_sms_list)
|
||||
sms_list_controls.addWidget(self.refresh_sms_button)
|
||||
|
||||
self.delete_sms_button = QPushButton("Delete Selected")
|
||||
self.delete_sms_button.clicked.connect(self.delete_selected_sms)
|
||||
sms_list_controls.addWidget(self.delete_sms_button)
|
||||
|
||||
sms_inbox_layout.addLayout(sms_list_controls)
|
||||
|
||||
# Create a horizontal splitter for SMS list and content
|
||||
sms_content_splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# SMS list
|
||||
self.sms_list = QListWidget()
|
||||
self.sms_list.itemClicked.connect(self.on_sms_item_clicked)
|
||||
self.sms_list.setMinimumHeight(150) # Set minimum height
|
||||
sms_content_splitter.addWidget(self.sms_list)
|
||||
|
||||
# SMS content
|
||||
sms_content_widget = QWidget()
|
||||
sms_content_layout = QVBoxLayout(sms_content_widget)
|
||||
sms_content_layout.addWidget(QLabel("Message Content:"))
|
||||
self.sms_content = QTextEdit()
|
||||
self.sms_content.setReadOnly(True)
|
||||
sms_content_layout.addWidget(self.sms_content)
|
||||
sms_content_splitter.addWidget(sms_content_widget)
|
||||
|
||||
# Set initial sizes for content splitter
|
||||
sms_content_splitter.setSizes([300, 300])
|
||||
|
||||
sms_inbox_layout.addWidget(sms_content_splitter)
|
||||
sms_inbox_group.setLayout(sms_inbox_layout)
|
||||
sms_middle_layout.addWidget(sms_inbox_group)
|
||||
|
||||
# Add SMS middle widget to splitter
|
||||
sms_splitter.addWidget(sms_middle_widget)
|
||||
|
||||
# Bottom widget for SMS history
|
||||
sms_bottom_widget = QWidget()
|
||||
sms_bottom_layout = QVBoxLayout(sms_bottom_widget)
|
||||
|
||||
# SMS history
|
||||
sms_history_group = QGroupBox("SMS History")
|
||||
sms_history_layout = QVBoxLayout()
|
||||
|
||||
# SMS history controls
|
||||
sms_history_controls = QHBoxLayout()
|
||||
self.refresh_sms_history_button = QPushButton("Refresh History")
|
||||
self.refresh_sms_history_button.clicked.connect(self.refresh_sms_history)
|
||||
sms_history_controls.addWidget(self.refresh_sms_history_button)
|
||||
|
||||
self.clear_sms_history_button = QPushButton("Clear Selected")
|
||||
self.clear_sms_history_button.clicked.connect(self.clear_selected_sms_history)
|
||||
sms_history_controls.addWidget(self.clear_sms_history_button)
|
||||
|
||||
sms_history_layout.addLayout(sms_history_controls)
|
||||
|
||||
# SMS history table
|
||||
self.sms_history_table = QTableWidget()
|
||||
self.sms_history_table.setColumnCount(4)
|
||||
self.sms_history_table.setHorizontalHeaderLabels(["Time", "Number", "Type", "Message"])
|
||||
self.sms_history_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.sms_history_table.itemClicked.connect(self.on_sms_history_item_clicked)
|
||||
self.sms_history_table.setMinimumHeight(150) # Set minimum height
|
||||
sms_history_layout.addWidget(self.sms_history_table)
|
||||
|
||||
sms_history_group.setLayout(sms_history_layout)
|
||||
sms_bottom_layout.addWidget(sms_history_group)
|
||||
|
||||
# Add SMS bottom widget to splitter
|
||||
sms_splitter.addWidget(sms_bottom_widget)
|
||||
|
||||
# Set initial sizes for SMS splitter
|
||||
sms_splitter.setSizes([200, 300, 300])
|
||||
|
||||
# Add tabs to inner tab widget
|
||||
inner_tab_widget.addTab(phone_widget, "Phone")
|
||||
inner_tab_widget.addTab(sms_widget, "SMS")
|
||||
|
||||
# Status display
|
||||
self.status_display = QTextEdit()
|
||||
self.status_display.setReadOnly(True)
|
||||
self.status_display.setMaximumHeight(100)
|
||||
main_layout.addWidget(QLabel("Status:"))
|
||||
main_layout.addWidget(self.status_display)
|
||||
|
||||
# Load initial data
|
||||
self.refresh_call_log()
|
||||
self.refresh_sms_history()
|
||||
|
||||
def update_call_ui_state(self, in_call=False):
|
||||
"""根据当前通话状态更新UI"""
|
||||
try:
|
||||
# 获取最新通话状态
|
||||
if self.lte_manager.is_connected():
|
||||
call_state = self.lte_manager.get_call_state_text()
|
||||
self.call_status_display.setText(f"通话状态: {call_state}")
|
||||
|
||||
# 获取当前通话
|
||||
calls = self.lte_manager.get_call_status()
|
||||
|
||||
if calls:
|
||||
# 有通话存在
|
||||
call = calls[0]
|
||||
stat = call.get('stat', -1)
|
||||
direction = call.get('dir', 0)
|
||||
|
||||
# 根据通话状态更新按钮状态
|
||||
if stat == 4 and direction == 1: # 来电中(MT)
|
||||
# 来电振铃中
|
||||
self.call_button.setEnabled(False)
|
||||
self.answer_button.setEnabled(True)
|
||||
self.hangup_button.setEnabled(True)
|
||||
|
||||
# 设置不同的样式以提示用户
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #FFF9C4; color: #E65100; border-radius: 3px;")
|
||||
elif stat in [0, 1, 2, 3]: # 活动、保持、拨号中、振铃中
|
||||
# 通话活动中
|
||||
self.call_button.setEnabled(False)
|
||||
self.answer_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(True)
|
||||
|
||||
if stat == 0: # 活动通话
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #C8E6C9; color: #2E7D32; border-radius: 3px;")
|
||||
elif stat == 1: # 保持通话
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #BBDEFB; color: #1565C0; border-radius: 3px;")
|
||||
else: # 拨号中、振铃中
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #E1BEE7; color: #6A1B9A; border-radius: 3px;")
|
||||
else:
|
||||
# 未知状态
|
||||
self.call_button.setEnabled(True)
|
||||
self.answer_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(False)
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #f0f0f0; border-radius: 3px;")
|
||||
else:
|
||||
# 无通话
|
||||
self.call_button.setEnabled(True)
|
||||
self.answer_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(False)
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #f0f0f0; border-radius: 3px;")
|
||||
else:
|
||||
# 未连接
|
||||
self.call_status_display.setText("通话状态: 未连接")
|
||||
self.call_button.setEnabled(False)
|
||||
self.answer_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(False)
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #FFCCBC; color: #BF360C; border-radius: 3px;")
|
||||
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 更新通话UI状态出错: {str(e)}")
|
||||
# 出错时重置为安全状态
|
||||
self.call_button.setEnabled(True)
|
||||
self.answer_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(False)
|
||||
|
||||
def send_dtmf(self, tone):
|
||||
"""发送DTMF拨号音"""
|
||||
if not self.lte_manager.is_connected() or not self.lte_manager.is_call_connected():
|
||||
# 只有在通话活动时才能发送DTMF音
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 无法发送DTMF: 当前无活动通话")
|
||||
self.sound_manager.play_error()
|
||||
QMessageBox.warning(self, "DTMF错误", "只有在通话接通时才能发送拨号音")
|
||||
return
|
||||
|
||||
try:
|
||||
# 发送AT+VTS命令发送DTMF音
|
||||
response = self.lte_manager.send_at_command(f"AT+VTS={tone}")
|
||||
if "OK" in response:
|
||||
# 发送成功,更新DTMF显示
|
||||
current_text = self.dtmf_display.text()
|
||||
self.dtmf_display.setText(current_text + tone)
|
||||
self.sound_manager.play_dtmf() # 播放提示音
|
||||
else:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送DTMF音失败: {response}")
|
||||
self.sound_manager.play_error()
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 发送DTMF音出错: {str(e)}")
|
||||
self.sound_manager.play_error()
|
||||
|
||||
def on_call_button_clicked(self):
|
||||
"""处理拨号按钮点击"""
|
||||
number = self.phone_number_input.text().strip()
|
||||
if not number:
|
||||
return
|
||||
|
||||
# 添加到拨号记录,无论成功与否
|
||||
self.call_log_history.append(f"拨打电话: {number}")
|
||||
self.update_call_log_display()
|
||||
|
||||
# 尝试拨打电话
|
||||
if self.lte_manager.make_call(number):
|
||||
self.update_call_ui_state(True) # 拨号中,更新UI状态
|
||||
|
||||
# 触发主窗口进行通话状态检查
|
||||
if hasattr(self.parent(), 'should_check_call_status'):
|
||||
self.parent().should_check_call_status = True
|
||||
self.parent().call_check_count = 0
|
||||
if not self.parent().call_status_timer.isActive():
|
||||
self.parent().call_status_timer.start(1000)
|
||||
|
||||
# 记录到数据库
|
||||
self.database.add_call(number, "outgoing")
|
||||
else:
|
||||
# 拨号失败,提示用户
|
||||
self.call_status_display.setText("拨号失败")
|
||||
self.call_log_history.append(f"拨号失败: {number}")
|
||||
self.update_call_log_display()
|
||||
|
||||
def on_answer_button_clicked(self):
|
||||
"""处理接听按钮点击"""
|
||||
# 先停止所有铃声,确保不会有铃声继续播放
|
||||
try:
|
||||
self.parent().sound_manager.stop_ringtone()
|
||||
self.parent().sound_manager.stop_incoming_call()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 尝试接听电话
|
||||
if self.lte_manager.answer_call():
|
||||
self.call_log_history.append("已接听来电")
|
||||
self.update_call_log_display()
|
||||
self.update_call_ui_state(True) # 通话中,更新UI状态
|
||||
|
||||
# 触发主窗口进行通话状态检查
|
||||
if hasattr(self.parent(), 'should_check_call_status'):
|
||||
self.parent().should_check_call_status = True
|
||||
self.parent().call_check_count = 0
|
||||
if not self.parent().call_status_timer.isActive():
|
||||
self.parent().call_status_timer.start(1000)
|
||||
|
||||
# 检查通话状态,确认通话是否实际连接
|
||||
time.sleep(0.5) # 给模块一点时间更新状态
|
||||
calls = self.lte_manager.get_call_status()
|
||||
call_connected = False
|
||||
|
||||
for call in calls:
|
||||
if call.get('stat') in [0, 1]: # 活动或保持状态
|
||||
call_connected = True
|
||||
self.call_status_display.setText(f"通话中: {call.get('number', '未知')}")
|
||||
break
|
||||
|
||||
if not call_connected:
|
||||
self.call_status_display.setText("接听失败或通话已结束")
|
||||
else:
|
||||
self.call_log_history.append("接听失败")
|
||||
self.update_call_log_display()
|
||||
self.call_status_display.setText("接听失败")
|
||||
|
||||
def on_hangup_button_clicked(self):
|
||||
"""处理挂断按钮点击"""
|
||||
if self.lte_manager.end_call():
|
||||
self.call_log_history.append("通话结束")
|
||||
self.update_call_log_display()
|
||||
self.update_call_ui_state(False) # 通话结束,更新UI状态
|
||||
|
||||
# 触发主窗口进行通话状态检查
|
||||
if hasattr(self.parent(), 'should_check_call_status'):
|
||||
self.parent().should_check_call_status = True
|
||||
self.parent().call_check_count = 0
|
||||
if not self.parent().call_status_timer.isActive():
|
||||
self.parent().call_status_timer.start(1000)
|
||||
else:
|
||||
self.call_log_history.append("挂断失败")
|
||||
self.update_call_log_display()
|
||||
|
||||
def on_send_sms_button_clicked(self):
|
||||
"""Handle send SMS button click"""
|
||||
number = self.sms_number_input.text().strip()
|
||||
message = self.sms_message_input.toPlainText().strip()
|
||||
|
||||
if not number:
|
||||
QMessageBox.warning(self, "Input Error", "Please enter a recipient number")
|
||||
return
|
||||
|
||||
if not message:
|
||||
QMessageBox.warning(self, "Input Error", "Please enter a message")
|
||||
return
|
||||
|
||||
if self.lte_manager.send_sms(number, message):
|
||||
self.sms_message_input.clear()
|
||||
self.add_status_message(f"SMS sent to {number}")
|
||||
|
||||
# Play success sound
|
||||
self.sound_manager.play_success()
|
||||
|
||||
# Add to database
|
||||
self.database.add_sms(number, message, "outgoing", "sent")
|
||||
|
||||
# Refresh SMS list and history
|
||||
self.refresh_sms_list()
|
||||
self.refresh_sms_history()
|
||||
else:
|
||||
QMessageBox.warning(self, "SMS Error", "Failed to send SMS")
|
||||
|
||||
# Play error sound
|
||||
self.sound_manager.play_error()
|
||||
|
||||
# Add to database as failed
|
||||
self.database.add_sms(number, message, "outgoing", "failed")
|
||||
|
||||
def on_call_received(self, number):
|
||||
"""Handle incoming call"""
|
||||
self.answer_button.setEnabled(True)
|
||||
self.call_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(True)
|
||||
self.add_to_call_log(f"Incoming call from {number}")
|
||||
|
||||
# Play ringtone
|
||||
self.sound_manager.play_ringtone()
|
||||
|
||||
# Note: Call recording in database is now handled in the main window
|
||||
# to ensure it's recorded exactly once when the notification appears
|
||||
|
||||
def on_call_ended(self, duration):
|
||||
"""处理通话结束事件"""
|
||||
self.sound_manager.play_call_end()
|
||||
self.call_button.setEnabled(True)
|
||||
self.answer_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(False)
|
||||
self.add_to_call_log(f"通话结束,持续时间: {duration}")
|
||||
|
||||
# 停止所有铃声
|
||||
self._stop_all_ringtones()
|
||||
|
||||
# 清除DTMF显示
|
||||
self.dtmf_display.clear()
|
||||
|
||||
# 更新通话状态
|
||||
self.call_status_display.setText("通话状态: 无通话")
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #f0f0f0; border-radius: 3px;")
|
||||
|
||||
# 刷新通话记录
|
||||
self.refresh_call_log()
|
||||
|
||||
def _stop_all_ringtones(self):
|
||||
"""停止所有铃声,确保彻底停止"""
|
||||
try:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 停止所有铃声")
|
||||
self.sound_manager.stop_ringtone()
|
||||
self.sound_manager.stop_incoming_call()
|
||||
|
||||
# 额外尝试停止系统声音
|
||||
try:
|
||||
import winsound
|
||||
winsound.PlaySound(None, winsound.SND_PURGE)
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 停止铃声出错: {str(e)}")
|
||||
|
||||
def on_sms_received(self, sender, timestamp, message):
|
||||
"""Handle SMS received"""
|
||||
self.add_status_message(f"SMS received from {sender}")
|
||||
|
||||
# Play message received sound - play three beeps
|
||||
self.sound_manager.play_message_received()
|
||||
self.sound_manager.play_message_received()
|
||||
self.sound_manager.play_message_received()
|
||||
|
||||
# Add to database
|
||||
self.database.add_sms(sender, message, "incoming", "received")
|
||||
|
||||
# Refresh SMS list and history
|
||||
self.refresh_sms_list()
|
||||
self.refresh_sms_history()
|
||||
|
||||
# Update the SMS content display directly
|
||||
self.sms_content.setText(f"From: {sender}\nTime: {timestamp}\n\n{message}")
|
||||
|
||||
# Show a message box to alert the user
|
||||
QMessageBox.information(self, "New SMS", f"New message from {sender}\n\n{message[:100]}" + ("..." if len(message) > 100 else ""))
|
||||
|
||||
def on_dtmf_received(self, tone):
|
||||
"""Handle DTMF tone received"""
|
||||
current_text = self.dtmf_display.text()
|
||||
self.dtmf_display.setText(current_text + tone)
|
||||
|
||||
def on_status_changed(self, status):
|
||||
"""Handle status change"""
|
||||
self.add_status_message(status)
|
||||
|
||||
def add_to_call_log(self, message):
|
||||
"""Add message to status display"""
|
||||
timestamp = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
|
||||
self.status_display.append(f"{timestamp} - {message}")
|
||||
self.status_display.ensureCursorVisible()
|
||||
|
||||
def add_status_message(self, message):
|
||||
"""Add message to status display"""
|
||||
timestamp = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
|
||||
self.status_display.append(f"{timestamp} - {message}")
|
||||
self.status_display.ensureCursorVisible()
|
||||
|
||||
def refresh_sms_list(self):
|
||||
"""Refresh SMS list from module"""
|
||||
if not self.lte_manager.is_connected():
|
||||
return
|
||||
|
||||
self.sms_list.clear()
|
||||
self.sms_content.clear()
|
||||
|
||||
# Get SMS type filter
|
||||
sms_type = self.sms_type_combo.currentText()
|
||||
if sms_type == "All":
|
||||
status = "ALL"
|
||||
elif sms_type == "Unread":
|
||||
status = "REC UNREAD"
|
||||
elif sms_type == "Read":
|
||||
status = "REC READ"
|
||||
elif sms_type == "Sent":
|
||||
status = "STO SENT"
|
||||
elif sms_type == "Unsent":
|
||||
status = "STO UNSENT"
|
||||
|
||||
# Get SMS list
|
||||
messages = self.lte_manager.get_sms_list(status)
|
||||
|
||||
# Add messages to list
|
||||
for msg in messages:
|
||||
item = QListWidgetItem(f"{msg['index']} - From: {msg['sender']} - {msg['timestamp']}")
|
||||
item.setData(Qt.UserRole, msg)
|
||||
self.sms_list.addItem(item)
|
||||
|
||||
# If no messages from module, show a message
|
||||
if self.sms_list.count() == 0:
|
||||
self.add_status_message("No messages found on the module. Check SMS history tab for stored messages.")
|
||||
|
||||
# Try to get messages from database to show in the content area
|
||||
db_messages = self.database.get_sms_history(limit=1)
|
||||
if db_messages:
|
||||
# Format: id, phone_number, message, sms_type, timestamp, status
|
||||
_, _, message, _, _, _ = db_messages[0]
|
||||
self.sms_content.setText("Last message from database:\n\n" + message)
|
||||
|
||||
def on_sms_item_clicked(self, item):
|
||||
"""Handle SMS item click"""
|
||||
msg = item.data(Qt.UserRole)
|
||||
if msg:
|
||||
self.sms_content.setText(msg['content'])
|
||||
|
||||
def delete_selected_sms(self):
|
||||
"""Delete selected SMS from module"""
|
||||
selected_items = self.sms_list.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "Selection Error", "Please select an SMS to delete")
|
||||
return
|
||||
|
||||
for item in selected_items:
|
||||
msg = item.data(Qt.UserRole)
|
||||
if msg:
|
||||
if self.lte_manager.delete_sms(msg['index']):
|
||||
self.add_status_message(f"Deleted SMS at index {msg['index']}")
|
||||
else:
|
||||
self.add_status_message(f"Failed to delete SMS at index {msg['index']}")
|
||||
|
||||
self.refresh_sms_list()
|
||||
|
||||
def refresh_call_log(self):
|
||||
"""Refresh call log from database"""
|
||||
# Get call history from database
|
||||
calls = self.database.get_call_history()
|
||||
|
||||
# Clear table
|
||||
self.call_log_table.setRowCount(0)
|
||||
|
||||
# Add calls to table
|
||||
for call in calls:
|
||||
row = self.call_log_table.rowCount()
|
||||
self.call_log_table.insertRow(row)
|
||||
|
||||
# Format: id, phone_number, call_type, duration, timestamp, notes
|
||||
call_id, phone_number, call_type, duration, timestamp, notes = call
|
||||
|
||||
# Format duration
|
||||
if duration:
|
||||
duration_str = f"{duration}s"
|
||||
else:
|
||||
duration_str = ""
|
||||
|
||||
# Add items to row
|
||||
self.call_log_table.setItem(row, 0, QTableWidgetItem(timestamp))
|
||||
self.call_log_table.setItem(row, 1, QTableWidgetItem(phone_number))
|
||||
self.call_log_table.setItem(row, 2, QTableWidgetItem(call_type))
|
||||
self.call_log_table.setItem(row, 3, QTableWidgetItem(duration_str))
|
||||
|
||||
# Store call ID in first column
|
||||
self.call_log_table.item(row, 0).setData(Qt.UserRole, call_id)
|
||||
|
||||
def clear_selected_call(self):
|
||||
"""Clear selected call from database"""
|
||||
selected_items = self.call_log_table.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "Selection Error", "Please select a call to delete")
|
||||
return
|
||||
|
||||
# Get unique rows
|
||||
rows = set()
|
||||
for item in selected_items:
|
||||
rows.add(item.row())
|
||||
|
||||
# Delete each selected call
|
||||
for row in rows:
|
||||
call_id = self.call_log_table.item(row, 0).data(Qt.UserRole)
|
||||
if self.database.delete_call(call_id):
|
||||
self.add_status_message(f"Deleted call record {call_id}")
|
||||
else:
|
||||
self.add_status_message(f"Failed to delete call record {call_id}")
|
||||
|
||||
# Refresh call log
|
||||
self.refresh_call_log()
|
||||
|
||||
def refresh_sms_history(self):
|
||||
"""Refresh SMS history from database"""
|
||||
# Get SMS history from database
|
||||
messages = self.database.get_sms_history()
|
||||
|
||||
# Clear table
|
||||
self.sms_history_table.setRowCount(0)
|
||||
|
||||
# Add messages to table
|
||||
for msg in messages:
|
||||
row = self.sms_history_table.rowCount()
|
||||
self.sms_history_table.insertRow(row)
|
||||
|
||||
# Format: id, phone_number, message, sms_type, timestamp, status
|
||||
sms_id, phone_number, message, sms_type, timestamp, status = msg
|
||||
|
||||
# Add items to row
|
||||
self.sms_history_table.setItem(row, 0, QTableWidgetItem(timestamp))
|
||||
self.sms_history_table.setItem(row, 1, QTableWidgetItem(phone_number))
|
||||
self.sms_history_table.setItem(row, 2, QTableWidgetItem(f"{sms_type} ({status})"))
|
||||
|
||||
# Truncate message if too long
|
||||
if len(message) > 50:
|
||||
display_message = message[:47] + "..."
|
||||
else:
|
||||
display_message = message
|
||||
|
||||
self.sms_history_table.setItem(row, 3, QTableWidgetItem(display_message))
|
||||
|
||||
# Store full message and SMS ID
|
||||
self.sms_history_table.item(row, 3).setData(Qt.UserRole, message)
|
||||
self.sms_history_table.item(row, 0).setData(Qt.UserRole, sms_id)
|
||||
|
||||
def on_sms_history_item_clicked(self, item):
|
||||
"""Handle SMS history item click"""
|
||||
# If clicked on message column, show full message
|
||||
if item.column() == 3:
|
||||
full_message = item.data(Qt.UserRole)
|
||||
if full_message:
|
||||
self.sms_content.setText(full_message)
|
||||
|
||||
def clear_selected_sms_history(self):
|
||||
"""Clear selected SMS from history database"""
|
||||
selected_items = self.sms_history_table.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "Selection Error", "Please select an SMS to delete")
|
||||
return
|
||||
|
||||
# Get unique rows
|
||||
rows = set()
|
||||
for item in selected_items:
|
||||
rows.add(item.row())
|
||||
|
||||
# Delete each selected SMS
|
||||
for row in rows:
|
||||
sms_id = self.sms_history_table.item(row, 0).data(Qt.UserRole)
|
||||
if self.database.delete_sms(sms_id):
|
||||
self.add_status_message(f"Deleted SMS record {sms_id}")
|
||||
else:
|
||||
self.add_status_message(f"Failed to delete SMS record {sms_id}")
|
||||
|
||||
# Refresh SMS history
|
||||
self.refresh_sms_history()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,362 @@
|
||||
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QLineEdit, QTextEdit, QGroupBox, QFormLayout, QComboBox,
|
||||
QGridLayout, QMessageBox, QSpinBox, QCheckBox)
|
||||
from PyQt5.QtCore import Qt, pyqtSlot, QTimer
|
||||
import serial.tools.list_ports
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
|
||||
class SettingsTab(QWidget):
|
||||
def __init__(self, lte_manager):
|
||||
"""初始化设置标签页"""
|
||||
super().__init__()
|
||||
self.lte_manager = lte_manager
|
||||
self.settings = {}
|
||||
self.settings_file = os.path.join(os.path.expanduser('~'), '.LTE', 'settings.json')
|
||||
|
||||
# 加载设置
|
||||
self.settings = {
|
||||
"at_port": "",
|
||||
"at_baudrate": "115200",
|
||||
"nmea_port": "None",
|
||||
"nmea_baudrate": "9600",
|
||||
"auto_connect": False
|
||||
}
|
||||
self.load_settings()
|
||||
|
||||
# 初始化UI
|
||||
self.init_ui()
|
||||
|
||||
# 注册状态变化的信号处理
|
||||
self.lte_manager.status_changed.connect(self.on_status_changed)
|
||||
|
||||
# 如果启用了自动连接,尝试连接
|
||||
if self.settings.get("auto_connect", False):
|
||||
QTimer.singleShot(1000, self.try_auto_connect)
|
||||
|
||||
def init_ui(self):
|
||||
"""初始化用户界面"""
|
||||
main_layout = QVBoxLayout()
|
||||
self.setLayout(main_layout)
|
||||
|
||||
# 端口设置组
|
||||
port_group = QGroupBox("端口设置")
|
||||
port_layout = QFormLayout()
|
||||
|
||||
# AT端口
|
||||
at_port_layout = QHBoxLayout()
|
||||
self.at_port_combo = QComboBox()
|
||||
at_port_layout.addWidget(self.at_port_combo)
|
||||
|
||||
self.refresh_ports_button = QPushButton("刷新")
|
||||
self.refresh_ports_button.clicked.connect(self.refresh_ports)
|
||||
at_port_layout.addWidget(self.refresh_ports_button)
|
||||
|
||||
port_layout.addRow("AT Port:", at_port_layout)
|
||||
|
||||
# AT波特率
|
||||
self.at_baudrate_combo = QComboBox()
|
||||
self.at_baudrate_combo.addItems(["9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"])
|
||||
self.at_baudrate_combo.setCurrentText(self.settings["at_baudrate"]) # 从保存的设置设置
|
||||
port_layout.addRow("AT Baudrate:", self.at_baudrate_combo)
|
||||
|
||||
# NMEA端口
|
||||
nmea_port_layout = QHBoxLayout()
|
||||
self.nmea_port_combo = QComboBox()
|
||||
self.nmea_port_combo.addItem("None")
|
||||
nmea_port_layout.addWidget(self.nmea_port_combo)
|
||||
|
||||
port_layout.addRow("NMEA Port:", nmea_port_layout)
|
||||
|
||||
# NMEA波特率
|
||||
self.nmea_baudrate_combo = QComboBox()
|
||||
self.nmea_baudrate_combo.addItems(["4800", "9600", "19200", "38400", "57600", "115200"])
|
||||
self.nmea_baudrate_combo.setCurrentText(self.settings["nmea_baudrate"]) # 从保存的设置设置
|
||||
port_layout.addRow("NMEA Baudrate:", self.nmea_baudrate_combo)
|
||||
|
||||
# 自动连接复选框
|
||||
self.auto_connect_checkbox = QCheckBox("自动连接")
|
||||
self.auto_connect_checkbox.setChecked(self.settings.get("auto_connect", False))
|
||||
self.auto_connect_checkbox.stateChanged.connect(self.on_auto_connect_changed)
|
||||
port_layout.addRow("启动选项:", self.auto_connect_checkbox)
|
||||
|
||||
# 刷新端口列表
|
||||
self.refresh_ports()
|
||||
|
||||
# 连接/断开按钮
|
||||
buttons_layout = QHBoxLayout()
|
||||
self.connect_button = QPushButton("连接")
|
||||
self.connect_button.clicked.connect(self.on_connect_button_clicked)
|
||||
buttons_layout.addWidget(self.connect_button)
|
||||
|
||||
self.disconnect_button = QPushButton("断开")
|
||||
self.disconnect_button.clicked.connect(self.on_disconnect_button_clicked)
|
||||
self.disconnect_button.setEnabled(False)
|
||||
buttons_layout.addWidget(self.disconnect_button)
|
||||
|
||||
port_layout.addRow("", buttons_layout)
|
||||
port_group.setLayout(port_layout)
|
||||
main_layout.addWidget(port_group)
|
||||
|
||||
# 模块信息组
|
||||
module_group = QGroupBox("模块信息")
|
||||
module_layout = QVBoxLayout()
|
||||
|
||||
# 添加信息文本显示区域
|
||||
self.info_text = QTextEdit()
|
||||
self.info_text.setReadOnly(True)
|
||||
self.info_text.setMinimumHeight(150)
|
||||
module_layout.addWidget(self.info_text)
|
||||
|
||||
# 刷新按钮
|
||||
self.refresh_info_button = QPushButton("刷新信息")
|
||||
self.refresh_info_button.clicked.connect(self.refresh_module_info)
|
||||
self.refresh_info_button.setEnabled(False)
|
||||
module_layout.addWidget(self.refresh_info_button)
|
||||
|
||||
module_group.setLayout(module_layout)
|
||||
main_layout.addWidget(module_group)
|
||||
|
||||
# AT命令控制台
|
||||
console_group = QGroupBox("AT命令控制台")
|
||||
console_layout = QVBoxLayout()
|
||||
|
||||
# 命令输入
|
||||
command_layout = QHBoxLayout()
|
||||
self.command_input = QLineEdit()
|
||||
self.command_input.setPlaceholderText("输入AT命令")
|
||||
command_layout.addWidget(self.command_input)
|
||||
|
||||
self.send_command_button = QPushButton("发送")
|
||||
self.send_command_button.clicked.connect(self.on_send_command_button_clicked)
|
||||
self.send_command_button.setEnabled(False)
|
||||
command_layout.addWidget(self.send_command_button)
|
||||
|
||||
console_layout.addLayout(command_layout)
|
||||
|
||||
# 响应显示
|
||||
console_layout.addWidget(QLabel("响应:"))
|
||||
self.response_display = QTextEdit()
|
||||
self.response_display.setReadOnly(True)
|
||||
console_layout.addWidget(self.response_display)
|
||||
|
||||
console_group.setLayout(console_layout)
|
||||
main_layout.addWidget(console_group)
|
||||
|
||||
# 状态显示
|
||||
self.status_display = QTextEdit()
|
||||
self.status_display.setReadOnly(True)
|
||||
self.status_display.setMaximumHeight(100)
|
||||
main_layout.addWidget(QLabel("状态:"))
|
||||
main_layout.addWidget(self.status_display)
|
||||
|
||||
def get_available_ports(self):
|
||||
"""Get list of available serial ports"""
|
||||
ports = []
|
||||
for port in serial.tools.list_ports.comports():
|
||||
ports.append(port.device)
|
||||
return ports
|
||||
|
||||
def refresh_ports(self):
|
||||
"""Refresh available serial ports"""
|
||||
self.at_port_combo.clear()
|
||||
self.nmea_port_combo.clear()
|
||||
self.nmea_port_combo.addItem("None")
|
||||
|
||||
ports = self.get_available_ports()
|
||||
for port in ports:
|
||||
self.at_port_combo.addItem(port)
|
||||
self.nmea_port_combo.addItem(port)
|
||||
|
||||
# Set saved ports if available
|
||||
if self.settings["at_port"] in ports:
|
||||
self.at_port_combo.setCurrentText(self.settings["at_port"])
|
||||
|
||||
if self.settings["nmea_port"] in ports or self.settings["nmea_port"] == "None":
|
||||
self.nmea_port_combo.setCurrentText(self.settings["nmea_port"])
|
||||
|
||||
def on_connect_button_clicked(self):
|
||||
"""Handle connect button click"""
|
||||
at_port = self.at_port_combo.currentText()
|
||||
at_baudrate = int(self.at_baudrate_combo.currentText())
|
||||
nmea_port = self.nmea_port_combo.currentText()
|
||||
if nmea_port == "None":
|
||||
nmea_port = ""
|
||||
nmea_baudrate = int(self.nmea_baudrate_combo.currentText())
|
||||
|
||||
# Save settings
|
||||
self.settings["at_port"] = at_port
|
||||
self.settings["at_baudrate"] = self.at_baudrate_combo.currentText()
|
||||
self.settings["nmea_port"] = nmea_port if nmea_port else "None"
|
||||
self.settings["nmea_baudrate"] = self.nmea_baudrate_combo.currentText()
|
||||
self.settings["auto_connect"] = self.auto_connect_checkbox.isChecked()
|
||||
self.save_settings()
|
||||
|
||||
if self.lte_manager.connect(at_port, at_baudrate, nmea_port, nmea_baudrate):
|
||||
self.connect_button.setEnabled(False)
|
||||
self.disconnect_button.setEnabled(True)
|
||||
self.refresh_info_button.setEnabled(True)
|
||||
self.send_command_button.setEnabled(True)
|
||||
self.add_status_message("Connected to LTE module")
|
||||
self.refresh_module_info()
|
||||
else:
|
||||
self.add_status_message("Failed to connect to LTE module")
|
||||
|
||||
def on_disconnect_button_clicked(self):
|
||||
"""Handle disconnect button click"""
|
||||
self.lte_manager.disconnect()
|
||||
self.connect_button.setEnabled(True)
|
||||
self.disconnect_button.setEnabled(False)
|
||||
self.refresh_info_button.setEnabled(False)
|
||||
self.send_command_button.setEnabled(False)
|
||||
self.add_status_message("Disconnected from LTE module")
|
||||
|
||||
# Reset module information
|
||||
self.info_text.clear()
|
||||
self.refresh_info_button.setEnabled(False)
|
||||
|
||||
def on_send_command_button_clicked(self):
|
||||
"""Handle send command button click"""
|
||||
command = self.command_input.text().strip()
|
||||
if not command:
|
||||
return
|
||||
|
||||
self.add_status_message(f"Sending command: {command}")
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
self.response_display.setText(response)
|
||||
self.command_input.clear()
|
||||
|
||||
def refresh_module_info(self):
|
||||
"""刷新模块信息显示"""
|
||||
if not self.lte_manager.is_connected():
|
||||
self.add_status_message("请先连接模块")
|
||||
return
|
||||
|
||||
try:
|
||||
self.info_text.clear()
|
||||
self.add_status_message("正在获取模块信息...")
|
||||
|
||||
# 获取模块信息
|
||||
module_info = self.lte_manager.get_module_info()
|
||||
if not module_info:
|
||||
self.add_status_message("获取模块信息失败")
|
||||
return
|
||||
|
||||
# 显示模块信息
|
||||
self.info_text.append("<b>模块信息:</b>")
|
||||
for key, value in module_info.items():
|
||||
if value: # 只显示有值的项目
|
||||
self.info_text.append(f"<b>{key}:</b> {value}")
|
||||
|
||||
# 获取运营商信息
|
||||
carrier_info = self.lte_manager.get_carrier_info()
|
||||
if carrier_info:
|
||||
if isinstance(carrier_info, tuple) and len(carrier_info) == 2:
|
||||
carrier, network = carrier_info
|
||||
self.info_text.append(f"<b>运营商:</b> {carrier}")
|
||||
self.info_text.append(f"<b>网络类型:</b> {network}")
|
||||
else:
|
||||
self.info_text.append(f"<b>运营商:</b> {carrier_info}")
|
||||
|
||||
# 获取电话号码
|
||||
phone_number = self.lte_manager.get_phone_number()
|
||||
if phone_number:
|
||||
self.info_text.append(f"<b>电话号码:</b> {phone_number}")
|
||||
|
||||
# 获取信号强度
|
||||
signal_info = self.lte_manager.get_signal_strength()
|
||||
if signal_info:
|
||||
if isinstance(signal_info, tuple) and len(signal_info) == 2:
|
||||
signal_text, signal_desc = signal_info
|
||||
self.info_text.append(f"<b>信号强度:</b> {signal_text} ({signal_desc})")
|
||||
else:
|
||||
self.info_text.append(f"<b>信号强度:</b> {signal_info}")
|
||||
|
||||
# 获取网络信息
|
||||
network_info = self.lte_manager.get_network_info()
|
||||
if network_info:
|
||||
self.info_text.append("<b>网络信息:</b>")
|
||||
for key, value in network_info.items():
|
||||
if value: # 只显示有值的项目
|
||||
self.info_text.append(f"<b>{key}:</b> {value}")
|
||||
|
||||
# 添加时间戳
|
||||
self.info_text.append(f"<i>更新时间: {time.strftime('%Y-%m-%d %H:%M:%S')}</i>")
|
||||
self.add_status_message("模块信息已更新")
|
||||
|
||||
except Exception as e:
|
||||
self.add_status_message(f"刷新模块信息出错: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def on_status_changed(self, status):
|
||||
"""Handle status change"""
|
||||
self.add_status_message(status)
|
||||
|
||||
def add_status_message(self, message):
|
||||
"""Add message to status display"""
|
||||
self.status_display.append(message)
|
||||
self.status_display.ensureCursorVisible()
|
||||
|
||||
def on_auto_connect_changed(self, state):
|
||||
"""Handle auto connect checkbox state change"""
|
||||
self.settings["auto_connect"] = bool(state)
|
||||
self.save_settings()
|
||||
|
||||
def try_auto_connect(self):
|
||||
"""Try to automatically connect using saved settings"""
|
||||
if not self.settings.get("auto_connect", False):
|
||||
return False
|
||||
|
||||
at_port = self.settings.get("at_port", "")
|
||||
if not at_port:
|
||||
self.add_status_message("Auto-connect: No saved port")
|
||||
return False
|
||||
|
||||
# Check if the saved port is available
|
||||
available_ports = self.get_available_ports()
|
||||
if at_port not in available_ports:
|
||||
self.add_status_message(f"Auto-connect: Port {at_port} not available")
|
||||
return False
|
||||
|
||||
# Get other settings
|
||||
at_baudrate = int(self.settings.get("at_baudrate", "115200"))
|
||||
nmea_port = self.settings.get("nmea_port", "None")
|
||||
if nmea_port == "None":
|
||||
nmea_port = ""
|
||||
nmea_baudrate = int(self.settings.get("nmea_baudrate", "9600"))
|
||||
|
||||
# Try to connect
|
||||
self.add_status_message(f"Auto-connect: Trying to connect to {at_port}")
|
||||
if self.lte_manager.connect(at_port, at_baudrate, nmea_port, nmea_baudrate):
|
||||
self.connect_button.setEnabled(False)
|
||||
self.disconnect_button.setEnabled(True)
|
||||
self.refresh_info_button.setEnabled(True)
|
||||
self.send_command_button.setEnabled(True)
|
||||
self.add_status_message("Auto-connect: Connected to LTE module")
|
||||
self.refresh_module_info()
|
||||
return True
|
||||
else:
|
||||
self.add_status_message("Auto-connect: Failed to connect")
|
||||
return False
|
||||
|
||||
def load_settings(self):
|
||||
"""Load settings from file"""
|
||||
try:
|
||||
if os.path.exists(self.settings_file):
|
||||
with open(self.settings_file, 'r') as f:
|
||||
loaded_settings = json.load(f)
|
||||
# Update settings with loaded values
|
||||
for key, value in loaded_settings.items():
|
||||
self.settings[key] = value
|
||||
except Exception as e:
|
||||
print(f"Error loading settings: {str(e)}")
|
||||
|
||||
def save_settings(self):
|
||||
"""Save settings to file"""
|
||||
try:
|
||||
with open(self.settings_file, 'w') as f:
|
||||
json.dump(self.settings, f, indent=4)
|
||||
except Exception as e:
|
||||
print(f"Error saving settings: {str(e)}")
|
||||
@@ -0,0 +1,181 @@
|
||||
import binascii
|
||||
|
||||
def text_to_ucs2(text):
|
||||
"""Convert text to UCS2 (UTF-16BE) hex string for SMS sending"""
|
||||
try:
|
||||
# Encode text to UTF-16BE bytes
|
||||
utf16be_bytes = text.encode('utf-16be')
|
||||
|
||||
# Convert bytes to hex string
|
||||
hex_str = binascii.hexlify(utf16be_bytes).decode('ascii').upper()
|
||||
|
||||
return hex_str
|
||||
except Exception as e:
|
||||
print(f"UCS2 encoding error: {str(e)}")
|
||||
return None
|
||||
|
||||
def ucs2_to_text(hex_str):
|
||||
"""Convert UCS2 (UTF-16BE) hex string to text for SMS display"""
|
||||
try:
|
||||
# Remove spaces if any
|
||||
hex_str = hex_str.replace(" ", "")
|
||||
|
||||
# Make sure we have a valid hex string
|
||||
if not all(c in "0123456789ABCDEFabcdef" for c in hex_str):
|
||||
return hex_str # Not a hex string, return as is
|
||||
|
||||
# Make sure the length is even (each character is 2 bytes in UCS2)
|
||||
if len(hex_str) % 2 != 0:
|
||||
hex_str = hex_str + "0" # Pad with zero if needed
|
||||
|
||||
# 针对特定格式长短信的处理(以62117ED94F6053D14E86957F6587672C开头)
|
||||
if hex_str.startswith("62117ED94F6053D14E86957F6587672C"):
|
||||
# 这是一种特定格式的长短信,尝试提取关键信息
|
||||
# 通常格式是:固定标记 + "003A"(冒号) + URL内容
|
||||
parts = hex_str.split("003A", 1)
|
||||
if len(parts) > 1 and parts[1]:
|
||||
try:
|
||||
# 提取并解码URL部分
|
||||
url_hex = "003A" + parts[1] # 加回冒号
|
||||
url_bytes = binascii.unhexlify(url_hex)
|
||||
url_text = url_bytes.decode('utf-16be', errors='replace')
|
||||
return url_text
|
||||
except Exception as url_error:
|
||||
print(f"URL extraction error: {str(url_error)}")
|
||||
# 如果提取失败,尝试完整解码
|
||||
|
||||
# For phone numbers in UCS2 format (e.g., 002B00380036...)
|
||||
if hex_str.startswith("002B") or all(c in "0123456789ABCDEF" for c in hex_str):
|
||||
# Check if it's likely a phone number (starts with +)
|
||||
if hex_str.startswith("002B"): # "+" in UCS2
|
||||
try:
|
||||
# Convert hex string to bytes
|
||||
utf16be_bytes = binascii.unhexlify(hex_str)
|
||||
|
||||
# Decode bytes to text
|
||||
text = utf16be_bytes.decode('utf-16be')
|
||||
return text
|
||||
except:
|
||||
# If it fails, try to extract the phone number directly
|
||||
phone = ""
|
||||
i = 0
|
||||
while i < len(hex_str):
|
||||
if i + 4 <= len(hex_str):
|
||||
chunk = hex_str[i:i+4]
|
||||
if chunk == "002B": # "+"
|
||||
phone += "+"
|
||||
elif chunk.startswith("00") and chunk[2:4].isdigit():
|
||||
phone += chunk[2:4]
|
||||
i += 4
|
||||
else:
|
||||
break
|
||||
if phone:
|
||||
return phone
|
||||
|
||||
# Try multiple decoding approaches
|
||||
try:
|
||||
# Standard UCS2 decoding
|
||||
utf16be_bytes = binascii.unhexlify(hex_str)
|
||||
text = utf16be_bytes.decode('utf-16be', errors='replace')
|
||||
return text
|
||||
except Exception as e1:
|
||||
print(f"Primary UCS2 decoding failed: {str(e1)}")
|
||||
|
||||
try:
|
||||
# Try with different endianness
|
||||
utf16le_bytes = binascii.unhexlify(hex_str)
|
||||
text = utf16le_bytes.decode('utf-16le', errors='replace')
|
||||
return text
|
||||
except Exception as e2:
|
||||
print(f"Secondary UCS2 decoding failed: {str(e2)}")
|
||||
|
||||
try:
|
||||
# 尝试以每4位(2字节)为单位解析,移除非ASCII字符
|
||||
result = ""
|
||||
i = 0
|
||||
while i < len(hex_str):
|
||||
if i + 4 <= len(hex_str):
|
||||
chunk = hex_str[i:i+4]
|
||||
try:
|
||||
# 检查是否可能是ASCII字符(大多数ASCII UCS2编码格式为00xx)
|
||||
if chunk.startswith("00") and 32 <= int(chunk[2:4], 16) <= 126:
|
||||
char = chr(int(chunk[2:4], 16))
|
||||
result += char
|
||||
# 对于非ASCII字符,尝试直接解码
|
||||
else:
|
||||
char_bytes = binascii.unhexlify(chunk)
|
||||
char = char_bytes.decode('utf-16be', errors='ignore')
|
||||
if char:
|
||||
result += char
|
||||
except:
|
||||
pass
|
||||
i += 4
|
||||
else:
|
||||
break
|
||||
|
||||
# 检测结果中的URL
|
||||
url_match = None
|
||||
if "http" in result:
|
||||
url_match = result[result.find("http"):]
|
||||
# 截断到第一个不合法URL字符处
|
||||
for i, c in enumerate(url_match):
|
||||
if c.isspace() or c in '",\'<>()[]{}':
|
||||
url_match = url_match[:i]
|
||||
break
|
||||
|
||||
# 如果找到URL,返回它
|
||||
if url_match and len(url_match) > 10: # 确保URL足够长
|
||||
return url_match
|
||||
|
||||
# 否则返回处理的结果
|
||||
if result:
|
||||
return result
|
||||
except Exception as e3:
|
||||
print(f"Chunk-by-chunk decoding failed: {str(e3)}")
|
||||
|
||||
# 如果所有解码方法都失败,最后尝试查找URL模式
|
||||
try:
|
||||
# 查找HTTP URL的常见模式
|
||||
http_pattern = "00680074007400700073003A002F002F" # "https://"
|
||||
http_alt = "00680074007400700073003a002f002f" # 小写冒号和斜杠
|
||||
|
||||
if http_pattern in hex_str or http_alt in hex_str:
|
||||
start_idx = hex_str.find(http_pattern) if http_pattern in hex_str else hex_str.find(http_alt)
|
||||
if start_idx >= 0:
|
||||
url_hex = hex_str[start_idx:]
|
||||
try:
|
||||
url_bytes = binascii.unhexlify(url_hex)
|
||||
url_text = url_bytes.decode('utf-16be', errors='replace')
|
||||
return url_text
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# 如果所有方法都失败,返回原始十六进制字符串
|
||||
return f"[Hex: {hex_str[:30]}...]"
|
||||
except Exception as e:
|
||||
print(f"UCS2 decoding error: {str(e)}")
|
||||
return f"[Decode error: {hex_str[:30]}...]"
|
||||
|
||||
def is_chinese_text(text):
|
||||
"""Check if text contains Chinese characters"""
|
||||
for char in text:
|
||||
if '\u4e00' <= char <= '\u9fff':
|
||||
return True
|
||||
return False
|
||||
|
||||
def format_phone_number(number):
|
||||
"""Format phone number for SMS sending (add +86 if needed)"""
|
||||
# Remove any spaces, dashes, or parentheses
|
||||
clean_number = ''.join(c for c in number if c.isdigit() or c == '+')
|
||||
|
||||
# If it's a Chinese number without country code, add +86
|
||||
if clean_number.startswith('1') and len(clean_number) == 11:
|
||||
return f"+86{clean_number}"
|
||||
|
||||
# If it doesn't have a + prefix, add it
|
||||
if not clean_number.startswith('+'):
|
||||
return f"+{clean_number}"
|
||||
|
||||
return clean_number
|
||||
@@ -0,0 +1,205 @@
|
||||
import winsound
|
||||
import threading
|
||||
import time
|
||||
|
||||
class SoundManager:
|
||||
def __init__(self):
|
||||
"""Initialize sound manager"""
|
||||
self.is_ringing = False
|
||||
self.ring_thread = None
|
||||
self.incoming_call_active = False
|
||||
self.incoming_call_thread = None
|
||||
|
||||
def play_ringtone(self):
|
||||
"""Play ringtone for incoming call"""
|
||||
if self.is_ringing:
|
||||
return
|
||||
|
||||
self.is_ringing = True
|
||||
self.ring_thread = threading.Thread(target=self._ring_loop)
|
||||
self.ring_thread.daemon = True
|
||||
self.ring_thread.start()
|
||||
|
||||
def _ring_loop(self):
|
||||
"""Ring loop for incoming call"""
|
||||
try:
|
||||
while self.is_ringing:
|
||||
# Play ringtone (1000Hz for 500ms)
|
||||
winsound.Beep(1000, 500)
|
||||
time.sleep(1.0)
|
||||
except Exception as e:
|
||||
print(f"Ringtone error: {str(e)}")
|
||||
finally:
|
||||
self.is_ringing = False
|
||||
|
||||
def stop_ringtone(self):
|
||||
"""停止普通铃声"""
|
||||
# 首先设置停止标志
|
||||
self.is_ringing = False
|
||||
|
||||
# 尝试立即停止系统音效
|
||||
try:
|
||||
winsound.PlaySound(None, winsound.SND_PURGE)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 尝试等待铃声线程结束,使用较短的超时时间
|
||||
if self.ring_thread and self.ring_thread.is_alive():
|
||||
try:
|
||||
self.ring_thread.join(timeout=0.5)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 如果线程仍在运行,创建新的线程引用使旧线程成为孤立线程
|
||||
if self.ring_thread.is_alive():
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 铃声线程没有正常停止,强制释放")
|
||||
self.ring_thread = None
|
||||
|
||||
# 再次确认停止标志已设置
|
||||
self.is_ringing = False
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 普通铃声停止完成")
|
||||
|
||||
def play_incoming_call(self):
|
||||
"""播放来电铃声"""
|
||||
# 如果已经在播放,不重复启动
|
||||
if self.incoming_call_active:
|
||||
return
|
||||
|
||||
# 确保任何之前的铃声线程都已经停止
|
||||
self.stop_incoming_call()
|
||||
|
||||
# 启动新的铃声
|
||||
self.incoming_call_active = True
|
||||
self.incoming_call_thread = threading.Thread(target=self._incoming_call_loop)
|
||||
self.incoming_call_thread.daemon = True
|
||||
self.incoming_call_thread.start()
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 开始播放来电铃声")
|
||||
|
||||
def _incoming_call_loop(self):
|
||||
"""来电铃声循环"""
|
||||
try:
|
||||
# 记录启动时间,避免铃声线程持续太久
|
||||
start_time = time.time()
|
||||
max_duration = 120 # 最长播放时间(秒)
|
||||
|
||||
while self.incoming_call_active and (time.time() - start_time) < max_duration:
|
||||
# 播放来电铃声,使用系统铃声
|
||||
try:
|
||||
winsound.PlaySound("SystemExclamation", winsound.SND_ALIAS)
|
||||
except:
|
||||
# 如果系统铃声不可用,使用传统铃声
|
||||
try:
|
||||
winsound.Beep(1200, 300)
|
||||
time.sleep(0.2)
|
||||
winsound.Beep(1000, 300)
|
||||
except:
|
||||
# 如果Beep也失败,只等待
|
||||
time.sleep(1.0)
|
||||
|
||||
# 每次播放后检查是否应该停止
|
||||
if not self.incoming_call_active:
|
||||
break
|
||||
|
||||
# 铃声间隔
|
||||
time.sleep(1.0)
|
||||
|
||||
# 如果退出是因为超时,打印日志
|
||||
if (time.time() - start_time) >= max_duration:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 来电铃声已达到最长播放时间,自动停止")
|
||||
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 来电铃声错误: {str(e)}")
|
||||
finally:
|
||||
# 确保停止标志被设置
|
||||
self.incoming_call_active = False
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 来电铃声线程正常退出")
|
||||
|
||||
def stop_incoming_call(self):
|
||||
"""停止来电铃声"""
|
||||
# 首先设置停止标志
|
||||
prev_state = self.incoming_call_active
|
||||
self.incoming_call_active = False
|
||||
|
||||
# 打印状态日志,帮助调试
|
||||
if prev_state:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 正在停止来电铃声(之前状态:激活)")
|
||||
else:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 正在停止来电铃声(之前状态:已停止)")
|
||||
|
||||
# 尝试立即停止系统声音
|
||||
try:
|
||||
winsound.PlaySound(None, winsound.SND_PURGE)
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 停止系统声音时出错: {str(e)}")
|
||||
|
||||
# 等待线程结束
|
||||
if self.incoming_call_thread and self.incoming_call_thread.is_alive():
|
||||
try:
|
||||
# 使用较短的超时时间,避免长时间等待
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 等待来电铃声线程结束...")
|
||||
self.incoming_call_thread.join(timeout=0.5)
|
||||
|
||||
# 检查线程是否已结束
|
||||
if not self.incoming_call_thread.is_alive():
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 来电铃声线程已正常结束")
|
||||
else:
|
||||
# 如果线程仍然活动,创建新线程以避免阻塞
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 来电铃声线程未能正常停止,强制释放")
|
||||
# 创建并设置新线程引用,使旧线程成为孤立线程(将被Python垃圾回收)
|
||||
self.incoming_call_thread = None
|
||||
except Exception as e:
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 等待来电铃声线程时出错: {str(e)}")
|
||||
# 重置线程引用
|
||||
self.incoming_call_thread = None
|
||||
|
||||
# 确保标志重置为False(多重保障)
|
||||
self.incoming_call_active = False
|
||||
|
||||
# 确认声音已停止
|
||||
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 来电铃声停止流程完成")
|
||||
|
||||
def play_call_end(self):
|
||||
"""Play call end sound"""
|
||||
try:
|
||||
# Play call end sound (800Hz for 200ms, then 600Hz for 300ms)
|
||||
winsound.Beep(800, 200)
|
||||
winsound.Beep(600, 300)
|
||||
except Exception as e:
|
||||
print(f"Call end sound error: {str(e)}")
|
||||
|
||||
def play_message_received(self):
|
||||
"""Play message received sound"""
|
||||
try:
|
||||
# Play message received sound (three beeps at 1200Hz for 200ms)
|
||||
winsound.Beep(1200, 200)
|
||||
time.sleep(0.1)
|
||||
winsound.Beep(1200, 200)
|
||||
time.sleep(0.1)
|
||||
winsound.Beep(1200, 200)
|
||||
except Exception as e:
|
||||
print(f"Message sound error: {str(e)}")
|
||||
|
||||
def play_error(self):
|
||||
"""Play error sound"""
|
||||
try:
|
||||
# Play error sound (400Hz for 400ms)
|
||||
winsound.Beep(400, 400)
|
||||
except Exception as e:
|
||||
print(f"Error sound error: {str(e)}")
|
||||
|
||||
def play_success(self):
|
||||
"""Play success sound"""
|
||||
try:
|
||||
# Play success sound (1000Hz for 200ms, then 1200Hz for 200ms)
|
||||
winsound.Beep(1000, 200)
|
||||
winsound.Beep(1200, 200)
|
||||
except Exception as e:
|
||||
print(f"Success sound error: {str(e)}")
|
||||
|
||||
def play_dtmf(self):
|
||||
"""播放DTMF按键提示音"""
|
||||
try:
|
||||
# 模拟DTMF音,播放简短高频音
|
||||
winsound.Beep(1400, 100)
|
||||
except Exception as e:
|
||||
print(f"DTMF音播放错误: {str(e)}")
|
||||
@@ -0,0 +1,134 @@
|
||||
import sys
|
||||
import time
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from incoming_call import show_incoming_call
|
||||
from sound_utils import SoundManager
|
||||
import serial
|
||||
|
||||
def simulate_incoming_call():
|
||||
"""模拟来电测试程序"""
|
||||
print("来电模拟器")
|
||||
print("-" * 50)
|
||||
|
||||
# 测试来电对话框
|
||||
print("测试1: 显示来电对话框")
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 创建声音管理器
|
||||
sound_manager = SoundManager()
|
||||
|
||||
# 播放来电铃声
|
||||
sound_manager.play_incoming_call()
|
||||
print("播放来电铃声...")
|
||||
|
||||
# 显示来电对话框
|
||||
caller_number = "+8613800138000"
|
||||
print(f"显示来电: {caller_number}")
|
||||
|
||||
result = show_incoming_call(caller_number)
|
||||
|
||||
# 停止来电铃声
|
||||
sound_manager.stop_incoming_call()
|
||||
|
||||
# 显示结果
|
||||
print(f"用户选择了: {'接听' if result else '拒绝'}")
|
||||
|
||||
# 测试完成
|
||||
print("=" * 50)
|
||||
print("测试完成")
|
||||
|
||||
def test_module_ring_function():
|
||||
"""测试模块的来电功能"""
|
||||
print("LTE模块来电测试")
|
||||
print("-" * 50)
|
||||
|
||||
# 获取COM口
|
||||
port = input("请输入AT命令COM口: ")
|
||||
if not port:
|
||||
print("未提供COM口,测试结束")
|
||||
return
|
||||
|
||||
# 打开串口
|
||||
try:
|
||||
ser = serial.Serial(port, 115200, timeout=1)
|
||||
print(f"成功打开COM口 {port}")
|
||||
except Exception as e:
|
||||
print(f"无法打开COM口: {str(e)}")
|
||||
return
|
||||
|
||||
# 发送AT命令检查模块
|
||||
try:
|
||||
ser.write(b"AT\r")
|
||||
time.sleep(0.5)
|
||||
response = ser.read(ser.in_waiting).decode('utf-8', errors='replace')
|
||||
print(f"AT响应: {response}")
|
||||
|
||||
if "OK" not in response:
|
||||
print("模块没有响应AT命令,测试结束")
|
||||
ser.close()
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"发送AT命令失败: {str(e)}")
|
||||
ser.close()
|
||||
return
|
||||
|
||||
# 主菜单
|
||||
while True:
|
||||
print("\n选择测试选项:")
|
||||
print("1. 模拟来电 (向模块发送AT+CLIP命令)")
|
||||
print("2. 等待实际来电")
|
||||
print("3. 退出")
|
||||
|
||||
choice = input("您的选择: ")
|
||||
|
||||
if choice == '1':
|
||||
# 模拟来电
|
||||
phone_number = input("请输入要模拟的来电号码: ")
|
||||
if not phone_number:
|
||||
phone_number = "+8613800138000"
|
||||
|
||||
# 发送RING和CLIP命令
|
||||
print(f"模拟来电: {phone_number}")
|
||||
ser.write(b"AT+CLIP=1\r")
|
||||
time.sleep(0.5)
|
||||
ser.read(ser.in_waiting) # 清除响应
|
||||
|
||||
# 发送RING和CLIP
|
||||
ser.write(b"RING\r\n")
|
||||
time.sleep(0.5)
|
||||
clip_cmd = f'AT+CLIP: "{phone_number}",129,"",0,"",0\r\n'
|
||||
ser.write(clip_cmd.encode())
|
||||
|
||||
print("模拟来电信号已发送,查看应用是否有响应")
|
||||
|
||||
elif choice == '2':
|
||||
# 等待实际来电
|
||||
print("请使用另一部手机拨打模块的电话号码...")
|
||||
print("按Enter键停止等待")
|
||||
input()
|
||||
|
||||
elif choice == '3':
|
||||
# 退出
|
||||
break
|
||||
|
||||
else:
|
||||
print("无效选择,请重试")
|
||||
|
||||
# 关闭串口
|
||||
ser.close()
|
||||
print("测试结束")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("来电功能测试工具")
|
||||
print("=" * 50)
|
||||
print("1. 测试来电对话框")
|
||||
print("2. 测试LTE模块来电功能")
|
||||
|
||||
choice = input("请选择测试类型: ")
|
||||
|
||||
if choice == '1':
|
||||
simulate_incoming_call()
|
||||
elif choice == '2':
|
||||
test_module_ring_function()
|
||||
else:
|
||||
print("无效选择,测试结束")
|
||||
@@ -0,0 +1,71 @@
|
||||
import sys
|
||||
import os
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QSystemTrayIcon, QMenu, QAction
|
||||
from PyQt5.QtGui import QIcon, QPixmap
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
class TrayIconTest(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Tray Icon Test")
|
||||
self.resize(300, 200)
|
||||
|
||||
# Create a simple colored icon
|
||||
pixmap = QPixmap(32, 32)
|
||||
pixmap.fill(Qt.green)
|
||||
self.icon = QIcon(pixmap)
|
||||
|
||||
# Set window icon
|
||||
self.setWindowIcon(self.icon)
|
||||
|
||||
# Create system tray icon
|
||||
self.tray_icon = QSystemTrayIcon(self)
|
||||
self.tray_icon.setIcon(self.icon)
|
||||
self.tray_icon.setToolTip("Tray Icon Test")
|
||||
|
||||
# Create tray menu
|
||||
tray_menu = QMenu()
|
||||
|
||||
# Add actions
|
||||
show_action = QAction("Show", self)
|
||||
show_action.triggered.connect(self.show)
|
||||
tray_menu.addAction(show_action)
|
||||
|
||||
exit_action = QAction("Exit", self)
|
||||
exit_action.triggered.connect(self.close)
|
||||
tray_menu.addAction(exit_action)
|
||||
|
||||
# Set tray menu
|
||||
self.tray_icon.setContextMenu(tray_menu)
|
||||
|
||||
# Connect signals
|
||||
self.tray_icon.activated.connect(self.on_tray_icon_activated)
|
||||
|
||||
# Show tray icon
|
||||
self.tray_icon.show()
|
||||
|
||||
print("Tray icon visible:", self.tray_icon.isVisible())
|
||||
print("System tray available:", QSystemTrayIcon.isSystemTrayAvailable())
|
||||
|
||||
def on_tray_icon_activated(self, reason):
|
||||
"""Handle tray icon activation"""
|
||||
if reason == QSystemTrayIcon.DoubleClick:
|
||||
if self.isVisible():
|
||||
self.hide()
|
||||
else:
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle close event"""
|
||||
# Remove tray icon
|
||||
if self.tray_icon.isVisible():
|
||||
self.tray_icon.hide()
|
||||
event.accept()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
window = TrayIconTest()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
Reference in New Issue
Block a user