✨ feat: 初始化 PCB 检测 WPF 应用程序
* 创建 AxiOmron.PcbCheck 项目主框架及解决方案 * 添加 Dashboard 和系统设置页面 * 实现 Modbus TCP PLC、扫码枪、SFTP 查询等核心服务 * 集成 Andon 报警、工作流托管服务与日志配置 * 补充项目文档和 UI 设计规范
This commit is contained in:
120
src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppConfigService.cs
Normal file
120
src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppConfigService.cs
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
206
src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs
Normal file
206
src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user