# ui_main_window.py
# 在 ui_main_window.py 文件开头添加
import datetime
import sys
from power.power_sync import PowerTableSynchronizer
import os
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QTextEdit, QFileDialog,
QComboBox, QGroupBox, QApplication, QMessageBox, QDialog, QCheckBox
)
from PyQt6.QtCore import QObject, pyqtSignal
import logging
from pathlib import Path
from utils import get_output_dir
print(f" 当前工作目录: {os.getcwd()}")
print(f" 文件所在目录: {Path(__file__).resolve()}")
from excel_to_clm import ExcelToCLMConverter
from channel.range_sync import CLMRangeSynchronizer # 使用统一模块
# ============= 日志重定向器 ==============
class LogEmitter(QObject):
log_signal = pyqtSignal(str)
# ============= 主窗口类 ==============
class CLMGeneratorUI(QWidget):
def __init__(self, generator=None):
super().__init__()
self.generator = generator or ExcelToCLMConverter()
self.log_emitter = LogEmitter()
self.log_emitter.log_signal.connect(self.update_log_area)
# 设置日志系统以捕获同步器输出
self._setup_logging_redirect()
self.setWindowTitle("Wi-Fi CLM Generator Tool v1.2")
self.resize(700, 600)
self.init_ui()
def _setup_logging_redirect(self):
"""仅将 channel.* 日志转发到 UI,不写文件"""
class QtStreamHandler(logging.Handler):
def __init__(self, emitter):
super().__init__()
self.emitter = emitter
self.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
def emit(self, record):
try:
msg = self.format(record)
if msg.strip():
self.emitter.log_signal.emit(msg.strip())
except Exception:
self.handleError(record)
logger = logging.getLogger("channel")
logger.setLevel(logging.INFO)
if logger.hasHandlers():
logger.handlers.clear()
handler = QtStreamHandler(self.log_emitter)
logger.addHandler(handler)
logger.propagate = False
class LogStream:
"""伪装成文件对象的日志流,用于 logging.Handler"""
def __init__(self, emitter):
self.emitter = emitter
def write(self, msg):
if msg.strip():
self.emitter.log_signal.emit(msg.strip())
def flush(self):
pass
def init_ui(self):
layout = QVBoxLayout()
# === 输入区:Excel & Config ===
input_group = QGroupBox("输入配置")
input_layout = QVBoxLayout()
# Excel 文件
excel_row = QHBoxLayout()
excel_row.addWidget(QLabel("Excel 文件:"))
self.excel_path = QLineEdit("input/Archer BE900US 2.xlsx")
self.btn_browse_excel = QPushButton(" 浏览...")
self.btn_browse_excel.clicked.connect(self.select_excel)
excel_row.addWidget(self.excel_path)
excel_row.addWidget(self.btn_browse_excel)
input_layout.addLayout(excel_row)
# Config 文件
config_row = QHBoxLayout()
config_row.addWidget(QLabel("Config 文件:"))
self.config_path = QLineEdit("config/config.json")
self.btn_browse_config = QPushButton(" 浏览...")
self.btn_browse_config.clicked.connect(self.select_config_file)
config_row.addWidget(self.config_path)
config_row.addWidget(self.btn_browse_config)
input_layout.addLayout(config_row)
# Locale
locale_row = QHBoxLayout()
locale_row.addWidget(QLabel("Locale ID:"))
self.locale_combo = QComboBox()
self.locale_combo.addItems(["US", "FCC", "EU", "JP", "CN"])
locale_row.addWidget(self.locale_combo)
input_layout.addLayout(locale_row)
input_group.setLayout(input_layout)
layout.addWidget(input_group)
# === CLM 同步区 ===
sync_group = QGroupBox("CLM 范围同步")
sync_layout = QVBoxLayout()
# C 文件
cfile_row = QHBoxLayout()
cfile_row.addWidget(QLabel("C 源文件:"))
self.c_file_path = QLineEdit("input/wlc_clm_data_6726b0.c")
self.btn_browse_cfile = QPushButton(" 浏览...")
self.btn_browse_cfile.clicked.connect(self.select_c_file)
cfile_row.addWidget(self.c_file_path)
cfile_row.addWidget(self.btn_browse_cfile)
sync_layout.addLayout(cfile_row)
sync_group.setLayout(sync_layout)
layout.addWidget(sync_group)
# === 操作流程按钮组 ===
btn_group = QGroupBox("操作流程控制")
btn_layout = QHBoxLayout()
self.btn_parse = QPushButton(" 解析 Excel")
self.btn_dry_run = QPushButton(" 预览同步")
self.btn_sync = QPushButton(" 执行同步")
self.btn_full_run = QPushButton(" 一键生成并同步")
self.btn_parse.clicked.connect(self.run_parse)
self.btn_dry_run.clicked.connect(lambda: self.run_range_sync(dry_run=True))
self.btn_sync.clicked.connect(lambda: self.run_range_sync(dry_run=False))
self.btn_full_run.clicked.connect(self.run_full_process)
btn_layout.addWidget(self.btn_parse)
btn_layout.addWidget(self.btn_dry_run)
btn_layout.addWidget(self.btn_sync)
btn_layout.addWidget(self.btn_full_run)
btn_group.setLayout(btn_layout)
layout.addWidget(btn_group)
# === 功率表同步区 ===
power_sync_group = QGroupBox("功率表 & 国家码同步")
power_sync_layout = QVBoxLayout()
# 添加预览模式开关
self.chk_dry_run = QCheckBox(" 启用预览模式(不修改文件)")
self.chk_dry_run.setChecked(True) # 默认开启预览,更安全
power_sync_layout.addWidget(self.chk_dry_run)
# Locale 枚举选择(可多选)
locale_power_row = QHBoxLayout()
locale_power_row.addWidget(QLabel("默认Locale:"))
self.power_locale_combo = QComboBox()
self.power_locale_combo.addItems(["DEFAULT", "FCC", "ETSI", "JP", "CN"])
locale_power_row.addWidget(self.power_locale_combo)
power_sync_layout.addLayout(locale_power_row)
# 执行按钮
self.btn_sync_power = QPushButton("⚡ 注入功率表与国家码")
self.btn_sync_power.clicked.connect(self.run_power_sync)
power_sync_layout.addWidget(self.btn_sync_power)
power_sync_group.setLayout(power_sync_layout)
layout.addWidget(power_sync_group)
# === 日志输出 ===
self.log_area = QTextEdit()
self.log_area.setReadOnly(True)
self.log_area.append(" CLM Generator 已启动,等待输入...")
layout.addWidget(self.log_area)
self.setLayout(layout)
def run_power_sync(self):
from datetime import datetime
"""运行功率表同步:提取 tx_limit_table.c 中的 DEFAULT 数据,注入到目标 .c 文件"""
target_c_path = self.c_file_path.text().strip()
if not target_c_path:
self.log(" 未指定目标 C 文件,请先设置!")
return
generated_table_path = Path("output") / "tx_limit_table.c"
config_path = self.config_path.text().strip() or "config/config.json"
if not generated_table_path.exists():
self.log(f" 无法找到生成的功率表文件: {generated_table_path}")
self.log(" 请先点击【开始生成】以生成 tx_limit_table.c")
return
# ========== 【新增】准备日志文件(仅在执行模式)==========
file_handler = None
log_file_path = None
dry_run = self.chk_dry_run.isChecked()
if not dry_run: # 只有执行模式才记录到文件
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_dir = Path(get_output_dir()) / "log"
log_dir.mkdir(parents=True, exist_ok=True)
log_file_path = log_dir / f"power_sync_{timestamp}.log"
# 创建专用 FileHandler
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
file_handler.setFormatter(
logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%H:%M:%S')
)
# 添加到 power 模块 logger(与 UI 输出共存)
target_logger = logging.getLogger("power")
target_logger.addHandler(file_handler)
self.log(f" 日志文件已创建: {log_file_path}")
try:
self.log(" 正在读取生成的功率表数据...")
gen_content = generated_table_path.read_text(encoding='utf-8')
# 提取 DEFAULT 功率表(Base 和 HT)
base_block = PowerTableSynchronizer._extract_locale_block(gen_content, "DEFAULT")
ht_block = PowerTableSynchronizer._extract_locale_block(gen_content, "DEFAULT_HT")
if not base_block and not ht_block:
self.log(" 未提取到任何 DEFAULT 功率表数据(检查是否生成了正确的 Locale)")
return
# 构造 locale_entries
selected_locale_idx = self.power_locale_combo.currentText()
idx_name = f"LOCALE_2G_IDX_{selected_locale_idx}"
locale_entries = {
"locale_2g_idx": idx_name
}
# 构造 power_tables
prefix = "locales_"
suffix = "[MAX_STREAMS_SUPPORTED][MODULATION_MODES]"
power_tables = {}
if base_block:
key = "2g_base_DEFAULT"
declaration = (
f"static const unsigned char {prefix}2g_base_DEFAULT[] = {{\n {base_block}\n}}"
)
power_tables[key] = declaration
self.log(f" 准备注入 2G Base 表")
if ht_block:
key = "2g_ht_DEFAULT"
declaration = (
f"static const unsigned char {prefix}2g_ht_DEFAULT[] = {{\n {ht_block}\n}}"
)
power_tables[key] = declaration
self.log(f" 准备注入 HT 表")
# 构造 country_entries(示例)
country_entries = [
f'REGION("CN", {idx_name})',
f'REGION("US", {idx_name})',
f'REGION("EU", {idx_name})'
]
self.log(f" 将为 CN/US/EU 注入国家码映射至 {idx_name}")
# === 关键:从复选框获取 dry_run 状态 ===
dry_run = self.chk_dry_run.isChecked()
# 创建同步器并执行
synchronizer = PowerTableSynchronizer(
config_path=config_path,
dry_run=dry_run # 可改为 True 测试
)
self.log(" 开始注入功率表和国家码...")
synchronizer.sync_all(
locale_entries=locale_entries,
power_tables=power_tables,
country_entries=country_entries
)
self.log(" 功率表与国家码同步完成!")
QMessageBox.information(self, "成功", "功率表与国家码已成功注入目标C文件!")
self.log(f"完整日志已保存至:\n{log_file_path}")
except Exception as e:
self.log(f" 功率表同步失败: {str(e)}")
import traceback
self.log(f" 错误详情:\n{traceback.format_exc()}")
QMessageBox.critical(self, "错误", f"同步过程出错:{str(e)}")
def run_parse(self):
"""解析 Excel 并生成 manifest.json,不清除 generator 实例"""
excel_path = self.excel_path.text().strip()
config_path = self.config_path.text().strip() or "config/config.json"
if not excel_path:
self.log(" 请先选择 Excel 文件!")
return
try:
# 复用已有 generator,仅更新参数
self.generator.config_file_path = config_path
self.generator.locale_display_name = self.locale_combo.currentText()
self.generator.input_file = excel_path
self.log(" 清理旧数据...")
self.generator.reset() # 统一调用 reset 方法
self.log(" 开始解析 Excel 文件...")
self.generator.parse_excel()
self.generator.generate_outputs(finalize_config=True)
num_ranges = len(self.generator.used_ranges)
manifest_path = self.generator.last_config
self.log(f" 成功生成 {num_ranges} 个 RANGE 宏 → {manifest_path}")
self.log(" 提示:现在可以点击【预览同步】查看对 .c 文件的影响")
except Exception as e:
self.log(f" 解析失败: {str(e)}")
import traceback
self.log(f" 错误详情:\n{traceback.format_exc()}")
def select_excel(self):
file, _ = QFileDialog.getOpenFileName(
self, "选择 Excel 文件", "", "Excel Files (*.xlsx *.xls)")
if file:
self.excel_path.setText(file)
def select_config_file(self):
file, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "config/", "JSON 配置文件 (*.json);;All Files (*)")
if file:
self.config_path.setText(file)
def select_c_file(self):
file, _ = QFileDialog.getOpenFileName(
self, "选择 C 源文件", "input/", "C Source Files (*.c)")
if file:
self.c_file_path.setText(file)
def log(self, msg):
"""发送日志消息到信号队列"""
self.log_emitter.log_signal.emit(msg)
def update_log_area(self, msg):
"""更新日志文本框"""
self.log_area.append(f"> {msg}")
def clear_log(self):
self.log_area.clear()
def show_preview(self):
try:
output_file = get_output_dir()
with open(output_file, "r", encoding="utf-8") as f:
content = f.read()
dialog = QDialog(self)
dialog.setWindowTitle(f"预览: {output_file}")
dialog.resize(700, 500)
layout = QVBoxLayout()
text_edit = QTextEdit()
text_edit.setReadOnly(True)
text_edit.setText(content[:2000] + "\n...(后续内容已截断)")
close_btn = QPushButton("关闭")
close_btn.clicked.connect(dialog.accept)
layout.addWidget(text_edit)
layout.addWidget(close_btn)
dialog.setLayout(layout)
dialog.exec()
except FileNotFoundError:
self.log(f" 无法预览: 文件未生成 '{output_file}',请先点击【开始生成】")
except Exception as e:
self.log(f" 预览失败: {str(e)}")
def run_range_sync(self, dry_run=False):
"""运行同步:使用 generator 生成的 manifest 路径自动同步到 C 文件"""
from pathlib import Path
import logging
from datetime import datetime
# 检查是否已生成 manifest
if not hasattr(self.generator, 'last_config') or not self.generator.last_config:
self.log(" 尚未生成任何 文件,请先点击【解析 Excel】")
return
# 默认使用自动生成的路径,允许用户手动覆盖
c_file_path = self.c_file_path.text().strip()
if not c_file_path:
self.log(" 未指定目标 C 文件路径,请在下方输入或选择")
return
# 验证文件存在性
if not Path(c_file_path).exists():
self.log(f" 目标 C 文件不存在: {c_file_path}")
return
# ========== 【新增】准备日志文件(仅在执行模式)==========
file_handler = None
log_file_path = None
if not dry_run: # 只有执行模式才记录到文件
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_dir = Path(get_output_dir()) / "log" # 先转成 Path
log_dir.mkdir(parents=True, exist_ok=True)
log_file_path = log_dir / f"range_sync_{timestamp}.log"
# 创建专用 FileHandler
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
file_handler.setFormatter(
logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%H:%M:%S')
)
# 添加到 channel logger(与 UI 输出共存)
target_logger = logging.getLogger("channel")
target_logger.addHandler(file_handler)
self.log(f" 日志文件已创建: {log_file_path}")
try:
mode = "预览模式" if dry_run else "执行模式"
self.log(f" 开始同步... ({mode})")
self.log(f" 修改文件: {c_file_path}")
# 创建同步器实例
sync = CLMRangeSynchronizer(
c_file_path=c_file_path,
dry_run=dry_run
)
was_modified = sync.run()
if dry_run:
if was_modified:
self.log("发现需要更新的内容,确认无误后可点击【执行同步】")
else:
self.log("所有 RANGE 已存在,无需修改")
else:
if was_modified:
self.log(f"同步成功!已更新 {c_file_path}")
self.log(f"备份已保存为 {c_file_path}.bak")
else:
self.log("文件已是最新,无需修改")
except Exception as e:
self.log(f"同步失败: {str(e)}")
import traceback
tb_msg = traceback.format_exc()
self.log(f"错误详情:\n{tb_msg}")
# 即使出错也要确保 file_handler 正确移除
raise
finally:
# ========== 【关键】清理资源:移除并关闭 FileHandler ==========
if file_handler:
try:
# 确保缓冲区写入磁盘
file_handler.flush()
file_handler.close()
logging.getLogger("channel").removeHandler(file_handler)
self.log("日志文件已关闭")
except Exception as e:
print(f"Warning: failed to close file handler: {e}")
# 可选:在 UI 显示最终提示
if not dry_run and log_file_path:
self.log(f"完整日志已保存至:\n{log_file_path}")
def run_full_process(self):
"""一键完成:解析 Excel → 生成 manifest → 同步到 .c 文件"""
reply = QMessageBox.question(
self,
"确认执行",
"是否要执行【解析Excel + 更新C文件】全流程?\n\n"
"建议先使用【预览同步】检查变更。",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply != QMessageBox.StandardButton.Yes:
return
self.run_parse()
if hasattr(self.generator, 'last_config') and self.generator.last_config:
self.run_range_sync(dry_run=False)
else:
self.log(" 解析失败,无法继续同步")
if __name__ == '__main__':
app = QApplication([])
try:
window = CLMGeneratorUI()
window.show()
sys.exit(app.exec())
except Exception as e:
msg = QMessageBox()
msg.setWindowTitle("严重错误")
msg.setText(f"程序启动失败:\n\n{type(e).__name__}: {e}\n\n详情查看日志或联系开发者。")
msg.setIcon(QMessageBox.Critical)
msg.exec()
print(f"Critical error: {e}")
找到要修改的地方