chore: import R0nY3n/LTE_manager main snapshot
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -0,0 +1,39 @@
|
||||
# Python bytecode files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
# Database files
|
||||
lte_data.db
|
||||
dist/lte_data.db
|
||||
|
||||
# PyInstaller
|
||||
build/
|
||||
*.spec
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
ffmpeg/
|
||||
# IDE specific files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
/lte_manager
|
||||
@@ -0,0 +1,19 @@
|
||||
Anti-Capitalist Software License (v 1.4)
|
||||
|
||||
Copyright (C) [Your Name] [Year]
|
||||
|
||||
This is free software. You may use, modify, and distribute it under the
|
||||
terms of the Anti-Capitalist Software License.
|
||||
|
||||
Purpose: To promote a non-capitalist economy where software is not
|
||||
exploited for profit.
|
||||
|
||||
Permissions:
|
||||
- You may use, modify, and share this software freely.
|
||||
|
||||
Restrictions:
|
||||
- You may NOT use this software for commercial purposes.
|
||||
- You may NOT sublicense it under terms that allow commercial use.
|
||||
- You must include this license with all copies of the software.
|
||||
|
||||
Full license text: https://anticapitalist.software/
|
||||
@@ -0,0 +1,340 @@
|
||||

|
||||
|
||||
# LTE Tool / LTE 工具
|
||||
|
||||
A Python+Qt application for managing communications with LTE modules. While specifically optimized for SIM7600CE-T modules, it's compatible with most modules supporting standard AT commands.
|
||||
|
||||
基于Python+Qt开发的LTE模块通信管理工具。虽然专为SIM7600CE-T模块优化,但兼容大多数支持标准AT命令的模块。
|
||||
|
||||
## Features / 功能特点
|
||||
|
||||
- Phone call management (make/receive calls, may not be supported by all modules)
|
||||
- SMS management (send/receive/decode messages, including Chinese)
|
||||
- Module configuration and status monitoring
|
||||
- Serial port configuration
|
||||
- System tray integration with connection status indicator
|
||||
- Improved sound notifications for incoming messages
|
||||
- Last used port memory for easier reconnection
|
||||
- Auto-connect feature at startup
|
||||
- Enhanced Chinese SMS support
|
||||
|
||||
---
|
||||
|
||||
- 电话管理(拨打/接听电话,部分模块可能不支持)
|
||||
- 短信管理(发送/接收/解码消息,支持中文)
|
||||
- 模块配置和状态监控
|
||||
- 串口配置
|
||||
- 系统托盘集成,带连接状态指示
|
||||
- 改进的来电提示音
|
||||
- 记住上次使用的端口,便于重新连接
|
||||
- 启动时自动连接功能
|
||||
- 增强的中文短信支持
|
||||
|
||||
## Requirements / 系统要求
|
||||
|
||||
- Python 3.6+
|
||||
- PyQt5
|
||||
- pyserial
|
||||
|
||||
## Installation / 安装方法
|
||||
|
||||
### English:
|
||||
1. Clone this repository
|
||||
2. Install dependencies:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. Or use the pre-built executable from the `dist` folder
|
||||
|
||||
### 中文:
|
||||
1. 克隆此仓库
|
||||
2. 安装依赖项:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. 或直接使用`dist`文件夹中的预编译可执行文件
|
||||
|
||||
## Usage / 使用方法
|
||||
|
||||
### English:
|
||||
Run the main application:
|
||||
```
|
||||
python main.py
|
||||
```
|
||||
|
||||
Or launch the executable `LTE_Manager.exe` from the `dist` folder.
|
||||
|
||||
### 中文:
|
||||
运行主应用程序:
|
||||
```
|
||||
python main.py
|
||||
```
|
||||
|
||||
或直接从`dist`文件夹启动可执行文件`LTE_Manager.exe`。
|
||||
|
||||
## Module Configuration / 模块配置
|
||||
|
||||
### SMS Configuration / 短信配置
|
||||
#### English:
|
||||
The application is configured to work with modules set to automatically push SMS notifications. By default, it supports the `AT+CNMI=2,2,0,0,0` mode, which provides direct SMS content delivery.
|
||||
|
||||
To configure your module for optimal SMS handling:
|
||||
|
||||
1. **Set SMS text mode**:
|
||||
```
|
||||
AT+CMGF=1
|
||||
```
|
||||
|
||||
2. **Configure SMS automatic notification**:
|
||||
```
|
||||
AT+CNMI=2,2,0,0,0
|
||||
```
|
||||
|
||||
Parameters explanation:
|
||||
- 2: Enable SMS status reports (immediate notification for new messages)
|
||||
- 2: New message notifications sent directly to serial port with content
|
||||
- 0: Disable read status notifications
|
||||
- 0: Disable cell broadcast
|
||||
- 0: Disable reporting of unread messages (only report new messages)
|
||||
|
||||
3. **Verify configuration**:
|
||||
```
|
||||
AT+CNMI?
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```
|
||||
+CNMI: 2,2,0,0,0
|
||||
```
|
||||
|
||||
With this configuration, when a new SMS is received, the module will automatically output the complete message content:
|
||||
```
|
||||
+CMT: "+8613812345678","","23/03/13,15:30:00+32"
|
||||
Hello, this is a test message!
|
||||
```
|
||||
|
||||
#### 中文:
|
||||
应用程序配置为与设置为自动推送短信通知的模块一起工作。默认情况下,它支持`AT+CNMI=2,2,0,0,0`模式,该模式提供直接的短信内容推送。
|
||||
|
||||
要为您的模块配置最佳短信处理:
|
||||
|
||||
1. **设置短信文本模式**:
|
||||
```
|
||||
AT+CMGF=1
|
||||
```
|
||||
|
||||
2. **配置短信自动通知**:
|
||||
```
|
||||
AT+CNMI=2,2,0,0,0
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- 2: 使能短信状态报告(新消息时直接通知)
|
||||
- 2: 新消息通知直接发送到串口并包含内容
|
||||
- 0: 禁用已读状态通知
|
||||
- 0: 关闭小区广播
|
||||
- 0: 关闭上报未读短信(只报告新消息)
|
||||
|
||||
3. **验证配置**:
|
||||
```
|
||||
AT+CNMI?
|
||||
```
|
||||
|
||||
预期响应:
|
||||
```
|
||||
+CNMI: 2,2,0,0,0
|
||||
```
|
||||
|
||||
使用此配置,当收到新短信时,模块将自动输出完整的消息内容:
|
||||
```
|
||||
+CMT: "+8613812345678","","23/03/13,15:30:00+32"
|
||||
Hello, this is a test message!
|
||||
```
|
||||
|
||||
### Alternative SMS Configuration / 替代短信配置
|
||||
#### English:
|
||||
If you prefer to receive only notifications without content and manually read messages, you can use:
|
||||
```
|
||||
AT+CNMI=2,1,0,0,0
|
||||
```
|
||||
|
||||
With this setting, when a new SMS is received, the module will output:
|
||||
```
|
||||
+CMTI: "SM",3
|
||||
```
|
||||
Where "SM" indicates storage in SIM card and "3" is the message index.
|
||||
|
||||
To read the message content, use:
|
||||
```
|
||||
AT+CMGR=3
|
||||
```
|
||||
(where 3 is the index from the notification)
|
||||
|
||||
#### 中文:
|
||||
如果您希望只接收通知而不包含内容,然后手动读取消息,可以使用:
|
||||
```
|
||||
AT+CNMI=2,1,0,0,0
|
||||
```
|
||||
|
||||
使用此设置,当收到新短信时,模块将输出:
|
||||
```
|
||||
+CMTI: "SM",3
|
||||
```
|
||||
其中"SM"表示存储在SIM卡中,"3"是消息索引。
|
||||
|
||||
要读取消息内容,请使用:
|
||||
```
|
||||
AT+CMGR=3
|
||||
```
|
||||
(其中3是通知中的索引)
|
||||
|
||||
## Features Description / 功能描述
|
||||
|
||||
### Phone & SMS Tab / 电话和短信标签页
|
||||
#### English:
|
||||
- Make and receive phone calls (if supported by your module)
|
||||
- Send and receive SMS messages (with Chinese support)
|
||||
- View call and message history
|
||||
- Manage SMS storage
|
||||
|
||||
#### 中文:
|
||||
- 拨打和接听电话(如果您的模块支持)
|
||||
- 发送和接收短信(支持中文)
|
||||
- 查看通话和短信历史记录
|
||||
- 管理短信存储
|
||||
|
||||
### Settings Tab / 设置标签页
|
||||
#### English:
|
||||
- Configure serial ports (AT and NMEA)
|
||||
- View module information (IMEI, IMSI, etc.)
|
||||
- Monitor network status
|
||||
- Enable auto-connect at startup
|
||||
|
||||
#### 中文:
|
||||
- 配置串口(AT和NMEA)
|
||||
- 查看模块信息(IMEI、IMSI等)
|
||||
- 监控网络状态
|
||||
- 启用启动时自动连接
|
||||
|
||||
### System Tray Features / 系统托盘功能
|
||||
#### English:
|
||||
- The application minimizes to system tray
|
||||
- Icon indicates connection status (connected/disconnected)
|
||||
- Right-click menu provides quick access to common functions
|
||||
- Double-click on the tray icon to restore the application window
|
||||
|
||||
#### 中文:
|
||||
- 应用程序可最小化到系统托盘
|
||||
- 图标指示连接状态(已连接/未连接)
|
||||
- 右键菜单提供对常用功能的快速访问
|
||||
- 双击托盘图标可恢复应用程序窗口
|
||||
|
||||
## Recent Improvements / 最近改进
|
||||
### English:
|
||||
- Enhanced AT command response parsing to remove command echoes
|
||||
- Added multiple beep sounds for message notifications
|
||||
- Implemented port selection memory to remember last used ports
|
||||
- Added connection status indicator in system tray
|
||||
- Fixed icon display issues in system tray
|
||||
- Added auto-connect feature at startup
|
||||
- Improved Chinese SMS encoding/decoding
|
||||
- Fixed phone number encoding for Chinese SMS
|
||||
- Added compatibility with a wider range of AT command modules
|
||||
|
||||
### 中文:
|
||||
- 增强AT命令响应解析,移除命令回显
|
||||
- 添加多次蜂鸣声用于消息通知
|
||||
- 实现端口选择记忆功能,记住上次使用的端口
|
||||
- 在系统托盘中添加连接状态指示器
|
||||
- 修复系统托盘图标显示问题
|
||||
- 添加启动时自动连接功能
|
||||
- 改进中文短信编码/解码
|
||||
- 修复中文短信的电话号码编码问题
|
||||
- 增加与更广泛AT命令模块的兼容性
|
||||
|
||||
## Compatibility / 兼容性
|
||||
|
||||
### English:
|
||||
While optimized for SIM7600CE-T modules, this tool is designed to work with most modules that support standard AT commands. The phone functionality may not be available on all modules, but the SMS and configuration features should work on most AT command compatible devices.
|
||||
|
||||
### 中文:
|
||||
虽然针对SIM7600CE-T模块进行了优化,但此工具设计为可与大多数支持标准AT命令的模块一起使用。电话功能可能并非在所有模块上都可用,但短信和配置功能应该在大多数兼容AT命令的设备上正常工作。
|
||||
|
||||
## Donation / 打赏支持
|
||||
|
||||
### English:
|
||||
If you find this tool helpful, consider supporting the developer:
|
||||
|
||||
### 中文:
|
||||
如果您觉得这个工具有用,可以考虑打赏支持开发者,谢谢:
|
||||
|
||||

