feat: 初始化 PCB 检测 WPF 应用程序

* 创建 AxiOmron.PcbCheck 项目主框架及解决方案
* 添加 Dashboard 和系统设置页面
* 实现 Modbus TCP PLC、扫码枪、SFTP 查询等核心服务
* 集成 Andon 报警、工作流托管服务与日志配置
* 补充项目文档和 UI 设计规范
This commit is contained in:
2026-04-17 10:43:51 +08:00
parent 660ee99442
commit 49f113dcf3
46 changed files with 8042 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
using AxiOmron.PcbCheck.Options;
using AxiOmron.PcbCheck.Services.Interfaces;
namespace AxiOmron.PcbCheck.DesignTime;
/// <summary>
/// 提供设计时配置服务,返回固定的示例配置数据。
/// </summary>
public sealed class DesignTimeAppConfigService : IAppConfigService
{
private readonly AppConfig _config;
/// <summary>
/// 初始化设计时配置服务。
/// </summary>
public DesignTimeAppConfigService()
{
_config = CreateSampleConfig();
}
/// <summary>
/// 读取设计时配置副本。
/// </summary>
/// <returns>示例根配置对象。</returns>
public AppConfig Load()
{
return CreateSampleConfig();
}
/// <summary>
/// 保存设计时配置,占位实现,仅更新内存中的副本。
/// </summary>
/// <param name="config">待保存的配置对象。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="config"/> 为 <see langword="null"/> 时抛出。</exception>
public void Save(AppConfig config)
{
ArgumentNullException.ThrowIfNull(config);
_config.Plc = config.Plc;
_config.Scanner = config.Scanner;
_config.Sftp = config.Sftp;
_config.Andon = config.Andon;
_config.Workflow = config.Workflow;
}
/// <summary>
/// 获取设计时展示用的示例配置路径。
/// </summary>
/// <returns>固定的设计时配置路径文本。</returns>
public string GetConfigPath()
{
return @"D:\DesignTime\appConfig.Development.json";
}
/// <summary>
/// 创建设计器使用的示例配置对象。
/// </summary>
/// <returns>填充默认值后的配置对象。</returns>
private static AppConfig CreateSampleConfig()
{
return new AppConfig
{
Plc = new PlcOptions
{
Host = "192.168.10.25",
Port = 502,
UnitId = 1,
PollIntervalMs = 200,
ConnectTimeoutMs = 3000,
HeartbeatIntervalMs = 500,
ReleasePulseMs = 450,
ReleaseAckTimeoutMs = 2500
},
Scanner = new ScannerOptions
{
PortName = "COM3",
BaudRate = 9600,
DataBits = 8,
Parity = "None",
StopBits = "One",
ReadTimeoutMs = 2500,
TriggerCommand = "SCAN\\r",
ResponseTerminator = "\\r",
MaxScanAttempts = 3
},
Sftp = new SftpOptions
{
Host = "10.10.20.35",
Port = 22,
Username = "pcb_user",
Password = "******",
PrivateKeyPath = @"C:\Keys\pcb-check.ppk",
RootPath = "/data/pcb",
FileNamePattern = "${barcode}.txt",
RetryIntervalSeconds = 2,
MaxRetryCount = 3,
ConnectTimeoutMs = 3000
},
Andon = new AndonOptions
{
Enable = true,
Url = "http://10.10.20.50/api/andon/alarm",
Method = "POST",
TimeoutMs = 3000,
StationCode = "OMRON-L01",
StationName = "欧姆龙 PCB 检测",
EnableScanFailAlarm = true,
EnableFileNotFoundAlarm = true
},
Workflow = new WorkflowOptions
{
RequirePlcReady = true,
RequireAutoMode = true,
RequireStationEnable = true,
RequireManualResetAfterFault = true,
MaxUiLogEntries = 200,
MaxBoardRecords = 100
}
};
}
}

View File

