重构前的状态
OTA 桌面工具最初是一个单体 WPF 项目,所有代码堆在一个 .csproj 里:
| 文件 | 承担的职责 |
|---|---|
MainWindow.xaml.cs | UI 事件、串口列表刷新、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.UI | WPF 启动层、XAML 视图、DI 装配、资源 | 全部 |
分阶段实施
Phase 1:物理拆分(不引入 MVVM)
目标:把底层和核心逻辑从 WPF 项目中剥离,但 UI 仍保持事件驱动。
Models 层:抽出的纯数据类型
FirmwareSlot:A/B 槽位枚举FirmwareImageInfo:镜像文件元数据(路径、大小、CRC32、SHA256、向量表地址)LocalUpgradeOptions:升级参数(串口、超时、镜像路径)LocalSerialSettings、PortOption、RunningSlotRefreshResult、LocalUpgradePreparation等跨层数据对象
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 组装 LocalUpgradeView 和 RemoteUpgradeView:
<TabControl>
<TabItem Header="本地升级">
<views:LocalUpgradeView />
</TabItem>
<TabItem Header="远程维护">
<views:RemoteUpgradeView />
</TabItem>
</TabControl>
MainWindow.xaml.cs 最终收缩为应用外壳代码,只保留设备变更消息、空闲轮询定时器和窗口生命周期处理。
Phase 4:清理与增强
在结构稳定后做命名统一、接口抽象、单元测试补充。
重构原则
- 先物理解耦,后 UI 解耦:先拆项目边界,再引入 MVVM
- 先保持行为一致,再追求结构优雅:协议常量、串口帧、超时、寄存器地址不改语义
- 每一阶段都必须可编译:不出现”拆了一半编译不过”的中间态
- 单次改动聚焦一个目标:不把架构升级和功能增强混在一起
关键决策
| 决策 | 理由 |
|---|---|
使用 CommunityToolkit.Mvvm | 官方推荐,源生成器减少样板代码 |
5 个 .csproj 而非更多 | 平衡了职责分离和项目维护成本 |
OTA.slnx 作为主解决方案 | 兼容 .NET 10 SDK 的并行构建 |
| 协议真值保持不变 | 重构阶段不修改魔法数字、寄存器地址、帧内容 |
| 实机验证由用户执行 | 涉及串口和 OTA 行为的验证不能自动化 |
当前状态
Phase 1-3 已全部完成落地:
- 5 个项目骨架已建立,依赖方向正确
App.xaml已去掉StartupUri,DI 容器已接入MainViewModel已启用,承接主界面状态MainWindow已改为TabControl壳层,LocalUpgradeView和RemoteUpgradeView已拆分- 用户已完成本地升级功能实机验证,主流程正常
- 构建命令:
dotnet build OTA.slnx -m:1