BUG-005: 模板搜索路径优先查找 Code/src/Template/ 目录 BUG-006: 上边 Name 移至 row 0,完全独立于其他边 - 37/37 测试全部通过 - docs: 更新 bugs.md(BUG-005/BUG-006 状态) - docs: 更新 tasks.md(T028 打包进行中→已完成) - docs: 添加 modification-assessment-v1.5.5.md - CHANGELOG.md: 追加 v1.5.5 版本日志
394 lines
15 KiB
Python
394 lines
15 KiB
Python
"""PinMAP ↔ PinList bidirectional converter
|
||
|
||
Usage:
|
||
python main.py # Interactive — choose direction + file
|
||
python main.py input.xls # MAP→List mode (legacy, specify file directly)
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
|
||
|
||
# ── Banner ──────────────────────────────────────────────────────────
|
||
|
||
def show_banner():
|
||
"""显示程序启动说明"""
|
||
print("=" * 60)
|
||
print(" PinMAP ↔ PinList 双向转换器")
|
||
print(" 支持 PinMAP→PinList 与 PinList→PinMAP 互转")
|
||
print(" 支持.xls和.xlsx格式,输出.xlsx格式")
|
||
print("=" * 60)
|
||
print()
|
||
|
||
|
||
def wait_for_exit():
|
||
"""等待用户按键后退出(Windows任意键,其他平台Enter键)"""
|
||
try:
|
||
import msvcrt
|
||
print("按任意键退出...")
|
||
msvcrt.getch()
|
||
except ImportError:
|
||
input("按Enter键退出...")
|
||
|
||
|
||
# ── Path helpers ────────────────────────────────────────────────────
|
||
|
||
def _find_pinlist_template_path() -> str | None:
|
||
"""查找 PinList-Template.xlsx。
|
||
|
||
MAP→List 输出使用 PinList 模板。
|
||
搜索顺序:
|
||
1. Code/src/Template/ 目录(首要位置)
|
||
2. 项目根目录(向后兼容)
|
||
3. 当前工作目录
|
||
"""
|
||
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||
# 1. Code/src/Template/ 目录
|
||
template_path = os.path.join(src_dir, "Template", "PinList-Template.xlsx")
|
||
if os.path.exists(template_path):
|
||
return template_path
|
||
# 2. 项目根目录(向后兼容)
|
||
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||
template_path = os.path.join(root_dir, "PinList-Template.xlsx")
|
||
if os.path.exists(template_path):
|
||
return template_path
|
||
# 3. 当前工作目录
|
||
cwd_template = os.path.join(os.getcwd(), "PinList-Template.xlsx")
|
||
if os.path.exists(cwd_template):
|
||
return cwd_template
|
||
return None
|
||
|
||
|
||
def _find_pinmap_template_path() -> str | None:
|
||
"""查找 PinMAP-Template.xlsx。
|
||
|
||
List→MAP 输出使用 PinMAP 模板。
|
||
搜索顺序:
|
||
1. Code/src/Template/ 目录(首要位置)
|
||
2. 项目根目录(向后兼容)
|
||
3. 当前工作目录
|
||
"""
|
||
src_dir = os.path.dirname(os.path.abspath(__file__))
|
||
# 1. Code/src/Template/ 目录
|
||
template_path = os.path.join(src_dir, "Template", "PinMAP-Template.xlsx")
|
||
if os.path.exists(template_path):
|
||
return template_path
|
||
# 2. 项目根目录(向后兼容)
|
||
root_dir = os.path.dirname(os.path.dirname(src_dir))
|
||
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
|
||
if os.path.exists(template_path):
|
||
return template_path
|
||
# 3. 当前工作目录
|
||
cwd_template = os.path.join(os.getcwd(), "PinMAP-Template.xlsx")
|
||
if os.path.exists(cwd_template):
|
||
return cwd_template
|
||
return None
|
||
|
||
|
||
def _build_output_path_map_to_list(input_path: str) -> str:
|
||
"""Generate output path: {original_filename}_PinList.xlsx"""
|
||
base, _ = os.path.splitext(input_path)
|
||
return f"{base}_PinList.xlsx"
|
||
|
||
|
||
def _build_output_path_list_to_map(input_path: str) -> str:
|
||
"""Generate output path: {original_filename}_PinMAP.xlsx"""
|
||
base, _ = os.path.splitext(input_path)
|
||
return f"{base}_PinMAP.xlsx"
|
||
|
||
|
||
# ── Direction 1: MAP → List ────────────────────────────────────────
|
||
|
||
def run_map_to_list(filepath: str):
|
||
"""执行 PinMAP → PinList 转换流程。"""
|
||
from file_selector import select_file
|
||
from xls_reader import read_excel_cells
|
||
from xlsx_reader import read_excel_cells as read_xlsx_cells
|
||
from pinmap_parser import parse_pinmap
|
||
from validator import validate_pinmap
|
||
from pinlist_generator import generate_pinlist
|
||
from xlsx_writer import write_xlsx, write_xlsx_with_style
|
||
from template_reader import read_template_styles
|
||
from models import FileFormatError, StructureError
|
||
|
||
# ── 1. File selection ───────────────────────────────────────────
|
||
if not filepath:
|
||
filepath = select_file(mode="map_to_list")
|
||
|
||
if not filepath:
|
||
print("未选择文件,退出。")
|
||
wait_for_exit()
|
||
return
|
||
|
||
# ── 2. Read Excel ───────────────────────────────────────────────
|
||
print(f"[INFO] 正在读取文件: {filepath}")
|
||
try:
|
||
if filepath.lower().endswith('.xlsx'):
|
||
cells = read_xlsx_cells(filepath)
|
||
else:
|
||
cells = read_excel_cells(filepath)
|
||
except Exception as e:
|
||
print(f"[FATAL] 文件读取失败: {e}")
|
||
wait_for_exit()
|
||
return
|
||
|
||
print(f"[INFO] 文件读取完成,共 {len(cells)} 个非空单元格")
|
||
|
||
# ── 3. Parse PinMAP ─────────────────────────────────────────────
|
||
print("[INFO] 正在解析 PinMAP 结构...")
|
||
try:
|
||
pinmap = parse_pinmap(cells)
|
||
print(f"[INFO] 解析完成: {pinmap.width}x{pinmap.height} 方形,共 {len(pinmap.pins)} 个Pin")
|
||
print(f"[INFO] 封装信息: {pinmap.package_info}")
|
||
except (FileFormatError, StructureError) as e:
|
||
print(f"[FATAL] 结构错误: {e}")
|
||
wait_for_exit()
|
||
return
|
||
|
||
# ── 4. Validate ─────────────────────────────────────────────────
|
||
print("[INFO] 正在验证数据...")
|
||
validation = validate_pinmap(pinmap)
|
||
|
||
if validation.errors:
|
||
print(f"[ERROR] 验证未通过,发现 {len(validation.errors)} 个错误:")
|
||
for err in validation.errors:
|
||
print(f" - {err.message}: {err.details}")
|
||
print("\n转换终止,请修正PinMAP文件后重试。")
|
||
wait_for_exit()
|
||
return
|
||
|
||
if validation.warnings:
|
||
print(f"[WARN] 发现 {len(validation.warnings)} 个警告:")
|
||
for warn in validation.warnings:
|
||
print(f" - {warn.message}: {warn.details}")
|
||
else:
|
||
print("[INFO] 验证通过")
|
||
|
||
# ── 5. Generate PinList ─────────────────────────────────────────
|
||
print("[INFO] 正在生成 PinList...")
|
||
pinlist = generate_pinlist(pinmap, validation)
|
||
|
||
# ── 6. Write XLSX ───────────────────────────────────────────────
|
||
output_path = _build_output_path_map_to_list(filepath)
|
||
print(f"[INFO] 正在写入输出文件: {output_path}")
|
||
try:
|
||
data = {}
|
||
data['A1'] = pinlist.package_info
|
||
for i, (pin_name, pin_num) in enumerate(pinlist.rows):
|
||
row = i + 2
|
||
data[f'A{row}'] = pin_name
|
||
data[f'B{row}'] = str(pin_num)
|
||
|
||
# 尝试读取 PinList 模板样式
|
||
template_path = _find_pinlist_template_path()
|
||
template_style = None
|
||
if template_path:
|
||
template_style = read_template_styles(template_path)
|
||
if template_style:
|
||
print(f"[INFO] 已加载 PinList 模板样式: {template_path}")
|
||
else:
|
||
print("[WARN] PinList 模板文件存在但解析失败,使用默认样式")
|
||
else:
|
||
print("[INFO] 未检测到 PinList-Template.xlsx,使用默认样式")
|
||
|
||
if template_style is not None:
|
||
write_xlsx_with_style(data, output_path, template_style)
|
||
else:
|
||
write_xlsx(data, output_path)
|
||
except Exception as e:
|
||
print(f"[FATAL] 输出失败: {e}")
|
||
wait_for_exit()
|
||
return
|
||
|
||
# ── 7. Result summary ───────────────────────────────────────────
|
||
print()
|
||
print("[SUCCESS] 转换完成!")
|
||
print(f" 输出文件: {output_path}")
|
||
print(f" 封装信息: {pinlist.package_info}")
|
||
print(f" Pin数量: {len(pinlist.rows)}")
|
||
|
||
# F008: 不再退出,返回主循环
|
||
|
||
|
||
# ── Direction 2: List → MAP ────────────────────────────────────────
|
||
|
||
def run_list_to_map(filepath: str):
|
||
"""执行 PinList → PinMAP 转换流程。"""
|
||
from file_selector import select_file
|
||
from pinlist_parser import parse_pinlist
|
||
from pinlist_validator import validate_pinlist
|
||
from pinmap_generator import generate_pinmap, generate_output_path
|
||
from template_reader import read_template_styles
|
||
from models import StructureError, LayoutError
|
||
|
||
# ── 1. File selection ───────────────────────────────────────────
|
||
if not filepath:
|
||
filepath = select_file(mode="list_to_map")
|
||
|
||
if not filepath:
|
||
print("未选择文件,退出。")
|
||
wait_for_exit()
|
||
return
|
||
|
||
# ── 2. Input PinMAP dimensions ──────────────────────────────────
|
||
while True:
|
||
try:
|
||
rows_input = input("请输入 PinMAP 行数: ").strip()
|
||
rows = int(rows_input)
|
||
if rows < 2:
|
||
print("[ERROR] 行数至少为 2")
|
||
continue
|
||
break
|
||
except ValueError:
|
||
print("[ERROR] 请输入有效的整数")
|
||
|
||
while True:
|
||
try:
|
||
cols_input = input("请输入 PinMAP 列数: ").strip()
|
||
cols = int(cols_input)
|
||
if cols < 2:
|
||
print("[ERROR] 列数至少为 2")
|
||
continue
|
||
break
|
||
except ValueError:
|
||
print("[ERROR] 请输入有效的整数")
|
||
|
||
print(f"[INFO] PinMAP 尺寸: {rows} 行 × {cols} 列")
|
||
|
||
# ── 3. Parse PinList ────────────────────────────────────────────
|
||
print(f"[INFO] 正在解析 PinList 文件: {filepath}")
|
||
try:
|
||
package_info, entries = parse_pinlist(filepath)
|
||
print(f"[INFO] 解析完成: 封装信息 '{package_info}', 共 {len(entries)} 个引脚")
|
||
except StructureError as e:
|
||
print(f"[FATAL] 解析失败: {e}")
|
||
wait_for_exit()
|
||
return
|
||
|
||
# ── 4. Validate ─────────────────────────────────────────────────
|
||
print("[INFO] 正在验证数据...")
|
||
validation = validate_pinlist(entries, rows, cols)
|
||
|
||
if validation.errors:
|
||
print(f"[ERROR] 验证未通过,发现 {len(validation.errors)} 个错误:")
|
||
for err in validation.errors:
|
||
print(f" - {err.message}: {err.details}")
|
||
print("\n转换终止,请修正PinList文件或网格尺寸后重试。")
|
||
wait_for_exit()
|
||
return
|
||
|
||
if validation.warnings:
|
||
print(f"[WARN] 发现 {len(validation.warnings)} 个警告:")
|
||
for warn in validation.warnings:
|
||
print(f" - {warn.message}: {warn.details}")
|
||
else:
|
||
print("[INFO] 验证通过")
|
||
|
||
# ── 5. Generate PinMAP ──────────────────────────────────────────
|
||
output_path = generate_output_path(filepath)
|
||
print(f"[INFO] 正在生成 PinMAP 并写入: {output_path}")
|
||
|
||
try:
|
||
# 尝试读取 PinMAP 模板样式
|
||
template_path = _find_pinmap_template_path()
|
||
template_style = None
|
||
if template_path:
|
||
template_style = read_template_styles(template_path)
|
||
if template_style:
|
||
print(f"[INFO] 已加载 PinMAP 模板样式: {template_path}")
|
||
else:
|
||
print("[WARN] PinMAP 模板文件存在但解析失败,使用默认样式")
|
||
else:
|
||
print("[INFO] 未检测到 PinMAP-Template.xlsx,使用默认样式")
|
||
|
||
generate_pinmap(
|
||
entries=entries,
|
||
rows=rows,
|
||
cols=cols,
|
||
package_info=package_info,
|
||
template_style=template_style,
|
||
output_path=output_path,
|
||
)
|
||
except LayoutError as e:
|
||
print(f"[FATAL] 布局计算失败: {e}")
|
||
wait_for_exit()
|
||
return
|
||
except Exception as e:
|
||
print(f"[FATAL] 输出失败: {e}")
|
||
wait_for_exit()
|
||
return
|
||
|
||
# ── 6. Result summary ───────────────────────────────────────────
|
||
print()
|
||
print("[SUCCESS] 转换完成!")
|
||
print(f" 输出文件: {output_path}")
|
||
print(f" 封装信息: {package_info}")
|
||
print(f" PinMAP 尺寸: {rows}×{cols}")
|
||
print(f" Pin数量: {len(entries)}")
|
||
|
||
# F008: 不再退出,返回主循环
|
||
|
||
|
||
# ── Main entry ──────────────────────────────────────────────────────
|
||
|
||
def main():
|
||
show_banner()
|
||
|
||
# F008: 循环处理流程
|
||
while True:
|
||
# ── Direction selection ─────────────────────────────────────
|
||
if len(sys.argv) > 1:
|
||
# Legacy mode: direct file argument → MAP→List
|
||
direction = 1
|
||
filepath = sys.argv[1]
|
||
sys.argv = [sys.argv[0]] # 清除 argv,下次循环进入交互模式
|
||
else:
|
||
print("请选择转换方向:")
|
||
print(" 1 — PinMAP → PinList")
|
||
print(" 2 — PinList → PinMAP")
|
||
print(" Q — 退出程序")
|
||
print()
|
||
|
||
choice = input("请输入选项 (1/2/Q): ").strip().upper()
|
||
if choice == 'Q':
|
||
print("感谢使用,再见!")
|
||
return
|
||
elif choice == '1':
|
||
direction = 1
|
||
elif choice == '2':
|
||
direction = 2
|
||
else:
|
||
print("[ERROR] 无效选项,请输入 1、2 或 Q")
|
||
continue
|
||
|
||
filepath = None
|
||
|
||
# ── Dispatch ────────────────────────────────────────────────
|
||
if direction == 1:
|
||
print()
|
||
print("─" * 40)
|
||
print(" 方向: PinMAP → PinList")
|
||
print("─" * 40)
|
||
print()
|
||
run_map_to_list(filepath)
|
||
else:
|
||
print()
|
||
print("─" * 40)
|
||
print(" 方向: PinList → PinMAP")
|
||
print("─" * 40)
|
||
print()
|
||
run_list_to_map(filepath)
|
||
|
||
# ── 处理完成后循环 ──────────────────────────────────────────
|
||
print()
|
||
print("=" * 40)
|
||
next_choice = input("输入 Q 退出,或按 Enter 返回主菜单继续转换: ").strip().upper()
|
||
if next_choice == 'Q':
|
||
print("感谢使用,再见!")
|
||
return
|
||
# 否则继续 while 循环,回到主菜单
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|