|
||||
|
||||
## License / 许可证
|
||||
|
||||
### English:
|
||||
This project is licensed under the Adaptive Community Source License (ACSL).
|
||||
|
||||
The ACSL is a community-oriented license that allows for free use, modification, and distribution of the software, while encouraging contributions back to the community. Key points:
|
||||
|
||||
1. You can use, modify, and distribute this software freely.
|
||||
2. If you distribute modified versions, you should make your changes available to the community.
|
||||
3. Commercial use is permitted, but commercial redistributions should contribute improvements back.
|
||||
4. No warranty is provided; use at your own risk.
|
||||
|
||||
[Read the full ACSL license](https://anticapitalist.software/)
|
||||
|
||||
### 中文:
|
||||
本项目采用自适应社区源代码许可证 (ACSL) 授权。
|
||||
|
||||
ACSL是一种面向社区的许可证,允许自由使用、修改和分发软件,同时鼓励向社区回馈贡献。主要要点:
|
||||
|
||||
1. 您可以自由使用、修改和分发此软件。
|
||||
2. 如果您分发修改版本,应将您的更改提供给社区。
|
||||
3. 允许商业使用,但商业再分发应将改进回馈给社区。
|
||||
4. 不提供任何保证;使用风险自负。
|
||||
|
||||
[阅读完整license](https://anticapitalist.software/)
|
||||
|
||||
## 数据存储
|
||||
|
||||
- 数据库文件现在保存在用户主目录下的 `.LTE` 文件夹中
|
||||
- 这样可以在应用更新后保留历史记录
|
||||
- 数据库路径: `C:\Users\<用户名>\.LTE\lte_data.db`
|
||||
|
||||
## 图标系统
|
||||
|
||||
应用现在使用三种状态图标:
|
||||
- default.png: 默认状态/未连接
|
||||
- running.png: 连接成功/正在运行
|
||||
- error.png: 发生错误
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 连接LTE模块到计算机
|
||||
2. 在设置选项卡中配置串口设置
|
||||
3. 连接到模块
|
||||
4. 使用电话和短信功能
|
||||
|
||||
## 系统托盘
|
||||
|
||||
应用程序可以最小化到系统托盘。双击托盘图标可以显示/隐藏主窗口。
|
||||
|
||||
## 开发者注意事项
|
||||
|
||||
使用PyInstaller打包时的资源文件注意事项:
|
||||
|
||||
1. 图标文件应在spec文件中添加为附加数据:
|
||||
```
|
||||
a = Analysis(...,
|
||||
datas=[
|
||||
('default.png', '.'),
|
||||
('running.png', '.'),
|
||||
('error.png', '.')
|
||||
],
|
||||
...)
|
||||
```
|
||||
|
||||
2. 可以使用 `create_icons.py` 脚本生成所需的默认图标文件
|
||||
|
||||
3. 数据库文件将自动保存在用户主目录的.LTE文件夹中
|
||||
|
After Width: | Height: | Size: 194 KiB |
@@ -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,588 @@
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from PyQt5.QtCore import QObject, pyqtSignal
|
||||
import platform
|
||||
|
||||
# 配置日志记录
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger("AudioFeatures")
|
||||
|
||||
class AudioFeatures(QObject):
|
||||
"""音频功能模块,提供通话录音和音频播放功能"""
|
||||
|
||||
status_changed = pyqtSignal(str) # 状态变化信号
|
||||
|
||||
def __init__(self, lte_manager):
|
||||
super().__init__()
|
||||
self.lte_manager = lte_manager
|
||||
self.recording = False
|
||||
self.playing = False
|
||||
self.auto_record_calls = False # 是否自动录制通话
|
||||
self.auto_play_after_call = False # 是否在通话结束后自动播放录音
|
||||
self.auto_play_on_answer = False # 是否在接听电话时自动播放声音
|
||||
self.answer_play_audio_file = None # 接听时播放的音频文件
|
||||
self.current_recording_file = None # 当前录音文件路径
|
||||
|
||||
# 获取用户 home 目录
|
||||
self.user_home = os.path.expanduser("~")
|
||||
|
||||
# 创建默认存储路径:用户home/.LTE/REC/
|
||||
self.base_storage_path = os.path.join(self.user_home, ".LTE")
|
||||
self.storage_path = os.path.join(self.base_storage_path, "REC")
|
||||
self.ensure_storage_path()
|
||||
|
||||
# 创建音频文件存储路径
|
||||
self.audio_storage_path = os.path.join(self.base_storage_path, "AUDIO")
|
||||
self.ensure_audio_storage_path()
|
||||
|
||||
# 支持的音频格式
|
||||
self.supported_formats = [".amr", ".wav", ".mp3", ".pcm"]
|
||||
|
||||
def ensure_storage_path(self):
|
||||
"""确保存储路径存在"""
|
||||
try:
|
||||
if not os.path.exists(self.storage_path):
|
||||
os.makedirs(self.storage_path)
|
||||
logger.info(f"已创建存储路径: {self.storage_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建存储路径失败: {str(e)}")
|
||||
|
||||
def ensure_audio_storage_path(self):
|
||||
"""确保音频文件存储路径存在"""
|
||||
try:
|
||||
if not os.path.exists(self.audio_storage_path):
|
||||
os.makedirs(self.audio_storage_path)
|
||||
logger.info(f"已创建音频存储路径: {self.audio_storage_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建音频存储路径失败: {str(e)}")
|
||||
|
||||
def set_storage_path(self, path):
|
||||
"""设置存储路径"""
|
||||
if os.path.exists(path):
|
||||
self.storage_path = path
|
||||
logger.info(f"存储路径已设置为: {path}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"存储路径不存在: {path}")
|
||||
return False
|
||||
|
||||
def set_auto_record_calls(self, enabled):
|
||||
"""设置是否自动录制通话"""
|
||||
self.auto_record_calls = enabled
|
||||
logger.info(f"自动录制通话功能已{'启用' if enabled else '禁用'}")
|
||||
return True
|
||||
|
||||
def set_auto_play_after_call(self, enabled):
|
||||
"""设置是否在通话结束后自动播放录音"""
|
||||
self.auto_play_after_call = enabled
|
||||
logger.info(f"通话结束后自动播放录音功能已{'启用' if enabled else '禁用'}")
|
||||
return True
|
||||
|
||||
def set_auto_play_on_answer(self, enabled, audio_file=None):
|
||||
"""
|
||||
设置是否在接听电话时自动播放音频
|
||||
|
||||
参数:
|
||||
- enabled: 是否启用该功能
|
||||
- audio_file: 要播放的音频文件路径,如果为None则使用当前设置的文件
|
||||
|
||||
返回:
|
||||
- bool: 是否成功设置
|
||||
"""
|
||||
self.auto_play_on_answer = enabled
|
||||
|
||||
if audio_file:
|
||||
# 检查文件是否存在且格式支持
|
||||
if os.path.exists(audio_file) and any(audio_file.lower().endswith(fmt) for fmt in self.supported_formats):
|
||||
self.answer_play_audio_file = audio_file
|
||||
logger.info(f"接听电话自动播放音频功能已{'启用' if enabled else '禁用'}, 音频文件: {os.path.basename(audio_file)}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"音频文件不存在或格式不支持: {audio_file}")
|
||||
return False
|
||||
else:
|
||||
# 如果未指定文件但已有设置的文件
|
||||
if enabled and not self.answer_play_audio_file:
|
||||
logger.warning("启用接听电话自动播放音频功能,但未指定音频文件")
|
||||
return False
|
||||
|
||||
logger.info(f"接听电话自动播放音频功能已{'启用' if enabled else '禁用'}")
|
||||
return True
|
||||
|
||||
def play_on_answer(self, phone_number=None):
|
||||
"""
|
||||
接听电话时自动播放音频
|
||||
|
||||
参数:
|
||||
- phone_number: 可选,来电号码用于日志记录
|
||||
|
||||
返回:
|
||||
- bool: 是否成功播放
|
||||
"""
|
||||
if not self.auto_play_on_answer or not self.answer_play_audio_file:
|
||||
logger.info("接听电话自动播放功能未启用或未设置音频文件")
|
||||
return False
|
||||
|
||||
if not os.path.exists(self.answer_play_audio_file):
|
||||
logger.error(f"接听自动播放音频文件不存在: {self.answer_play_audio_file}")
|
||||
return False
|
||||
|
||||
# 先停止可能正在播放的音频
|
||||
if self.playing:
|
||||
self.stop_audio()
|
||||
time.sleep(0.5) # 等待停止完成
|
||||
|
||||
# 使用远程播放模式,让对方能听到声音
|
||||
logger.info(f"接听电话({phone_number}),自动播放音频: {os.path.basename(self.answer_play_audio_file)}")
|
||||
return self.play_audio(self.answer_play_audio_file, play_path=1) # play_path=1表示远程播放,对方听得到
|
||||
|
||||
def start_call_recording(self, phone_number=None):
|
||||
"""
|
||||
开始通话录音
|
||||
|
||||
参数:
|
||||
- phone_number: 电话号码,用于文件命名
|
||||
|
||||
返回:
|
||||
- bool: 是否成功开始录音
|
||||
- str: 录音文件路径
|
||||
"""
|
||||
if not phone_number:
|
||||
phone_number = self.lte_manager.call_number if hasattr(self.lte_manager, 'call_number') else "unknown"
|
||||
|
||||
# 创建基于电话号码和时间的文件名
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"call_{phone_number}_{timestamp}.wav"
|
||||
|
||||
# 完整文件路径
|
||||
file_path = os.path.join(self.storage_path, filename)
|
||||
|
||||
# 存储当前录音文件路径用于可能的自动播放
|
||||
self.current_recording_file = file_path
|
||||
|
||||
# 调用录音方法,使用双方声音混合录制模式(3)
|
||||
result = self.start_recording(filename=filename, record_path=3)
|
||||
|
||||
return result, file_path
|
||||
|
||||
def start_recording(self, filename=None, record_path=1):
|
||||
"""
|
||||
开始录音
|
||||
|
||||
参数:
|
||||
- filename: 录音文件名,不包含路径。如果未提供,则使用时间戳命名
|
||||
- record_path: 录音路径类型
|
||||
1 = 本地路径 (录制本地麦克风)
|
||||
2 = 远程路径 (录制通话对方声音)
|
||||
3 = 混合模式 (录制双方声音)
|
||||
|
||||
返回:
|
||||
- bool: 是否成功开始录音
|
||||
"""
|
||||
if self.recording:
|
||||
logger.warning("当前已有录音正在进行")
|
||||
return False
|
||||
|
||||
if not self.lte_manager.is_connected():
|
||||
logger.error("未连接到LTE模块,无法开始录音")
|
||||
self.status_changed.emit("未连接到LTE模块,无法开始录音")
|
||||
return False
|
||||
|
||||
# 如果未提供文件名,使用时间戳命名
|
||||
if not filename:
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"recording_{timestamp}.wav" # 默认使用wav格式
|
||||
|
||||
# 确保文件格式正确
|
||||
if not any(filename.lower().endswith(fmt) for fmt in self.supported_formats):
|
||||
filename += ".wav" # 默认使用wav格式
|
||||
|
||||
# 组合完整路径
|
||||
file_path = os.path.join(self.storage_path, filename)
|
||||
# 使用模块内路径格式 (c:/ 对应模块内存储)
|
||||
module_path = f"c:/{os.path.basename(filename)}"
|
||||
|
||||
# 记录当前录音文件路径
|
||||
self.current_recording_file = file_path
|
||||
|
||||
# 发送录音命令
|
||||
command = f'AT+CREC={record_path},"{module_path}"'
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
|
||||
if "+CREC: 1" in response or "+CREC: 2" in response or "+CREC: 3" in response:
|
||||
self.recording = True
|
||||
logger.info(f"录音已开始: {file_path}")
|
||||
self.status_changed.emit(f"录音已开始: {os.path.basename(file_path)}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"开始录音失败: {response}")
|
||||
self.status_changed.emit(f"开始录音失败")
|
||||
self.current_recording_file = None
|
||||
return False
|
||||
|
||||
def stop_recording(self):
|
||||
"""
|
||||
停止录音
|
||||
|
||||
返回:
|
||||
- bool: 是否成功停止录音
|
||||
"""
|
||||
if not self.recording:
|
||||
logger.warning("当前没有录音正在进行")
|
||||
return False
|
||||
|
||||
if not self.lte_manager.is_connected():
|
||||
logger.error("未连接到LTE模块,无法停止录音")
|
||||
self.status_changed.emit("未连接到LTE模块,无法停止录音")
|
||||
return False
|
||||
|
||||
# 发送停止录音命令
|
||||
command = "AT+CREC=0"
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
|
||||
if "+CREC: 0" in response:
|
||||
self.recording = False
|
||||
logger.info("录音已停止")
|
||||
self.status_changed.emit("录音已停止")
|
||||
|
||||
# 等待模块完成录音处理
|
||||
time.sleep(0.5)
|
||||
|
||||
# 检查是否收到录音完成信号
|
||||
if "+CREC: crec stop" in response:
|
||||
logger.info("录音已完成处理")
|
||||
|
||||
# 检查是否需要自动播放录音
|
||||
if self.auto_play_after_call and self.current_recording_file:
|
||||
logger.info(f"准备自动播放录音: {self.current_recording_file}")
|
||||
# 等待一下以确保录音文件已完成保存
|
||||
time.sleep(1)
|
||||
self.play_audio(self.current_recording_file)
|
||||
|
||||
return True
|
||||
else:
|
||||
logger.error(f"停止录音失败: {response}")
|
||||
self.status_changed.emit("停止录音失败")
|
||||
return False
|
||||
|
||||
def is_recording(self):
|
||||
"""
|
||||
检查是否正在录音
|
||||
|
||||
返回:
|
||||
- bool: 是否正在录音
|
||||
"""
|
||||
if not self.lte_manager.is_connected():
|
||||
return False
|
||||
|
||||
# 查询录音状态
|
||||
response = self.lte_manager.send_at_command("AT+CREC?")
|
||||
|
||||
if "+CREC: 1" in response or "+CREC: 2" in response or "+CREC: 3" in response:
|
||||
self.recording = True
|
||||
return True
|
||||
else:
|
||||
self.recording = False
|
||||
return False
|
||||
|
||||
def play_audio(self, filename, repeat=0, play_path=0):
|
||||
"""
|
||||
播放音频文件
|
||||
|
||||
参数:
|
||||
- filename: 音频文件名,可以是绝对路径或相对路径
|
||||
- repeat: 重复播放次数,0表示只播放一次,1-255表示重复播放的次数
|
||||
- play_path: 播放路径
|
||||
0 = 本地播放(默认)
|
||||
1 = 远程播放(通话时对方听到)
|
||||
2 = 双方都播放(本地和远程)
|
||||
|
||||
返回:
|
||||
- bool: 是否成功开始播放
|
||||
"""
|
||||
if self.playing:
|
||||
logger.warning("当前已有音频正在播放")
|
||||
self.stop_audio() # 先停止当前播放
|
||||
|
||||
if not self.lte_manager.is_connected():
|
||||
logger.error("未连接到LTE模块,无法播放音频")
|
||||
self.status_changed.emit("未连接到LTE模块,无法播放音频")
|
||||
return False
|
||||
|
||||
# 确保文件存在且格式受支持
|
||||
if not os.path.exists(filename):
|
||||
# 尝试在存储路径中查找
|
||||
full_path = os.path.join(self.storage_path, filename)
|
||||
if not os.path.exists(full_path):
|
||||
# 尝试在音频路径中查找
|
||||
audio_path = os.path.join(self.audio_storage_path, filename)
|
||||
if not os.path.exists(audio_path):
|
||||
logger.error(f"音频文件不存在: {filename}")
|
||||
self.status_changed.emit(f"音频文件不存在: {os.path.basename(filename)}")
|
||||
return False
|
||||
filename = audio_path
|
||||
else:
|
||||
filename = full_path
|
||||
|
||||
if not any(filename.lower().endswith(fmt) for fmt in self.supported_formats):
|
||||
logger.error(f"不支持的音频格式: {filename}")
|
||||
self.status_changed.emit(f"不支持的音频格式: {os.path.basename(filename)}")
|
||||
return False
|
||||
|
||||
# 转换为模块内路径格式
|
||||
module_path = f"c:/{os.path.basename(filename)}"
|
||||
|
||||
# 根据文件类型选择播放命令 (对于wav文件可以使用AT+CCMXPLAYWAV)
|
||||
if filename.lower().endswith(".wav"):
|
||||
command = f'AT+CCMXPLAYWAV="{module_path}",{play_path}'
|
||||
else:
|
||||
# 其他格式使用AT+CCMXPLAY命令
|
||||
command = f'AT+CCMXPLAY="{module_path}",{play_path},{repeat}'
|
||||
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
|
||||
success = False
|
||||
if "+CCMXPLAY:" in response and "OK" in response:
|
||||
success = True
|
||||
elif "+CCMXPLAYWAV:" in response and "OK" in response:
|
||||
success = True
|
||||
|
||||
if success:
|
||||
self.playing = True
|
||||
logger.info(f"开始播放音频: {filename}")
|
||||
self.status_changed.emit(f"开始播放音频: {os.path.basename(filename)}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"播放音频失败: {response}")
|
||||
self.status_changed.emit("播放音频失败")
|
||||
return False
|
||||
|
||||
def stop_audio(self):
|
||||
"""
|
||||
停止音频播放
|
||||
|
||||
返回:
|
||||
- bool: 是否成功停止播放
|
||||
"""
|
||||
if not self.playing:
|
||||
logger.info("当前没有音频播放")
|
||||
return True
|
||||
|
||||
if not self.lte_manager.is_connected():
|
||||
logger.error("未连接到LTE模块,无法停止播放")
|
||||
self.status_changed.emit("未连接到LTE模块,无法停止播放")
|
||||
return False
|
||||
|
||||
# 检查是否正在播放WAV文件
|
||||
response_wav = self.lte_manager.send_at_command("AT+CCMXSTOPWAV")
|
||||
response_normal = self.lte_manager.send_at_command("AT+CCMXSTOP")
|
||||
|
||||
success = False
|
||||
|
||||
if "+CCMXSTOPWAV:" in response_wav and "OK" in response_wav:
|
||||
success = True
|
||||
elif "+CCMXSTOP:" in response_normal and "OK" in response_normal:
|
||||
success = True
|
||||
|
||||
if success:
|
||||
self.playing = False
|
||||
logger.info("音频播放已停止")
|
||||
self.status_changed.emit("音频播放已停止")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"停止音频播放失败: {response_wav}, {response_normal}")
|
||||
self.status_changed.emit("停止音频播放失败")
|
||||
return False
|
||||
|
||||
def set_ringtone(self, filename):
|
||||
"""
|
||||
设置来电铃声
|
||||
|
||||
参数:
|
||||
- filename: 铃声文件名,可以是绝对路径或相对路径
|
||||
|
||||
返回:
|
||||
- bool: 是否成功设置铃声
|
||||
"""
|
||||
if not self.lte_manager.is_connected():
|
||||
logger.error("未连接到LTE模块,无法设置铃声")
|
||||
self.status_changed.emit("未连接到LTE模块,无法设置铃声")
|
||||
return False
|
||||
|
||||
# 确保文件存在且格式受支持
|
||||
if not os.path.exists(filename):
|
||||
# 尝试在存储路径中查找
|
||||
full_path = os.path.join(self.storage_path, filename)
|
||||
if not os.path.exists(full_path):
|
||||
# 尝试在音频路径中查找
|
||||
audio_path = os.path.join(self.audio_storage_path, filename)
|
||||
if not os.path.exists(audio_path):
|
||||
logger.error(f"铃声文件不存在: {filename}")
|
||||
self.status_changed.emit(f"铃声文件不存在: {os.path.basename(filename)}")
|
||||
return False
|
||||
filename = audio_path
|
||||
else:
|
||||
filename = full_path
|
||||
|
||||
if not any(filename.lower().endswith(fmt) for fmt in self.supported_formats):
|
||||
logger.error(f"不支持的铃声格式: {filename}")
|
||||
self.status_changed.emit(f"不支持的铃声格式: {os.path.basename(filename)}")
|
||||
return False
|
||||
|
||||
# 转换为模块内路径格式
|
||||
module_path = f"c:/{os.path.basename(filename)}"
|
||||
|
||||
# 发送设置铃声命令
|
||||
command = f'AT+CRINGSET="{module_path}",1'
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
|
||||
if "OK" in response:
|
||||
logger.info(f"铃声已设置: {filename}")
|
||||
self.status_changed.emit(f"铃声已设置: {os.path.basename(filename)}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"设置铃声失败: {response}")
|
||||
self.status_changed.emit("设置铃声失败")
|
||||
return False
|
||||
|
||||
def ring_switch(self, enable=True):
|
||||
"""
|
||||
开启或关闭铃声
|
||||
|
||||
参数:
|
||||
- enable: 是否启用铃声
|
||||
|
||||
返回:
|
||||
- bool: 是否成功切换铃声状态
|
||||
"""
|
||||
if not self.lte_manager.is_connected():
|
||||
logger.error("未连接到LTE模块,无法切换铃声状态")
|
||||
self.status_changed.emit("未连接到LTE模块,无法切换铃声状态")
|
||||
return False
|
||||
|
||||
# 发送铃声开关命令
|
||||
status = 1 if enable else 0
|
||||
command = f"AT+CRTSWITCH={status}"
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
|
||||
if "OK" in response:
|
||||
state = "开启" if enable else "关闭"
|
||||
logger.info(f"铃声已{state}")
|
||||
self.status_changed.emit(f"铃声已{state}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"切换铃声状态失败: {response}")
|
||||
self.status_changed.emit("切换铃声状态失败")
|
||||
return False
|
||||
|
||||
def generate_dtmf(self, dtmf_string, duration=1, time_base=100):
|
||||
"""
|
||||
生成DTMF音
|
||||
|
||||
参数:
|
||||
- dtmf_string: DTMF字符串,如"1,2,3,4"
|
||||
- duration: 持续时间因子,1-100
|
||||
- time_base: 时间基准,50-500ms
|
||||
|
||||
返回:
|
||||
- bool: 是否成功生成DTMF音
|
||||
"""
|
||||
if not self.lte_manager.is_connected():
|
||||
logger.error("未连接到LTE模块,无法生成DTMF音")
|
||||
self.status_changed.emit("未连接到LTE模块,无法生成DTMF音")
|
||||
return False
|
||||
|
||||
# 确保DTMF字符串格式正确(数字、字母A-D、*、#,逗号分隔)
|
||||
valid_chars = set("0123456789ABCD*#,")
|
||||
if not all(c.upper() in valid_chars for c in dtmf_string):
|
||||
logger.error(f"无效的DTMF字符串: {dtmf_string}")
|
||||
self.status_changed.emit("无效的DTMF字符串")
|
||||
return False
|
||||
|
||||
# 发送DTMF生成命令
|
||||
command = f'AT+CLDTMF={duration},"{dtmf_string}",{time_base},0'
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
|
||||
if "OK" in response:
|
||||
logger.info(f"DTMF音已生成: {dtmf_string}")
|
||||
self.status_changed.emit(f"DTMF音已生成")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"生成DTMF音失败: {response}")
|
||||
self.status_changed.emit("生成DTMF音失败")
|
||||
return False
|
||||
|
||||
def generate_tone(self, frequency=1000, period_on=200, period_off=200, duration=1000):
|
||||
"""
|
||||
生成特定频率的音调
|
||||
|
||||
参数:
|
||||
- frequency: 频率,20-4000Hz
|
||||
- period_on: 音调开启周期,50-25500ms
|
||||
- period_off: 音调关闭周期,0或40-25500ms
|
||||
- duration: 持续时间,50-500000ms
|
||||
|
||||
返回:
|
||||
- bool: 是否成功生成音调
|
||||
"""
|
||||
if not self.lte_manager.is_connected():
|
||||
logger.error("未连接到LTE模块,无法生成音调")
|
||||
self.status_changed.emit("未连接到LTE模块,无法生成音调")
|
||||
return False
|
||||
|
||||
# 检查参数范围
|
||||
if not (20 <= frequency <= 4000):
|
||||
logger.error(f"频率超出范围(20-4000Hz): {frequency}")
|
||||
frequency = max(20, min(frequency, 4000))
|
||||
|
||||
if not (50 <= period_on <= 25500):
|
||||
logger.error(f"开启周期超出范围(50-25500ms): {period_on}")
|
||||
period_on = max(50, min(period_on, 25500))
|
||||
|
||||
if period_off != 0 and not (40 <= period_off <= 25500):
|
||||
logger.error(f"关闭周期超出范围(0或40-25500ms): {period_off}")
|
||||
period_off = max(40, min(period_off, 25500))
|
||||
|
||||
if not (50 <= duration <= 500000):
|
||||
logger.error(f"持续时间超出范围(50-500000ms): {duration}")
|
||||
duration = max(50, min(duration, 500000))
|
||||
|
||||
# 发送音调生成命令
|
||||
command = f"AT+SIMTONE=1,{frequency},{period_on},{period_off},{duration}"
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
|
||||
if "OK" in response:
|
||||
logger.info(f"音调已生成: {frequency}Hz")
|
||||
self.status_changed.emit(f"音调已生成: {frequency}Hz")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"生成音调失败: {response}")
|
||||
self.status_changed.emit("生成音调失败")
|
||||
return False
|
||||
|
||||
def stop_tone(self):
|
||||
"""
|
||||
停止音调
|
||||
|
||||
返回:
|
||||
- bool: 是否成功停止音调
|
||||
"""
|
||||
if not self.lte_manager.is_connected():
|
||||
logger.error("未连接到LTE模块,无法停止音调")
|
||||
self.status_changed.emit("未连接到LTE模块,无法停止音调")
|
||||
return False
|
||||
|
||||
# 发送停止音调命令
|
||||
command = "AT+SIMTONE=0"
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
|
||||
if "OK" in response:
|
||||
logger.info("音调已停止")
|
||||
self.status_changed.emit("音调已停止")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"停止音调失败: {response}")
|
||||
self.status_changed.emit("停止音调失败")
|
||||
return False
|
||||
|
After Width: | Height: | Size: 194 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
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 '拒绝'}来电")
|
||||
|
After Width: | Height: | Size: 113 KiB |
@@ -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_())
|
||||
|
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()
|
||||
|
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_())
|
||||
@@ -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
|
||||
|
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 '拒绝'}来电")
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"at_port": "COM9",
|
||||
"at_baudrate": "115200",
|
||||
"nmea_port": "None",
|
||||
"nmea_baudrate": "9600",
|
||||
"auto_connect": true,
|
||||
"audio_enabled": true,
|
||||
"ringtone_file": "C:/Users/Ron21/.LTE/incommingcall.mp3",
|
||||
"recording_path": "",
|
||||
"auto_record_calls": true,
|
||||
"auto_play_after_call": false,
|
||||
"auto_play_on_answer": false,
|
||||
"answer_play_audio_file": ""
|
||||
}
|
||||
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,791 @@
|
||||
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):
|
||||
"""Make phone call"""
|
||||
number = self.phone_number_input.text().strip()
|
||||
if not number:
|
||||
QMessageBox.warning(self, "输入错误", "请输入电话号码")
|
||||
return
|
||||
|
||||
if self.lte_manager.make_call(number):
|
||||
self.call_button.setEnabled(False)
|
||||
self.answer_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(True)
|
||||
self.add_to_call_log(f"正在拨打 {number}")
|
||||
|
||||
# 更新通话状态
|
||||
self.call_status_display.setText(f"通话状态: 呼出通话, 拨号中, 号码: {number}")
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #E1BEE7; color: #6A1B9A; border-radius: 3px;")
|
||||
|
||||
# Add to database
|
||||
self.database.add_call(number, "outgoing")
|
||||
else:
|
||||
QMessageBox.warning(self, "通话错误", "拨打电话失败")
|
||||
self.sound_manager.play_error()
|
||||
|
||||
def on_answer_button_clicked(self):
|
||||
"""处理接听按钮点击"""
|
||||
# 获取通话状态,确认有来电
|
||||
calls = self.lte_manager.get_call_status()
|
||||
has_incoming_call = False
|
||||
caller_number = ""
|
||||
|
||||
for call in calls:
|
||||
if call.get('stat') == 4 and call.get('dir') == 1: # 来电中(MT)
|
||||
has_incoming_call = True
|
||||
caller_number = call.get('number', self.lte_manager.call_number)
|
||||
break
|
||||
|
||||
if not has_incoming_call:
|
||||
QMessageBox.warning(self, "通话错误", "当前没有待接听的来电")
|
||||
self.sound_manager.play_error()
|
||||
return
|
||||
|
||||
# 停止所有铃声
|
||||
self._stop_all_ringtones()
|
||||
|
||||
# 尝试接听
|
||||
answer_result = self.lte_manager.answer_call()
|
||||
|
||||
# 再次检查通话状态,确认是否实际接通(即使API返回失败)
|
||||
time.sleep(0.5) # 给模块一点时间更新状态
|
||||
calls_after = self.lte_manager.get_call_status()
|
||||
call_established = False
|
||||
|
||||
for call in calls_after:
|
||||
if call.get('stat') in [0, 1] and call.get('dir') == 1: # 活动或保持的呼入通话
|
||||
call_established = True
|
||||
break
|
||||
|
||||
if answer_result or call_established:
|
||||
self.call_button.setEnabled(False)
|
||||
self.answer_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(True)
|
||||
self.add_to_call_log(f"已接听来电: {caller_number}")
|
||||
|
||||
# 更新通话状态
|
||||
self.call_status_display.setText(f"通话状态: 呼入通话, 已接通, 号码: {caller_number}")
|
||||
self.call_status_display.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px; background-color: #C8E6C9; color: #2E7D32; border-radius: 3px;")
|
||||
|
||||
# 不需要重复添加数据库记录,main.py已经在显示来电对话框时添加
|
||||
else:
|
||||
QMessageBox.warning(self, "通话错误", "接听来电失败")
|
||||
self.sound_manager.play_error()
|
||||
|
||||
def on_hangup_button_clicked(self):
|
||||
"""处理挂断按钮点击"""
|
||||
if self.lte_manager.end_call():
|
||||
self.call_button.setEnabled(True)
|
||||
self.answer_button.setEnabled(False)
|
||||
self.hangup_button.setEnabled(False)
|
||||
self.add_to_call_log("通话结束")
|
||||
|
||||
# 更新通话状态
|
||||
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.sound_manager.play_call_end()
|
||||
else:
|
||||
QMessageBox.warning(self, "通话错误", "挂断电话失败")
|
||||
self.sound_manager.play_error()
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,6 @@
|
||||
PyQt5==5.15.9
|
||||
pyserial==3.5
|
||||
sounddevice==0.4.6
|
||||
numpy==1.26.3
|
||||
pyinstaller==6.0.0
|
||||
# SQLite is included in Python standard library
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,723 @@
|
||||
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QLineEdit, QTextEdit, QGroupBox, QFormLayout, QComboBox,
|
||||
QGridLayout, QMessageBox, QSpinBox, QCheckBox, QFileDialog)
|
||||
from PyQt5.QtCore import Qt, pyqtSlot
|
||||
import serial.tools.list_ports
|
||||
import os
|
||||
import json
|
||||
|
||||
class SettingsTab(QWidget):
|
||||
def __init__(self, lte_manager, audio_features=None):
|
||||
super().__init__()
|
||||
self.lte_manager = lte_manager
|
||||
self.audio_features = audio_features
|
||||
|
||||
# Settings file path
|
||||
self.settings_file = "lte_settings.json"
|
||||
|
||||
# Default settings
|
||||
self.settings = {
|
||||
"at_port": "",
|
||||
"at_baudrate": "115200",
|
||||
"nmea_port": "None",
|
||||
"nmea_baudrate": "9600",
|
||||
"auto_connect": False,
|
||||
"audio_enabled": True,
|
||||
"ringtone_file": "",
|
||||
"recording_path": "",
|
||||
"auto_record_calls": True, # 自动录制通话
|
||||
"auto_play_after_call": False, # 通话结束后自动播放录音
|
||||
"auto_play_on_answer": False, # 接听电话时自动播放声音
|
||||
"answer_play_audio_file": "", # 接听时播放的音频文件
|
||||
}
|
||||
|
||||
# 从文件加载设置
|
||||
try:
|
||||
if os.path.exists(self.settings_file):
|
||||
with open(self.settings_file, 'r') as f:
|
||||
saved_settings = json.load(f)
|
||||
for key, value in saved_settings.items():
|
||||
self.settings[key] = value
|
||||
except Exception as e:
|
||||
print(f"加载设置失败: {str(e)}")
|
||||
|
||||
# Connect signals
|
||||
self.lte_manager.status_changed.connect(self.on_status_changed)
|
||||
if self.audio_features:
|
||||
self.audio_features.status_changed.connect(self.on_status_changed)
|
||||
|
||||
# Create UI components first
|
||||
self._create_ui_components()
|
||||
|
||||
# Then setup the UI layout
|
||||
self.setup_ui()
|
||||
|
||||
# Finally refresh the ports
|
||||
self.refresh_ports()
|
||||
|
||||
# 应用设置到UI
|
||||
self._apply_settings_to_ui()
|
||||
|
||||
# 如果初始化时有音频特性模块,立即应用相关设置
|
||||
if self.audio_features:
|
||||
# 设置录音路径
|
||||
if self.settings.get("recording_path") and os.path.exists(self.settings.get("recording_path")):
|
||||
self.audio_features.set_storage_path(self.settings.get("recording_path"))
|
||||
|
||||
# 设置自动录音选项
|
||||
self.audio_features.set_auto_record_calls(self.settings.get("auto_record_calls", True))
|
||||
|
||||
# 设置自动播放选项
|
||||
self.audio_features.set_auto_play_after_call(self.settings.get("auto_play_after_call", False))
|
||||
|
||||
def _apply_settings_to_ui(self):
|
||||
"""将加载的设置应用到UI组件"""
|
||||
# 加载设置
|
||||
self.auto_record_cb.setChecked(self.settings.get('auto_record_calls', True))
|
||||
self.auto_play_cb.setChecked(self.settings.get('auto_play_after_call', False))
|
||||
self.auto_play_on_answer_cb.setChecked(self.settings.get('auto_play_on_answer', False))
|
||||
|
||||
recording_path = self.settings.get('recording_path', '')
|
||||
self.recording_path_edit.setText(recording_path)
|
||||
|
||||
answer_play_file = self.settings.get('answer_play_audio_file', '')
|
||||
self.answer_play_edit.setText(answer_play_file)
|
||||
|
||||
# 如果音频特性实例存在,应用设置
|
||||
if self.audio_features:
|
||||
self.audio_features.set_auto_record_calls(self.auto_record_cb.isChecked())
|
||||
self.audio_features.set_auto_play_after_call(self.auto_play_cb.isChecked())
|
||||
|
||||
answer_audio_file = self.answer_play_edit.text()
|
||||
if answer_audio_file and os.path.exists(answer_audio_file):
|
||||
self.audio_features.set_auto_play_on_answer(
|
||||
self.auto_play_on_answer_cb.isChecked(),
|
||||
answer_audio_file
|
||||
)
|
||||
|
||||
def _create_ui_components(self):
|
||||
"""创建所有UI组件"""
|
||||
# 创建串口选择下拉框
|
||||
self.at_port_combo = QComboBox()
|
||||
self.at_port_combo.setMinimumWidth(120)
|
||||
|
||||
self.at_baud_combo = QComboBox()
|
||||
self.at_baud_combo.setMinimumWidth(100)
|
||||
self.at_baud_combo.addItems(['115200', '9600', '38400', '57600'])
|
||||
|
||||
self.nmea_port_combo = QComboBox()
|
||||
self.nmea_port_combo.setMinimumWidth(120)
|
||||
self.nmea_port_combo.addItem("None")
|
||||
|
||||
self.nmea_baud_combo = QComboBox()
|
||||
self.nmea_baud_combo.setMinimumWidth(100)
|
||||
self.nmea_baud_combo.addItems(['9600', '38400', '57600', '115200'])
|
||||
|
||||
# 创建按钮
|
||||
self.connect_btn = QPushButton("连接")
|
||||
self.connect_btn.clicked.connect(self.toggle_connection)
|
||||
|
||||
# 创建AT命令输入框和发送按钮
|
||||
self.at_command_input = QLineEdit()
|
||||
self.at_command_input.setPlaceholderText("输入AT命令")
|
||||
self.send_btn = QPushButton("发送")
|
||||
self.send_btn.clicked.connect(self.send_at_command)
|
||||
|
||||
# 创建文本显示区域
|
||||
self.at_response_text = QTextEdit()
|
||||
self.at_response_text.setMaximumHeight(150)
|
||||
self.at_response_text.setReadOnly(True)
|
||||
|
||||
self.status_text = QTextEdit()
|
||||
self.status_text.setMinimumHeight(200)
|
||||
self.status_text.setReadOnly(True)
|
||||
|
||||
# 创建复选框
|
||||
self.auto_connect_check = QCheckBox("启动时自动连接")
|
||||
self.auto_connect_check.setChecked(self.settings.get("auto_connect", False))
|
||||
|
||||
# 音频功能组件
|
||||
self.audio_enabled_check = QCheckBox("启用音频功能")
|
||||
self.audio_enabled_check.setChecked(self.settings.get("audio_enabled", True))
|
||||
|
||||
# 自动录音选项
|
||||
self.auto_record_calls_check = QCheckBox("自动录制通话")
|
||||
self.auto_record_calls_check.setChecked(self.settings.get("auto_record_calls", True))
|
||||
self.auto_record_calls_check.setToolTip("接听电话时自动开始录音,通话结束时自动停止")
|
||||
self.auto_record_calls_check.stateChanged.connect(self.on_auto_record_changed)
|
||||
|
||||
# 自动播放录音选项
|
||||
self.auto_play_after_call_check = QCheckBox("通话结束后自动播放录音")
|
||||
self.auto_play_after_call_check.setChecked(self.settings.get("auto_play_after_call", False))
|
||||
self.auto_play_after_call_check.setToolTip("通话结束后自动播放刚录制的通话录音")
|
||||
self.auto_play_after_call_check.stateChanged.connect(self.on_auto_play_changed)
|
||||
|
||||
# 录音路径
|
||||
self.recording_path_input = QLineEdit()
|
||||
# 如果没有设置过录音路径,使用音频特性模块的默认路径
|
||||
if self.audio_features and not self.settings.get("recording_path"):
|
||||
self.settings["recording_path"] = self.audio_features.storage_path
|
||||
self.recording_path_input.setText(self.settings.get("recording_path", ""))
|
||||
|
||||
self.recording_path_btn = QPushButton("浏览...")
|
||||
self.recording_path_btn.clicked.connect(self.browse_recording_path)
|
||||
self.recording_path_reset_btn = QPushButton("重置")
|
||||
self.recording_path_reset_btn.setToolTip("重置为默认路径")
|
||||
self.recording_path_reset_btn.clicked.connect(self.reset_recording_path)
|
||||
|
||||
# 铃声设置
|
||||
self.ringtone_path_input = QLineEdit()
|
||||
self.ringtone_path_input.setText(self.settings.get("ringtone_file", ""))
|
||||
self.ringtone_path_btn = QPushButton("浏览...")
|
||||
self.ringtone_path_btn.clicked.connect(self.browse_ringtone_file)
|
||||
self.set_ringtone_btn = QPushButton("设置铃声")
|
||||
self.set_ringtone_btn.clicked.connect(self.set_ringtone)
|
||||
|
||||
# 录音控制
|
||||
self.start_recording_btn = QPushButton("开始录音")
|
||||
self.start_recording_btn.clicked.connect(self.start_recording)
|
||||
self.stop_recording_btn = QPushButton("停止录音")
|
||||
self.stop_recording_btn.clicked.connect(self.stop_recording)
|
||||
|
||||
# 录音类型选择
|
||||
self.recording_type_combo = QComboBox()
|
||||
self.recording_type_combo.addItems(["本地麦克风", "通话对方声音", "双方声音混合"])
|
||||
|
||||
# 播放音频
|
||||
self.play_audio_input = QLineEdit()
|
||||
self.play_audio_input.setPlaceholderText("输入音频文件名或路径")
|
||||
self.play_audio_btn = QPushButton("播放")
|
||||
self.play_audio_btn.clicked.connect(self.play_audio)
|
||||
self.stop_audio_btn = QPushButton("停止")
|
||||
self.stop_audio_btn.clicked.connect(self.stop_audio)
|
||||
self.browse_audio_btn = QPushButton("浏览...")
|
||||
self.browse_audio_btn.clicked.connect(self.browse_audio_file)
|
||||
|
||||
# 播放类型选择
|
||||
self.play_type_combo = QComboBox()
|
||||
self.play_type_combo.addItems(["本地播放", "远程播放(对方听)", "双方都播放"])
|
||||
|
||||
# 创建音频控制部分
|
||||
self.audio_group = QGroupBox("音频控制")
|
||||
self.audio_layout = QVBoxLayout()
|
||||
|
||||
# 通话录音部分
|
||||
self.recording_section = QGroupBox("通话录音")
|
||||
recording_layout = QVBoxLayout()
|
||||
|
||||
# 自动录制选项
|
||||
self.auto_record_cb = QCheckBox("自动录制通话")
|
||||
self.auto_record_cb.setToolTip("接听电话时自动开始录音,挂断时自动停止")
|
||||
self.auto_record_cb.stateChanged.connect(self.on_auto_record_changed)
|
||||
recording_layout.addWidget(self.auto_record_cb)
|
||||
|
||||
# 录音后自动播放选项
|
||||
self.auto_play_cb = QCheckBox("录音后自动播放")
|
||||
self.auto_play_cb.setToolTip("通话结束后自动播放录音")
|
||||
self.auto_play_cb.stateChanged.connect(self.on_auto_play_changed)
|
||||
recording_layout.addWidget(self.auto_play_cb)
|
||||
|
||||
# 录音路径选择
|
||||
recording_path_layout = QHBoxLayout()
|
||||
self.recording_path_label = QLabel("录音存储路径:")
|
||||
self.recording_path_edit = QLineEdit()
|
||||
self.recording_path_edit.setReadOnly(True)
|
||||
self.browse_recording_btn = QPushButton("浏览...")
|
||||
self.browse_recording_btn.clicked.connect(self.browse_recording_path)
|
||||
self.reset_recording_btn = QPushButton("重置")
|
||||
self.reset_recording_btn.clicked.connect(self.reset_recording_path)
|
||||
|
||||
recording_path_layout.addWidget(self.recording_path_label)
|
||||
recording_path_layout.addWidget(self.recording_path_edit)
|
||||
recording_path_layout.addWidget(self.browse_recording_btn)
|
||||
recording_path_layout.addWidget(self.reset_recording_btn)
|
||||
recording_layout.addLayout(recording_path_layout)
|
||||
|
||||
# 录音控制按钮
|
||||
recording_control_layout = QHBoxLayout()
|
||||
self.start_recording_btn = QPushButton("开始录音")
|
||||
self.start_recording_btn.clicked.connect(self.start_recording)
|
||||
self.stop_recording_btn = QPushButton("停止录音")
|
||||
self.stop_recording_btn.clicked.connect(self.stop_recording)
|
||||
|
||||
recording_control_layout.addWidget(self.start_recording_btn)
|
||||
recording_control_layout.addWidget(self.stop_recording_btn)
|
||||
recording_layout.addLayout(recording_control_layout)
|
||||
|
||||
self.recording_section.setLayout(recording_layout)
|
||||
self.audio_layout.addWidget(self.recording_section)
|
||||
|
||||
# 音频播放部分
|
||||
self.playback_section = QGroupBox("音频播放")
|
||||
playback_layout = QVBoxLayout()
|
||||
|
||||
# 自动接听播放选项
|
||||
self.auto_play_on_answer_cb = QCheckBox("接听电话时自动播放音频")
|
||||
self.auto_play_on_answer_cb.setToolTip("接听电话时自动向对方播放指定音频文件")
|
||||
self.auto_play_on_answer_cb.stateChanged.connect(self.on_auto_play_on_answer_changed)
|
||||
playback_layout.addWidget(self.auto_play_on_answer_cb)
|
||||
|
||||
# 接听播放音频选择
|
||||
answer_play_layout = QHBoxLayout()
|
||||
self.answer_play_label = QLabel("接听播放音频:")
|
||||
self.answer_play_edit = QLineEdit()
|
||||
self.answer_play_edit.setReadOnly(True)
|
||||
self.browse_answer_play_btn = QPushButton("浏览...")
|
||||
self.browse_answer_play_btn.clicked.connect(self.browse_answer_play_file)
|
||||
|
||||
answer_play_layout.addWidget(self.answer_play_label)
|
||||
answer_play_layout.addWidget(self.answer_play_edit)
|
||||
answer_play_layout.addWidget(self.browse_answer_play_btn)
|
||||
playback_layout.addLayout(answer_play_layout)
|
||||
|
||||
# 音频文件选择
|
||||
audio_file_layout = QHBoxLayout()
|
||||
self.audio_file_label = QLabel("音频文件:")
|
||||
self.audio_file_edit = QLineEdit()
|
||||
self.audio_file_edit.setReadOnly(True)
|
||||
self.browse_audio_btn = QPushButton("浏览...")
|
||||
self.browse_audio_btn.clicked.connect(self.browse_audio_file)
|
||||
|
||||
audio_file_layout.addWidget(self.audio_file_label)
|
||||
audio_file_layout.addWidget(self.audio_file_edit)
|
||||
audio_file_layout.addWidget(self.browse_audio_btn)
|
||||
playback_layout.addLayout(audio_file_layout)
|
||||
|
||||
# 播放控制按钮
|
||||
playback_control_layout = QHBoxLayout()
|
||||
self.play_audio_btn = QPushButton("播放音频")
|
||||
self.play_audio_btn.clicked.connect(self.play_audio)
|
||||
self.stop_audio_btn = QPushButton("停止播放")
|
||||
self.stop_audio_btn.clicked.connect(self.stop_audio)
|
||||
|
||||
playback_control_layout.addWidget(self.play_audio_btn)
|
||||
playback_control_layout.addWidget(self.stop_audio_btn)
|
||||
playback_layout.addLayout(playback_control_layout)
|
||||
|
||||
self.playback_section.setLayout(playback_layout)
|
||||
self.audio_layout.addWidget(self.playback_section)
|
||||
|
||||
# 铃声设置部分
|
||||
self.ringtone_section = QGroupBox("铃声设置")
|
||||
ringtone_layout = QVBoxLayout()
|
||||
|
||||
# 铃声文件选择
|
||||
ringtone_file_layout = QHBoxLayout()
|
||||
self.ringtone_file_label = QLabel("铃声文件:")
|
||||
self.ringtone_file_edit = QLineEdit()
|
||||
self.ringtone_file_edit.setReadOnly(True)
|
||||
self.browse_ringtone_btn = QPushButton("浏览...")
|
||||
self.browse_ringtone_btn.clicked.connect(self.browse_ringtone_file)
|
||||
|
||||
ringtone_file_layout.addWidget(self.ringtone_file_label)
|
||||
ringtone_file_layout.addWidget(self.ringtone_file_edit)
|
||||
ringtone_file_layout.addWidget(self.browse_ringtone_btn)
|
||||
ringtone_layout.addLayout(ringtone_file_layout)
|
||||
|
||||
# 铃声设置按钮
|
||||
self.set_ringtone_btn = QPushButton("设置铃声")
|
||||
self.set_ringtone_btn.clicked.connect(self.set_ringtone)
|
||||
ringtone_layout.addWidget(self.set_ringtone_btn)
|
||||
|
||||
self.ringtone_section.setLayout(ringtone_layout)
|
||||
self.audio_layout.addWidget(self.ringtone_section)
|
||||
|
||||
self.audio_group.setLayout(self.audio_layout)
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI布局"""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# 串口设置组
|
||||
serial_group = QGroupBox("串口设置")
|
||||
serial_layout = QGridLayout()
|
||||
|
||||
# 第一行:AT串口和波特率
|
||||
serial_layout.addWidget(QLabel("AT串口:"), 0, 0)
|
||||
serial_layout.addWidget(self.at_port_combo, 0, 1)
|
||||
|
||||
serial_layout.addWidget(QLabel("AT波特率:"), 0, 2)
|
||||
serial_layout.addWidget(self.at_baud_combo, 0, 3)
|
||||
|
||||
# 第二行:NMEA串口和波特率
|
||||
serial_layout.addWidget(QLabel("NMEA串口:"), 1, 0)
|
||||
serial_layout.addWidget(self.nmea_port_combo, 1, 1)
|
||||
|
||||
serial_layout.addWidget(QLabel("NMEA波特率:"), 1, 2)
|
||||
serial_layout.addWidget(self.nmea_baud_combo, 1, 3)
|
||||
|
||||
# 第三行:刷新按钮和自动连接选项
|
||||
refresh_btn = QPushButton("刷新串口")
|
||||
refresh_btn.clicked.connect(self.refresh_ports)
|
||||
serial_layout.addWidget(refresh_btn, 2, 0, 1, 2)
|
||||
|
||||
serial_layout.addWidget(self.auto_connect_check, 2, 2, 1, 2)
|
||||
|
||||
serial_group.setLayout(serial_layout)
|
||||
layout.addWidget(serial_group)
|
||||
|
||||
# 连接按钮
|
||||
layout.addWidget(self.connect_btn)
|
||||
|
||||
# 添加音频控制组
|
||||
layout.addWidget(self.audio_group)
|
||||
|
||||
# AT命令区域
|
||||
at_group = QGroupBox("AT命令")
|
||||
at_layout = QVBoxLayout()
|
||||
|
||||
# AT命令输入区域
|
||||
at_input_layout = QHBoxLayout()
|
||||
at_input_layout.addWidget(self.at_command_input)
|
||||
at_input_layout.addWidget(self.send_btn)
|
||||
at_layout.addLayout(at_input_layout)
|
||||
|
||||
# AT响应显示区域
|
||||
at_layout.addWidget(self.at_response_text)
|
||||
|
||||
at_group.setLayout(at_layout)
|
||||
layout.addWidget(at_group)
|
||||
|
||||
# 状态信息区域
|
||||
status_group = QGroupBox("状态信息")
|
||||
status_layout = QVBoxLayout()
|
||||
status_layout.addWidget(self.status_text)
|
||||
status_group.setLayout(status_layout)
|
||||
layout.addWidget(status_group)
|
||||
|
||||
def on_auto_record_changed(self, state):
|
||||
"""处理自动录制通话选项变更"""
|
||||
is_checked = state == Qt.Checked
|
||||
if self.audio_features:
|
||||
self.audio_features.set_auto_record_calls(is_checked)
|
||||
self.settings["auto_record_calls"] = is_checked
|
||||
self.save_settings()
|
||||
|
||||
def on_auto_play_changed(self, state):
|
||||
"""处理自动播放录音选项变更"""
|
||||
is_checked = state == Qt.Checked
|
||||
if self.audio_features:
|
||||
self.audio_features.set_auto_play_after_call(is_checked)
|
||||
self.settings["auto_play_after_call"] = is_checked
|
||||
self.save_settings()
|
||||
|
||||
def reset_recording_path(self):
|
||||
"""重置录音路径为默认值"""
|
||||
if self.audio_features:
|
||||
# 重置为音频模块的默认路径
|
||||
default_path = self.audio_features.storage_path
|
||||
self.recording_path_edit.setText(default_path)
|
||||
self.audio_features.set_storage_path(default_path)
|
||||
self.settings["recording_path"] = default_path
|
||||
self.save_settings()
|
||||
self.add_status_message(f"录音路径已重置为: {default_path}")
|
||||
|
||||
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 toggle_connection(self):
|
||||
"""切换连接状态"""
|
||||
if not self.lte_manager.is_connected():
|
||||
self.connect()
|
||||
else:
|
||||
self.disconnect()
|
||||
|
||||
def connect(self):
|
||||
"""连接到LTE模块"""
|
||||
at_port = self.at_port_combo.currentText()
|
||||
at_baud = int(self.at_baud_combo.currentText())
|
||||
|
||||
# 旧代码保留在注释中,以便后续可能的恢复
|
||||
# nmea_port = self.nmea_port_combo.currentText()
|
||||
# nmea_baud = int(self.nmea_baud_combo.currentText())
|
||||
# if nmea_port == "None":
|
||||
# nmea_port = ""
|
||||
|
||||
# 使用新的连接方法,只传递AT端口和波特率
|
||||
if self.lte_manager.connect(port=at_port, baudrate=at_baud):
|
||||
self.connect_btn.setText("断开")
|
||||
self.save_settings()
|
||||
|
||||
# 更新音频控件状态
|
||||
if self.audio_features:
|
||||
self.update_audio_controls_state()
|
||||
|
||||
# 如果有设置铃声,自动应用
|
||||
if self.settings.get("ringtone_file") and os.path.exists(self.settings.get("ringtone_file")):
|
||||
self.set_ringtone()
|
||||
|
||||
# 如果有设置录音路径,自动应用
|
||||
if self.settings.get("recording_path") and os.path.exists(self.settings.get("recording_path")):
|
||||
self.audio_features.set_storage_path(self.settings.get("recording_path"))
|
||||
|
||||
# 应用自动录音和自动播放设置
|
||||
self.audio_features.set_auto_record_calls(self.auto_record_cb.isChecked())
|
||||
self.audio_features.set_auto_play_after_call(self.auto_play_cb.isChecked())
|
||||
else:
|
||||
QMessageBox.warning(self, "连接错误", "无法连接到LTE模块")
|
||||
|
||||
def disconnect(self):
|
||||
"""断开LTE模块连接"""
|
||||
self.lte_manager.disconnect()
|
||||
self.connect_btn.setText("连接")
|
||||
|
||||
# 更新音频控件状态
|
||||
if self.audio_features:
|
||||
self.update_audio_controls_state()
|
||||
|
||||
def send_at_command(self):
|
||||
"""发送AT命令"""
|
||||
if not self.lte_manager.is_connected():
|
||||
QMessageBox.warning(self, "错误", "未连接到LTE模块")
|
||||
return
|
||||
|
||||
command = self.at_command_input.text().strip()
|
||||
if not command:
|
||||
return
|
||||
|
||||
self.at_response_text.append(f">>> {command}")
|
||||
response = self.lte_manager.send_at_command(command)
|
||||
if response:
|
||||
self.at_response_text.append(response)
|
||||
self.at_command_input.clear()
|
||||
|
||||
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_text.append(message)
|
||||
self.status_text.ensureCursorVisible()
|
||||
|
||||
def save_settings(self):
|
||||
"""保存设置到文件"""
|
||||
try:
|
||||
# 更新设置字典
|
||||
self.settings["at_port"] = self.at_port_combo.currentText()
|
||||
self.settings["at_baudrate"] = self.at_baud_combo.currentText()
|
||||
self.settings["nmea_port"] = self.nmea_port_combo.currentText()
|
||||
self.settings["nmea_baudrate"] = self.nmea_baud_combo.currentText()
|
||||
self.settings["auto_connect"] = self.auto_connect_check.isChecked()
|
||||
self.settings["recording_path"] = self.recording_path_edit.text()
|
||||
self.settings["auto_record_calls"] = self.auto_record_cb.isChecked()
|
||||
self.settings["auto_play_after_call"] = self.auto_play_cb.isChecked()
|
||||
self.settings["auto_play_on_answer"] = self.auto_play_on_answer_cb.isChecked()
|
||||
self.settings["answer_play_audio_file"] = self.answer_play_edit.text()
|
||||
|
||||
with open(self.settings_file, 'w') as f:
|
||||
json.dump(self.settings, f, indent=4)
|
||||
|
||||
print("设置已保存到", self.settings_file)
|
||||
except Exception as e:
|
||||
print(f"保存设置失败: {str(e)}")
|
||||
|
||||
def try_auto_connect(self):
|
||||
"""尝试自动连接"""
|
||||
if self.auto_connect_check.isChecked():
|
||||
self.connect()
|
||||
|
||||
# 音频功能相关方法
|
||||
def update_audio_controls_state(self):
|
||||
"""更新音频控制按钮的状态"""
|
||||
if not hasattr(self, 'audio_features') or not self.audio_features:
|
||||
return
|
||||
|
||||
connected = self.lte_manager.is_connected()
|
||||
recording = self.audio_features.is_recording() if connected else False
|
||||
playing = self.audio_features.playing if connected else False
|
||||
|
||||
# 启用/禁用录音控制按钮
|
||||
self.start_recording_btn.setEnabled(connected and not recording)
|
||||
self.stop_recording_btn.setEnabled(connected and recording)
|
||||
|
||||
# 启用/禁用播放控制按钮
|
||||
self.play_audio_btn.setEnabled(connected and not playing)
|
||||
self.stop_audio_btn.setEnabled(connected and playing)
|
||||
|
||||
# 铃声设置按钮
|
||||
self.set_ringtone_btn.setEnabled(connected)
|
||||
|
||||
# 自动功能控制
|
||||
self.auto_record_cb.setEnabled(connected)
|
||||
self.auto_play_cb.setEnabled(connected)
|
||||
self.auto_play_on_answer_cb.setEnabled(connected)
|
||||
|
||||
# 路径设置按钮
|
||||
self.browse_recording_btn.setEnabled(True) # 这个不依赖连接状态
|
||||
self.reset_recording_btn.setEnabled(True) # 这个不依赖连接状态
|
||||
self.browse_audio_btn.setEnabled(connected)
|
||||
self.browse_ringtone_btn.setEnabled(connected)
|
||||
self.browse_answer_play_btn.setEnabled(connected)
|
||||
|
||||
def browse_recording_path(self):
|
||||
"""浏览并选择录音存储路径"""
|
||||
current_path = self.recording_path_edit.text() or os.path.expanduser("~")
|
||||
|
||||
dir_path = QFileDialog.getExistingDirectory(
|
||||
self, "选择录音存储路径", current_path
|
||||
)
|
||||
|
||||
if dir_path:
|
||||
self.recording_path_edit.setText(dir_path)
|
||||
if self.audio_features:
|
||||
self.audio_features.set_storage_path(dir_path)
|
||||
self.save_settings()
|
||||
self.add_status_message(f"录音存储路径已设置为: {dir_path}")
|
||||
|
||||
def browse_ringtone_file(self):
|
||||
"""浏览并选择铃声文件"""
|
||||
home_dir = os.path.expanduser("~")
|
||||
file_dialog = QFileDialog()
|
||||
file_path, _ = file_dialog.getOpenFileName(
|
||||
self, "选择铃声文件", home_dir,
|
||||
"音频文件 (*.mp3 *.wav *.amr);;所有文件 (*)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.ringtone_file_edit.setText(file_path)
|
||||
self.save_settings()
|
||||
|
||||
def set_ringtone(self):
|
||||
"""设置铃声"""
|
||||
if not self.audio_features or not self.lte_manager.is_connected():
|
||||
return
|
||||
|
||||
ringtone_file = self.ringtone_file_edit.text()
|
||||
if not ringtone_file:
|
||||
QMessageBox.warning(self, "设置铃声", "请先选择铃声文件")
|
||||
return
|
||||
|
||||
if self.audio_features.set_ringtone(ringtone_file):
|
||||
QMessageBox.information(self, "设置铃声", "铃声设置成功")
|
||||
else:
|
||||
QMessageBox.warning(self, "设置铃声", "铃声设置失败")
|
||||
|
||||
def start_recording(self):
|
||||
"""开始录音"""
|
||||
if not self.audio_features or not self.lte_manager.is_connected():
|
||||
return
|
||||
|
||||
record_type = self.recording_type_combo.currentIndex() + 1 # 1=本地, 2=远程, 3=混合
|
||||
|
||||
if self.audio_features.start_recording(record_path=record_type):
|
||||
self.update_audio_controls_state()
|
||||
self.add_status_message("录音已开始")
|
||||
else:
|
||||
QMessageBox.warning(self, "录音", "开始录音失败")
|
||||
|
||||
def stop_recording(self):
|
||||
"""停止录音"""
|
||||
if not self.audio_features or not self.lte_manager.is_connected():
|
||||
return
|
||||
|
||||
if self.audio_features.stop_recording():
|
||||
self.update_audio_controls_state()
|
||||
self.add_status_message("录音已停止")
|
||||
else:
|
||||
QMessageBox.warning(self, "录音", "停止录音失败")
|
||||
|
||||
def browse_audio_file(self):
|
||||
"""浏览并选择音频文件"""
|
||||
home_dir = os.path.expanduser("~")
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self, "选择音频文件", home_dir,
|
||||
"音频文件 (*.mp3 *.wav *.amr *.pcm);;所有文件 (*)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.audio_file_edit.setText(file_path)
|
||||
|
||||
def play_audio(self):
|
||||
"""播放选择的音频文件"""
|
||||
if not self.lte_manager.is_connected() or not self.audio_features:
|
||||
QMessageBox.warning(self, "播放音频", "未连接到LTE模块或音频功能未启用")
|
||||
return
|
||||
|
||||
audio_file = self.audio_file_edit.text()
|
||||
if not audio_file:
|
||||
# 如果未选择文件,提示用户
|
||||
self.browse_audio_file()
|
||||
audio_file = self.audio_file_edit.text()
|
||||
if not audio_file:
|
||||
return
|
||||
|
||||
play_type = self.play_type_combo.currentIndex()
|
||||
|
||||
success = self.audio_features.play_audio(audio_file, play_type)
|
||||
if success:
|
||||
self.add_status_message(f"开始播放: {os.path.basename(audio_file)}")
|
||||
else:
|
||||
QMessageBox.warning(self, "播放音频", "播放音频失败")
|
||||
|
||||
def stop_audio(self):
|
||||
"""停止播放音频"""
|
||||
if not self.audio_features or not self.lte_manager.is_connected():
|
||||
return
|
||||
|
||||
if self.audio_features.stop_audio():
|
||||
self.update_audio_controls_state()
|
||||
self.add_status_message("音频播放已停止")
|
||||
else:
|
||||
QMessageBox.warning(self, "播放音频", "停止播放失败")
|
||||
|
||||
def on_auto_play_on_answer_changed(self, state):
|
||||
"""处理接听电话自动播放音频选项变更"""
|
||||
enabled = state == Qt.Checked
|
||||
|
||||
if self.audio_features:
|
||||
# 如果启用但未设置音频文件,提示用户选择
|
||||
if enabled and not self.answer_play_edit.text():
|
||||
self.browse_answer_play_file()
|
||||
# 如果用户取消了选择,则取消勾选
|
||||
if not self.answer_play_edit.text():
|
||||
self.auto_play_on_answer_cb.setChecked(False)
|
||||
return
|
||||
|
||||
audio_file = self.answer_play_edit.text() if self.answer_play_edit.text() else None
|
||||
self.audio_features.set_auto_play_on_answer(enabled, audio_file)
|
||||
|
||||
self.save_settings()
|
||||
|
||||
def browse_answer_play_file(self):
|
||||
"""浏览并选择接听时要播放的音频文件"""
|
||||
home_dir = os.path.expanduser("~")
|
||||
file_dialog = QFileDialog()
|
||||
file_path, _ = file_dialog.getOpenFileName(
|
||||
self, "选择音频文件", home_dir,
|
||||
"音频文件 (*.amr *.wav *.mp3 *.pcm);;所有文件 (*)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.answer_play_edit.setText(file_path)
|
||||
|
||||
# 如果音频特性实例存在,更新设置
|
||||
if self.audio_features:
|
||||
self.audio_features.set_auto_play_on_answer(
|
||||
self.auto_play_on_answer_cb.isChecked(),
|
||||
file_path
|
||||
)
|
||||
|
||||
self.save_settings()
|
||||
@@ -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_())
|
||||