@@ -0,0 +1,206 @@
using AxiOmron.PcbCheck.Models;
using AxiOmron.PcbCheck.Services.Interfaces;
namespace AxiOmron.PcbCheck.DesignTime;
/// <summary>
/// 提供设计时运行态存储,向真实 ViewModel 回放固定的演示数据。
/// </summary>
public sealed class DesignTimeAppStateStore : IAppStateStore
{
private readonly RuntimeSnapshot _snapshot;
private readonly IReadOnlyList<UiLogEntry> _logs;
private readonly IReadOnlyList<BoardProcessRecord> _records;
private EventHandler<RuntimeSnapshot>? _snapshotChanged;
private EventHandler<UiLogEntry>? _logAdded;
private EventHandler<BoardProcessRecord>? _recordAdded;
/// <summary>
/// 初始化设计时运行态存储。
/// </summary>
public DesignTimeAppStateStore()
{
DateTimeOffset now = DateTimeOffset.Now;
_snapshot = new RuntimeSnapshot
{
PlcStatus = "已连接",
ScannerStatus = "在线",
SftpStatus = "可访问",
AndonStatus = "接口正常",
WorkflowState = WorkflowState.CheckingSftp,
WorkflowStateText = WorkflowState.CheckingSftp.ToDisplayText(),
CurrentBarcode = "PCB240417000128",
ResultDescription = "已扫码,等待 SFTP 文件确认",
FaultMessage = string.Empty,
ScanTryCount = 1,
SftpTryCount = 2,
ResultCode = (ushort)WorkflowResultCode.Processing,
AlarmCode = (ushort)AlarmCode.None,
LastTriggeredAt = now.AddSeconds(-18),
LastCompletedAt = now.AddMinutes(-2),
IsBusy = true,
ProcessDone = false,
SystemFault = false,
AlarmRaised = false,
LastUpdatedAt = now
};
_logs = new List<UiLogEntry>
{
new() { Timestamp = now.AddSeconds(-2), Level = "Info", Message = "SFTP 第 2 次查询:正在检查目标文件。" },
new() { Timestamp = now.AddSeconds(-9), Level = "Warning", Message = "第 1 次 SFTP 查询未命中,准备重试。" },
new() { Timestamp = now.AddSeconds(-16), Level = "Info", Message = "扫码成功,条码=PCB240417000128" },
new() { Timestamp = now.AddSeconds(-21), Level = "Info", Message = "检测到 PCB 到位,流程开始执行。" },
new() { Timestamp = now.AddMinutes(-1), Level = "Error", Message = "上一片文件查询超时,已按规则放行。" }
};
_records = new List<BoardProcessRecord>
{
new()
{
StartedAt = now.AddMinutes(-4),
CompletedAt = now.AddMinutes(-3).AddSeconds(-18),
Barcode = "PCB240417000125",
ScanTryCount = 1,
SftpTryCount = 1,
ResultCode = (ushort)WorkflowResultCode.Passed,
ResultDescription = "OK 放行",
ReleaseSent = true,
AlarmRaised = false,
ExceptionSummary = string.Empty
},
new()
{
StartedAt = now.AddMinutes(-3),
CompletedAt = now.AddMinutes(-2).AddSeconds(-12),
Barcode = "PCB240417000126",
ScanTryCount = 3,
SftpTryCount = 0,
ResultCode = (ushort)WorkflowResultCode.ScanFailedReleased,
ResultDescription = "扫码失败后放行",
ReleaseSent = true,
AlarmRaised = true,
ExceptionSummary = "扫码连续失败三次"
},
new()
{
StartedAt = now.AddMinutes(-2),
CompletedAt = now.AddMinutes(-1).AddSeconds(-25),
Barcode = "PCB240417000127",
ScanTryCount = 1,
SftpTryCount = 3,
ResultCode = (ushort)WorkflowResultCode.FileNotFoundReleased,
ResultDescription = "文件超时未找到后放行",
ReleaseSent = true,
AlarmRaised = true,
ExceptionSummary = "SFTP 文件查询超时"
}
};
}
/// <summary>
/// 当运行态快照发生变化时触发。
/// </summary>
public event EventHandler<RuntimeSnapshot>? SnapshotChanged
{
add
{
_snapshotChanged += value;
}
remove
{
_snapshotChanged -= value;
}
}
/// <summary>
/// 当新增日志时触发;订阅时会立即回放现有设计时日志。
/// </summary>
public event EventHandler<UiLogEntry>? LogAdded
{
add
{
if (value is null)
{
return;
}
_logAdded += value;
foreach (UiLogEntry entry in _logs)
{
value(this, entry);
}
}
remove
{
_logAdded -= value;
}
}
/// <summary>
/// 当新增单板记录时触发;订阅时会立即回放现有设计时记录。
/// </summary>
public event EventHandler<BoardProcessRecord>? RecordAdded
{
add
{
if (value is null)
{
return;
}
_recordAdded += value;
foreach (BoardProcessRecord record in _records)
{
value(this, record);
}
}
remove
{
_recordAdded -= value;
}
}
/// <summary>
/// 获取当前设计时快照副本。
/// </summary>
/// <returns>当前快照副本。</returns>
public RuntimeSnapshot GetSnapshot()
{
return _snapshot.Clone();
}
/// <summary>
/// 更新设计时快照并通知订阅者。
/// </summary>
/// <param name="updateAction">用于修改快照的委托。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="updateAction"/> 为 <see langword="null"/> 时抛出。</exception>
public void UpdateSnapshot(Action<RuntimeSnapshot> updateAction)
{
ArgumentNullException.ThrowIfNull(updateAction);
updateAction(_snapshot);
_snapshotChanged?.Invoke(this, _snapshot.Clone());
}
/// <summary>
/// 追加一条设计时日志并通知订阅者。
/// </summary>
/// <param name="entry">待追加的日志对象。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="entry"/> 为 <see langword="null"/> 时抛出。</exception>
public void AddLog(UiLogEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
_logAdded?.Invoke(this, entry);
}
/// <summary>
/// 追加一条设计时处理记录并通知订阅者。
/// </summary>
/// <param name="record">待追加的记录对象。</param>
/// <exception cref="ArgumentNullException">当 <paramref name="record"/> 为 <see langword="null"/> 时抛出。</exception>
public void AddRecord(BoardProcessRecord record)
{
ArgumentNullException.ThrowIfNull(record);
_recordAdded?.Invoke(this, record);
}
}

