重构前的状态

OTA 桌面工具最初是一个单体 WPF 项目,所有代码堆在一个 .csproj 里:

文件承担的职责
MainWindow.xaml.csUI 事件、串口列表刷新、PowerShell 调用、槽位识别、升级流程编排、日志输出
UpgradeAbSupport.cs模型定义、镜像识别规则、Modbus 读槽位协议逻辑
LocalUpgradeService.cs升级编排、YMODEM 发包逻辑、CRC 算法、固件摘要计算
App.xaml通过 StartupUri 启动主窗口,无依赖注入

这种”大泥球”结构导致几个问题:改一个功能要跨多个文件翻找,无法单独测试协议层,UI 逻辑和业务逻辑深度耦合。

目标架构

按职责拆分为 5 个项目,依赖方向单向:

Models ← Protocols ← Core ← ViewModels ← UI
项目职责依赖
OTA.Models纯数据实体、枚举、事件参数
OTA.Protocols串口通道、Modbus 协议、YMODEM 包结构、CRC 算法Models
OTA.Core升级业务编排、槽位与镜像校验、任务流程控制Models, Protocols
OTA.ViewModels界面状态管理、命令绑定、属性通知Models, Core
OTA.UIWPF 启动层、XAML 视图、DI 装配、资源全部

分阶段实施

Phase 1:物理拆分(不引入 MVVM)

目标:把底层和核心逻辑从 WPF 项目中剥离,但 UI 仍保持事件驱动。

Models 层:抽出的纯数据类型

  • FirmwareSlot:A/B 槽位枚举
  • FirmwareImageInfo:镜像文件元数据(路径、大小、CRC32、SHA256、向量表地址)
  • LocalUpgradeOptions:升级参数(串口、超时、镜像路径)
  • LocalSerialSettingsPortOptionRunningSlotRefreshResultLocalUpgradePreparation 等跨层数据对象

Protocols 层:通信协议实现

  • SerialPortHelper:串口枚举与打开
  • SerialOperationGate:带锁的串口操作门控
  • ModbusRawFrameBuilder/Parser:Modbus RTU 帧构造与解析
  • ModbusCrc16:Modbus CRC16 计算
  • RunningSlotProtocol:读取寄存器 0x005A 判断当前运行槽位
  • YModemProtocol:YMODEM 包构造、CRC16-CCITT、控制字节处理
  • LocalUpgradeTransport:YMODEM 发送流程

Core 层:业务编排

  • PortDiscoveryService:串口枚举与设备描述查询
  • FirmwareSignatureService:固件签名与校验
  • LocalUpgradeCoordinator:升级前参数校验、镜像识别、槽位探测结果整合、推荐镜像路径判断
  • LocalUpgradeService:完整升级流程(解锁 → 进 Bootloader → YMODEM 传输 → 等待完成)
  • RemoteOtaControlService:远程升级控制
  • OtaErrorMapper:错误码映射

UI 层:瘦身后的 WPF 壳

MainWindow.xaml.cs 在 Phase 1 结束后只保留:事件入口、读取界面输入、调用 Core、回填界面状态、日志桥接。

Phase 2:DI 容器接入

去掉 StartupUri 直启窗口方式,在 App.xaml.cs 中建立 ServiceCollection

// App.xaml.cs
services.AddSingleton<PortDiscoveryService>();
services.AddSingleton<LocalUpgradeCoordinator>();
services.AddSingleton<MainViewModel>();
services.AddTransient<MainWindow>();

由容器创建 MainWindow,为后续 MVVM 做准备。

Phase 3:MVVM 化

引入 CommunityToolkit.Mvvm,创建 MainViewModel

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty] private bool _isBusy;
    [ObservableProperty] private string _statusText = "";
    [ObservableProperty] private ObservableCollection<PortOption> _ports = new();
    [ObservableProperty] private string _currentSlotText = "";
    [ObservableProperty] private string _recommendedImageText = "";
    [ObservableProperty] private string _imagePath = "";
    [ObservableProperty] private string _logText = "";

    [RelayCommand]
    private async Task StartUpgradeAsync() { ... }
}

MainWindow.xaml 改为 TabControl 壳层,通过两个 TabItem 组装 LocalUpgradeViewRemoteUpgradeView

<TabControl>
    <TabItem Header="本地升级">
        <views:LocalUpgradeView />
    </TabItem>
    <TabItem Header="远程维护">
        <views:RemoteUpgradeView />
    </TabItem>
</TabControl>

MainWindow.xaml.cs 最终收缩为应用外壳代码,只保留设备变更消息、空闲轮询定时器和窗口生命周期处理。

Phase 4:清理与增强

在结构稳定后做命名统一、接口抽象、单元测试补充。

重构原则

  1. 先物理解耦,后 UI 解耦:先拆项目边界,再引入 MVVM
  2. 先保持行为一致,再追求结构优雅:协议常量、串口帧、超时、寄存器地址不改语义
  3. 每一阶段都必须可编译:不出现”拆了一半编译不过”的中间态
  4. 单次改动聚焦一个目标:不把架构升级和功能增强混在一起

关键决策

决策理由
使用 CommunityToolkit.Mvvm官方推荐,源生成器减少样板代码
5 个 .csproj 而非更多平衡了职责分离和项目维护成本
OTA.slnx 作为主解决方案兼容 .NET 10 SDK 的并行构建
协议真值保持不变重构阶段不修改魔法数字、寄存器地址、帧内容
实机验证由用户执行涉及串口和 OTA 行为的验证不能自动化

当前状态

Phase 1-3 已全部完成落地:

  • 5 个项目骨架已建立,依赖方向正确
  • App.xaml 已去掉 StartupUri,DI 容器已接入
  • MainViewModel 已启用,承接主界面状态
  • MainWindow 已改为 TabControl 壳层,LocalUpgradeViewRemoteUpgradeView 已拆分
  • 用户已完成本地升级功能实机验证,主流程正常
  • 构建命令:dotnet build OTA.slnx -m:1