View File

@@ -0,0 +1,22 @@
using AxiOmron.PcbCheck.Services.Interfaces;
namespace AxiOmron.PcbCheck.DesignTime;
/// <summary>
/// 提供设计时 Dispatcher 调度能力,直接在当前线程执行委托。
/// </summary>
public sealed class DesignTimeDispatcherService : IDispatcherService
{
/// <summary>
/// 在当前线程中立即执行指定动作。
/// </summary>
/// <param name="action">待执行的动作。</param>
/// <returns>表示执行完成的任务。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
public Task InvokeAsync(Action action)
{
ArgumentNullException.ThrowIfNull(action);
action();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,39 @@
using AxiOmron.PcbCheck.Options;
using AxiOmron.PcbCheck.ViewModels;
namespace AxiOmron.PcbCheck.DesignTime;
/// <summary>
/// 为 XAML 设计器提供基于真实 ViewModel 的设计时定位器。
/// </summary>
public sealed class DesignTimeViewModelLocator
{
private readonly DesignTimeAppStateStore _appStateStore = new();
private readonly DesignTimeDispatcherService _dispatcherService = new();
private readonly DesignTimeWorkflowControlService _workflowControlService = new();
private readonly DesignTimeAppConfigService _appConfigService = new();
private MainWindowViewModel? _mainWindowViewModel;
private SystemSettingViewModel? _systemSettingViewModel;
/// <summary>
/// 获取首页设计时视图模型。
/// </summary>
public MainWindowViewModel MainWindowViewModel
=> _mainWindowViewModel ??= CreateMainWindowViewModel();
/// <summary>
/// 获取系统设置设计时视图模型。
/// </summary>
public SystemSettingViewModel SystemSettingViewModel
=> _systemSettingViewModel ??= new SystemSettingViewModel(_appConfigService);
/// <summary>
/// 创建首页设计时视图模型。
/// </summary>
/// <returns>填充了设计时演示数据的真实视图模型实例。</returns>
private MainWindowViewModel CreateMainWindowViewModel()
{
AppConfig config = _appConfigService.Load();
return new MainWindowViewModel(_appStateStore, _dispatcherService, _workflowControlService, config);
}
}

View File

@@ -0,0 +1,57 @@
using AxiOmron.PcbCheck.Services.Interfaces;
namespace AxiOmron.PcbCheck.DesignTime;
/// <summary>
/// 提供设计时流程控制服务,所有命令均为无副作用占位实现。
/// </summary>
public sealed class DesignTimeWorkflowControlService : IWorkflowControlService
{
/// <summary>
/// 模拟手动复位流程命令,不执行实际业务操作。
/// </summary>
/// <param name="cancellationToken">取消令牌;若已取消则立即返回已取消任务。</param>
/// <returns>表示命令已完成的任务。</returns>
public Task ResetAsync(CancellationToken cancellationToken)
{
return cancellationToken.IsCancellationRequested
? Task.FromCanceled(cancellationToken)
: Task.CompletedTask;
}
/// <summary>
/// 模拟 PLC 重连命令,不执行实际设备通信。
/// </summary>
/// <param name="cancellationToken">取消令牌;若已取消则立即返回已取消任务。</param>
/// <returns>表示命令已完成的任务。</returns>
public Task ReconnectPlcAsync(CancellationToken cancellationToken)
{
return cancellationToken.IsCancellationRequested
? Task.FromCanceled(cancellationToken)
: Task.CompletedTask;
}
/// <summary>
/// 模拟扫码枪重连命令,不执行实际设备通信。
/// </summary>
/// <param name="cancellationToken">取消令牌;若已取消则立即返回已取消任务。</param>
/// <returns>表示命令已完成的任务。</returns>
public Task ReconnectScannerAsync(CancellationToken cancellationToken)
{
return cancellationToken.IsCancellationRequested
? Task.FromCanceled(cancellationToken)
: Task.CompletedTask;
}
/// <summary>
/// 模拟安灯测试命令,不执行实际网络请求。
/// </summary>
/// <param name="cancellationToken">取消令牌;若已取消则立即返回已取消任务。</param>
/// <returns>表示命令已完成的任务。</returns>
public Task TestAndonAsync(CancellationToken cancellationToken)
{
return cancellationToken.IsCancellationRequested
? Task.FromCanceled(cancellationToken)
: Task.CompletedTask;
}
}