From 27e2c9d5d0f4156adabfc925dc5dd39cec955111 Mon Sep 17 00:00:00 2001 From: "yunxiao.zhu" Date: Tue, 2 Jun 2026 17:26:24 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(*):=20=E6=9B=B4=E6=96=B0=20PLC?= =?UTF-8?q?=20=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=E4=B8=8E=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 appConfig.json 中添加 EndianFormat 配置项并更新相关输入输出地址 - 修改 ResultCode 类型为 short,确保与 PLC 通信一致 - 新增 DesignTimePlcService 以支持设计时的 PLC 测试写入功能 - 更新 SystemSettingViewModel 和 DashboardPage 以支持手动测试写入 - 调整多个视图模型和服务以适应新的 PLC 写入逻辑 - 更新相关单元测试以验证新功能的正确性 --- src/AxiOmron.PcbCheck/Assets/AppIcon.ico | Bin 0 -> 34765 bytes src/AxiOmron.PcbCheck/Assets/AppIcon.png | Bin 0 -> 17409 bytes .../AxiOmron.PcbCheck.csproj | 6 + .../ResultCodeToTagTextConverter.cs | 22 +- .../DesignTime/DesignTimeAppStateStore.cs | 14 +- .../DesignTime/DesignTimePlcService.cs | 64 +++++ .../DesignTime/DesignTimeViewModelLocator.cs | 3 +- src/AxiOmron.PcbCheck/MainWindow.xaml | 1 + .../Models/PlcWriteModels.cs | 105 ++++++++ .../Models/RuntimeSnapshot.cs | 2 +- .../Models/WorkflowModels.cs | 6 +- src/AxiOmron.PcbCheck/Options/AppConfig.cs | 5 + .../PublishProfiles/FolderProfile.pubxml | 28 +-- .../Properties/launchSettings.json | 10 - .../CsvBoardRecordRepository.cs | 10 +- .../Implementations/FileCheckPathMatcher.cs | 3 +- .../Implementations/OmronFinsPlcService.cs | 234 +++++++++++++++++- .../Implementations/WorkflowHostedService.cs | 8 +- .../Services/Interfaces/CoreInterfaces.cs | 11 + .../ViewModels/MainWindowViewModel.cs | 17 +- .../ViewModels/SystemSettingViewModel.cs | 82 +++++- .../Views/Pages/DashboardPage.xaml | 25 +- .../Views/Pages/SystemSettingsPage.xaml | 56 +++++ src/AxiOmron.PcbCheck/appConfig.json | 23 +- .../CsvBoardRecordRepositoryTests.cs | 10 +- .../LocalFileCheckProviderTests.cs | 12 +- .../MainWindowViewModelTests.cs | 23 +- .../OmronFinsPlcServiceTests.cs | 41 +++ .../ResultCodeTypeTests.cs | 33 +++ .../SystemSettingViewModelTests.cs | 42 +++- .../WorkflowHostedServiceTests.cs | 26 +- 31 files changed, 812 insertions(+), 110 deletions(-) create mode 100644 src/AxiOmron.PcbCheck/Assets/AppIcon.ico create mode 100644 src/AxiOmron.PcbCheck/Assets/AppIcon.png create mode 100644 src/AxiOmron.PcbCheck/DesignTime/DesignTimePlcService.cs create mode 100644 src/AxiOmron.PcbCheck/Models/PlcWriteModels.cs delete mode 100644 src/AxiOmron.PcbCheck/Properties/launchSettings.json create mode 100644 tests/AxiOmron.PcbCheck.Tests/OmronFinsPlcServiceTests.cs create mode 100644 tests/AxiOmron.PcbCheck.Tests/ResultCodeTypeTests.cs diff --git a/src/AxiOmron.PcbCheck/Assets/AppIcon.ico b/src/AxiOmron.PcbCheck/Assets/AppIcon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1d88bbf7dfb822b2497ae02fd5ab10a84698fd1f GIT binary patch literal 34765 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 tdA0EP*}9Mk#}chD}9U>?{TtEWU2`FE#Dg8 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 literal 0 HcmV?d00001 diff --git a/src/AxiOmron.PcbCheck/Assets/AppIcon.png b/src/AxiOmron.PcbCheck/Assets/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..cf2649b690e67b32460ba57ed976e9465c37e696 GIT binary patch literal 17409 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 literal 0 HcmV?d00001 diff --git a/src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj b/src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj index 496e887..acccc63 100644 --- a/src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj +++ b/src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj @@ -5,6 +5,8 @@ enable enable true + Assets\AppIcon.ico + $(InterceptorsNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration @@ -18,6 +20,10 @@ + + + + Always diff --git a/src/AxiOmron.PcbCheck/Converters/ResultCodeToTagTextConverter.cs b/src/AxiOmron.PcbCheck/Converters/ResultCodeToTagTextConverter.cs index 30970a2..28cf4f1 100644 --- a/src/AxiOmron.PcbCheck/Converters/ResultCodeToTagTextConverter.cs +++ b/src/AxiOmron.PcbCheck/Converters/ResultCodeToTagTextConverter.cs @@ -5,7 +5,7 @@ using AxiOmron.PcbCheck.Models; namespace AxiOmron.PcbCheck.Converters; /// -/// 将 (以 存储)映射为简短 Tag 文本。 +/// 将 (以 存储)映射为简短 Tag 文本。 /// /// /// 映射规则:Passed → "OK";Processing → "处理中";None → "-";其余视为 "NG"。 @@ -20,21 +20,21 @@ public sealed class ResultCodeToTagTextConverter : IValueConverter /// 扩展参数。 /// 当前区域信息。 /// 简短标签文本。 - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - ushort code = value switch + short code = value switch { - ushort u => u, - int i => (ushort)i, - WorkflowResultCode rc => (ushort)rc, - _ => (ushort)0 + short s => s, + int i => (short)i, + WorkflowResultCode rc => (short)rc, + _ => 0 }; return code switch { - (ushort)WorkflowResultCode.Passed => "OK", - (ushort)WorkflowResultCode.Processing => "处理中", - (ushort)WorkflowResultCode.None => "-", + (short)WorkflowResultCode.Passed => "OK", + (short)WorkflowResultCode.Processing => "处理中", + (short)WorkflowResultCode.None => "-", _ => "NG" }; } @@ -47,7 +47,7 @@ public sealed class ResultCodeToTagTextConverter : IValueConverter /// 扩展参数。 /// 当前区域信息。 /// 抛出不支持异常。 - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotSupportedException(); } diff --git a/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs index 063cb9d..fac8ace 100644 --- a/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs +++ b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs @@ -32,7 +32,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore CurrentBarcode = "PCB240417000128", ResultDescription = "已扫码,等待 SFTP 文件确认", FaultMessage = string.Empty, - ResultCode = (ushort)WorkflowResultCode.Processing, + ResultCode = (short)WorkflowResultCode.Processing, LastTriggeredAt = now.AddSeconds(-18), LastCompletedAt = now.AddMinutes(-2), IsBusy = true, @@ -44,7 +44,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore _snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Inputs", Name = "PlcAckRelease", Address = "10003", CurrentValue = "False", LastUpdatedAt = now.AddSeconds(-1) }); _snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Outputs", Name = "PcBusy", Address = "51", CurrentValue = "True", LastUpdatedAt = now.AddSeconds(-1) }); _snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Outputs", Name = "ReleasePermit", Address = "52", CurrentValue = "False", LastUpdatedAt = now.AddSeconds(-1) }); - _snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Registers", Name = "ResultCode", Address = "40001", CurrentValue = ((ushort)WorkflowResultCode.Processing).ToString(), LastUpdatedAt = now.AddSeconds(-1) }); + _snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Registers", Name = "ResultCode", Address = "40001", CurrentValue = ((short)WorkflowResultCode.Processing).ToString(), LastUpdatedAt = now.AddSeconds(-1) }); _logs = new List { @@ -64,7 +64,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore Barcode = "PCB240417000125", ScanTryCount = 1, SftpTryCount = 1, - ResultCode = (ushort)WorkflowResultCode.Passed, + ResultCode = (short)WorkflowResultCode.Passed, ResultDescription = "OK 放行", ReleaseSent = true, ExceptionSummary = string.Empty @@ -76,7 +76,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore Barcode = "PCB240417000126", ScanTryCount = 3, SftpTryCount = 0, - ResultCode = (ushort)WorkflowResultCode.NgReleased, + ResultCode = (short)WorkflowResultCode.NgReleased, ResultDescription = "扫码失败后放行", ReleaseSent = true, ExceptionSummary = "扫码连续失败三次" @@ -88,7 +88,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore Barcode = "PCB240417000127", ScanTryCount = 1, SftpTryCount = 3, - ResultCode = (ushort)WorkflowResultCode.NgReleased, + ResultCode = (short)WorkflowResultCode.NgReleased, ResultDescription = "处理结果 NG,随后放行", ReleaseSent = true, ExceptionSummary = "SFTP 文件查询超时" @@ -97,8 +97,8 @@ public sealed class DesignTimeAppStateStore : IAppStateStore _snapshot.RecordStatisticDate = DateOnly.FromDateTime(now.LocalDateTime.Date); _snapshot.TodayProcessCount = _records.Count; - _snapshot.TodayOkCount = _records.Count(record => record.ResultCode == (ushort)WorkflowResultCode.Passed); - _snapshot.TodayNgCount = _records.Count(record => record.ResultCode != (ushort)WorkflowResultCode.Passed); + _snapshot.TodayOkCount = _records.Count(record => record.ResultCode == (short)WorkflowResultCode.Passed); + _snapshot.TodayNgCount = _records.Count(record => record.ResultCode != (short)WorkflowResultCode.Passed); _snapshot.RecordsLastUpdatedAt = now; foreach (BoardProcessRecord record in _records) { diff --git a/src/AxiOmron.PcbCheck/DesignTime/DesignTimePlcService.cs b/src/AxiOmron.PcbCheck/DesignTime/DesignTimePlcService.cs new file mode 100644 index 0000000..0635212 --- /dev/null +++ b/src/AxiOmron.PcbCheck/DesignTime/DesignTimePlcService.cs @@ -0,0 +1,64 @@ +using AxiOmron.PcbCheck.Models; +using AxiOmron.PcbCheck.Services.Interfaces; + +namespace AxiOmron.PcbCheck.DesignTime; + +/// +/// 为设计器提供 PLC 通信能力的设计时占位服务,所有方法均为无副作用实现。 +/// +public sealed class DesignTimePlcService : IPlcService +{ + /// + /// 返回设计时输入信号快照。 + /// + /// 取消令牌。 + /// 空的输入信号快照。 + public Task ReadSignalsAsync(CancellationToken cancellationToken) + { + return Task.FromResult(new PlcSignalSnapshot()); + } + + /// + /// 返回设计时监控镜像快照。 + /// + /// 取消令牌。 + /// 空的监控镜像快照。 + public Task ReadMonitorSnapshotAsync(CancellationToken cancellationToken) + { + return Task.FromResult(new PlcMonitorSnapshot()); + } + + /// + /// 模拟写入流程状态,不执行实际设备通信。 + /// + /// 待写入状态。 + /// 取消令牌。 + /// 已完成任务。 + public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + /// 模拟 PLC 重连,不执行实际设备通信。 + /// + /// 取消令牌。 + /// 已完成任务。 + public Task ForceReconnectAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + /// 返回设计时测试写入成功结果,不执行实际设备通信。 + /// + /// 目标地址。 + /// 变量类型。 + /// 写入值文本。 + /// 取消令牌。 + /// 固定成功结果。 + public Task WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken) + { + return Task.FromResult(PlcWriteResult.Success(address, type, value)); + } +} diff --git a/src/AxiOmron.PcbCheck/DesignTime/DesignTimeViewModelLocator.cs b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeViewModelLocator.cs index c8b56c4..27b896a 100644 --- a/src/AxiOmron.PcbCheck/DesignTime/DesignTimeViewModelLocator.cs +++ b/src/AxiOmron.PcbCheck/DesignTime/DesignTimeViewModelLocator.cs @@ -15,6 +15,7 @@ public sealed class DesignTimeViewModelLocator private readonly DesignTimeAppConfigService _appConfigService = new(); private readonly DesignTimeAdminUnlockDialogService _adminUnlockDialogService = new(); private readonly DesignTimeSftpLookupService _sftpLookupService = new(); + private readonly DesignTimePlcService _plcService = new(); private MainWindowViewModel? _mainWindowViewModel; private SystemSettingViewModel? _systemSettingViewModel; @@ -28,7 +29,7 @@ public sealed class DesignTimeViewModelLocator /// 获取系统设置设计时视图模型。 /// public SystemSettingViewModel SystemSettingViewModel - => _systemSettingViewModel ??= new SystemSettingViewModel(_appConfigService, _sftpLookupService, _appStateStore, _dispatcherService); + => _systemSettingViewModel ??= new SystemSettingViewModel(_appConfigService, _sftpLookupService, _appStateStore, _dispatcherService, _plcService); /// /// 创建首页设计时视图模型。 diff --git a/src/AxiOmron.PcbCheck/MainWindow.xaml b/src/AxiOmron.PcbCheck/MainWindow.xaml index 14538d0..d4d9ae3 100644 --- a/src/AxiOmron.PcbCheck/MainWindow.xaml +++ b/src/AxiOmron.PcbCheck/MainWindow.xaml @@ -10,6 +10,7 @@ Height="950" MinWidth="1400" MinHeight="860" + Icon="Assets/AppIcon.ico" Background="{DynamicResource {x:Static hc:ResourceToken.BackgroundBrush}}" WindowStartupLocation="CenterScreen"> diff --git a/src/AxiOmron.PcbCheck/Models/PlcWriteModels.cs b/src/AxiOmron.PcbCheck/Models/PlcWriteModels.cs new file mode 100644 index 0000000..566a2e9 --- /dev/null +++ b/src/AxiOmron.PcbCheck/Models/PlcWriteModels.cs @@ -0,0 +1,105 @@ +namespace AxiOmron.PcbCheck.Models; + +/// +/// 表示 PLC 测试写入支持的变量类型。 +/// +public enum PlcWriteType +{ + /// + /// 布尔位。 + /// + Bool = 0, + + /// + /// 有符号 16 位整数。 + /// + Int16 = 1, + + /// + /// 无符号 16 位整数。 + /// + UInt16 = 2, + + /// + /// 有符号 32 位整数。 + /// + Int32 = 3, + + /// + /// 无符号 32 位整数。 + /// + UInt32 = 4, + + /// + /// 单精度浮点数。 + /// + Float = 5 +} + +/// +/// 表示一次 PLC 测试写入的执行结果。 +/// +public sealed record PlcWriteResult +{ + /// + /// 获取一个值,指示写入是否成功。 + /// + public bool IsSuccess { get; init; } + + /// + /// 获取面向用户的结果说明。 + /// + public string Message { get; init; } = string.Empty; + + /// + /// 获取本次写入的目标地址。 + /// + public string Address { get; init; } = string.Empty; + + /// + /// 获取本次写入的变量类型名称。 + /// + public string TypeName { get; init; } = string.Empty; + + /// + /// 获取本次实际写入的文本值。 + /// + public string WrittenText { get; init; } = string.Empty; + + /// + /// 创建一个表示写入成功的结果。 + /// + /// 目标地址。 + /// 变量类型。 + /// 实际写入的文本值。 + /// 成功结果对象。 + public static PlcWriteResult Success(string address, PlcWriteType type, string writtenText) + { + return new PlcWriteResult + { + IsSuccess = true, + Address = address, + TypeName = type.ToString(), + WrittenText = writtenText, + Message = $"写入成功:{address} = {writtenText} ({type})" + }; + } + + /// + /// 创建一个表示写入失败的结果。 + /// + /// 目标地址。 + /// 变量类型。 + /// 失败说明。 + /// 失败结果对象。 + public static PlcWriteResult Failure(string address, PlcWriteType type, string message) + { + return new PlcWriteResult + { + IsSuccess = false, + Address = address, + TypeName = type.ToString(), + Message = message + }; + } +} diff --git a/src/AxiOmron.PcbCheck/Models/RuntimeSnapshot.cs b/src/AxiOmron.PcbCheck/Models/RuntimeSnapshot.cs index 3b1c1f5..1911f6f 100644 --- a/src/AxiOmron.PcbCheck/Models/RuntimeSnapshot.cs +++ b/src/AxiOmron.PcbCheck/Models/RuntimeSnapshot.cs @@ -63,7 +63,7 @@ public sealed class RuntimeSnapshot /// /// 获取或设置结果代码。 /// - public ushort ResultCode { get; set; } + public short ResultCode { get; set; } /// /// 获取或设置上次触发时间。 diff --git a/src/AxiOmron.PcbCheck/Models/WorkflowModels.cs b/src/AxiOmron.PcbCheck/Models/WorkflowModels.cs index a4fb15a..06e9128 100644 --- a/src/AxiOmron.PcbCheck/Models/WorkflowModels.cs +++ b/src/AxiOmron.PcbCheck/Models/WorkflowModels.cs @@ -69,7 +69,7 @@ public enum WorkflowState /// /// 表示最终结果代码定义。 /// -public enum WorkflowResultCode : ushort +public enum WorkflowResultCode : short { /// /// 无结果。 @@ -182,7 +182,7 @@ public sealed class PlcProcessState /// /// 获取或设置结果代码寄存器值。 /// - public ushort ResultCode { get; set; } + public short ResultCode { get; set; } /// /// 创建当前状态对象的浅拷贝。 @@ -496,7 +496,7 @@ public sealed class BoardProcessRecord /// /// 获取或设置结果代码。 /// - public ushort ResultCode { get; set; } + public short ResultCode { get; set; } /// /// 获取或设置结果描述。 diff --git a/src/AxiOmron.PcbCheck/Options/AppConfig.cs b/src/AxiOmron.PcbCheck/Options/AppConfig.cs index c995fad..87713c7 100644 --- a/src/AxiOmron.PcbCheck/Options/AppConfig.cs +++ b/src/AxiOmron.PcbCheck/Options/AppConfig.cs @@ -129,6 +129,11 @@ public sealed class PlcOptions /// public byte SourceNodeAddress { get; set; } = 25; + /// + /// 获取或设置 PLC 寄存器字节序,默认使用 BADC 以匹配现场 D 区 Int16 高低字节顺序。 + /// + public string? EndianFormat { get; set; } = "BADC"; + /// /// 获取或设置轮询周期,单位为毫秒。 /// diff --git a/src/AxiOmron.PcbCheck/Properties/PublishProfiles/FolderProfile.pubxml b/src/AxiOmron.PcbCheck/Properties/PublishProfiles/FolderProfile.pubxml index 1e728be..6af5f01 100644 --- a/src/AxiOmron.PcbCheck/Properties/PublishProfiles/FolderProfile.pubxml +++ b/src/AxiOmron.PcbCheck/Properties/PublishProfiles/FolderProfile.pubxml @@ -1,15 +1,13 @@ - - - - Release - Any CPU - bin\Release\net8.0-windows\publish\ - FileSystem - net8.0-windows - win-x64 - false - true - false - false - - + + + + + Release + Any CPU + bin\Release\net8.0-windows\publish\ + FileSystem + <_TargetId>Folder + net8.0-windows + false + + \ No newline at end of file diff --git a/src/AxiOmron.PcbCheck/Properties/launchSettings.json b/src/AxiOmron.PcbCheck/Properties/launchSettings.json deleted file mode 100644 index 714055d..0000000 --- a/src/AxiOmron.PcbCheck/Properties/launchSettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "profiles": { - "AxiOmron.PcbCheck": { - "commandName": "Project", - "environmentVariables": { - "DOTNET_ENVIRONMENT": "Development" - } - } - } -} diff --git a/src/AxiOmron.PcbCheck/Services/Implementations/CsvBoardRecordRepository.cs b/src/AxiOmron.PcbCheck/Services/Implementations/CsvBoardRecordRepository.cs index 1f786de..ea680eb 100644 --- a/src/AxiOmron.PcbCheck/Services/Implementations/CsvBoardRecordRepository.cs +++ b/src/AxiOmron.PcbCheck/Services/Implementations/CsvBoardRecordRepository.cs @@ -247,10 +247,10 @@ public sealed class CsvBoardRecordRepository : IBoardRecordRepository { StatisticDate = date, TotalCount = records.Count, - OkCount = records.Count(record => record.ResultCode == (ushort)WorkflowResultCode.Passed), - NgCount = records.Count(record => record.ResultCode != (ushort)WorkflowResultCode.Passed - && record.ResultCode != (ushort)WorkflowResultCode.Processing - && record.ResultCode != (ushort)WorkflowResultCode.None), + OkCount = records.Count(record => record.ResultCode == (short)WorkflowResultCode.Passed), + NgCount = records.Count(record => record.ResultCode != (short)WorkflowResultCode.Passed + && record.ResultCode != (short)WorkflowResultCode.Processing + && record.ResultCode != (short)WorkflowResultCode.None), LastUpdatedAt = _timeProvider.GetLocalNow() }; @@ -400,7 +400,7 @@ public sealed class CsvBoardRecordRepository : IBoardRecordRepository || !DateTimeOffset.TryParse(fields[1], out DateTimeOffset completedAt) || !int.TryParse(fields[3], out int scanTryCount) || !int.TryParse(fields[4], out int sftpTryCount) - || !ushort.TryParse(fields[5], out ushort resultCode) + || !short.TryParse(fields[5], out short resultCode) || !bool.TryParse(fields[7], out bool releaseSent) || !bool.TryParse(fields[8], out bool alarmRaised)) { diff --git a/src/AxiOmron.PcbCheck/Services/Implementations/FileCheckPathMatcher.cs b/src/AxiOmron.PcbCheck/Services/Implementations/FileCheckPathMatcher.cs index 98fb0ad..5194942 100644 --- a/src/AxiOmron.PcbCheck/Services/Implementations/FileCheckPathMatcher.cs +++ b/src/AxiOmron.PcbCheck/Services/Implementations/FileCheckPathMatcher.cs @@ -29,7 +29,6 @@ internal static class FileCheckPathMatcher public static bool IsMatch(string fileName, string candidateName, string barcode) { return fileName.Equals(candidateName, StringComparison.OrdinalIgnoreCase) - || WildcardMatcher.IsMatch(fileName, candidateName) - || fileName.Contains(barcode, StringComparison.OrdinalIgnoreCase); + || WildcardMatcher.IsMatch(fileName, candidateName); } } diff --git a/src/AxiOmron.PcbCheck/Services/Implementations/OmronFinsPlcService.cs b/src/AxiOmron.PcbCheck/Services/Implementations/OmronFinsPlcService.cs index f22c7fc..4e50ea2 100644 --- a/src/AxiOmron.PcbCheck/Services/Implementations/OmronFinsPlcService.cs +++ b/src/AxiOmron.PcbCheck/Services/Implementations/OmronFinsPlcService.cs @@ -89,7 +89,7 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable { PcBusy = ReadBoolean(ToDataMemoryAddress(_options.Outputs.PcBusy)), ReleasePermit = ReadBoolean(ToDataMemoryAddress(_options.Outputs.ReleasePermit)), - ResultCode = ReadUInt16(ToDataMemoryAddress(_options.Registers.ResultCode)) + ResultCode = ReadInt16(ToDataMemoryAddress(_options.Registers.ResultCode)) }, CapturedAt = capturedAt }; @@ -155,6 +155,54 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable } } + /// + /// 向指定地址写入一个原始值,供调试界面手动测试使用。 + /// 复用与回读相同的 串行化,且不修改 ,因此不会干扰后台轮询回读与流程状态机写入。 + /// + /// 完整的 FINS 地址字符串,例如 D100 或 D100.0。 + /// 写入的变量类型。 + /// 待写入值的文本表示。 + /// 取消令牌。取消后立即终止本次写入并返回已取消任务。 + /// 包含成功状态与说明的写入结果。 + public async Task WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken) + { + string trimmedAddress = address.Trim(); + if (string.IsNullOrEmpty(trimmedAddress)) + { + return PlcWriteResult.Failure(trimmedAddress, type, "写入失败:地址不能为空。"); + } + + if (!TryParseWriteValue(type, value, out object parsedValue, out string parseError)) + { + return PlcWriteResult.Failure(trimmedAddress, type, $"写入失败:{parseError}"); + } + + await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + cancellationToken.ThrowIfCancellationRequested(); + EnsureConnected(); + WriteRawValueUnsafe(trimmedAddress, parsedValue); + string writtenText = FormatWrittenText(parsedValue); + _logger.LogInformation("测试写入 PLC 成功,地址={Address},类型={Type},值={Value}", true, trimmedAddress, type, writtenText); + return PlcWriteResult.Success(trimmedAddress, type, writtenText); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + // 仅记录并返回失败结果,不断开连接,避免干扰后台轮询回读循环。 + _logger.LogWarning(ex, "测试写入 PLC 失败,地址={Address},类型={Type}", true, trimmedAddress, type); + return PlcWriteResult.Failure(trimmedAddress, type, $"写入失败:{ex.Message}"); + } + finally + { + _ioLock.Release(); + } + } + /// /// 释放 PLC 通信资源。 /// @@ -181,7 +229,8 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable } DisconnectUnsafe(); - var client = new OmronFinsClient(_options.Host, _options.Port, _options.ConnectTimeoutMs, EndianFormat.ABCD) + EndianFormat endianFormat = ResolveEndianFormat(_options); + var client = new OmronFinsClient(_options.Host, _options.Port, _options.ConnectTimeoutMs, endianFormat) { UnitAddress = _options.UnitAddress, SA1 = _options.SourceNodeAddress @@ -189,7 +238,28 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable var openResult = client.Open(); EnsureSuccess(openResult.IsSucceed, openResult.Err, "连接 OMRON FINS PLC 失败"); _client = client; - _logger.LogInformation("已通过 IoTClient FINS 连接 PLC {Host}:{Port}", false, _options.Host, _options.Port); + _logger.LogInformation("已通过 IoTClient FINS 连接 PLC {Host}:{Port},字节序={EndianFormat}", false, _options.Host, _options.Port, endianFormat); + } + + /// + /// 解析 PLC 字节序配置。 + /// + /// PLC 通信配置。 + /// IoTClient 使用的字节序枚举。 + /// 当 为空时抛出。 + /// 当字节序配置为空或无法识别时抛出。 + internal static EndianFormat ResolveEndianFormat(PlcOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + string? configuredValue = options.EndianFormat?.Trim(); + if (string.IsNullOrWhiteSpace(configuredValue) + || !Enum.TryParse(configuredValue, ignoreCase: true, out EndianFormat endianFormat)) + { + throw new InvalidOperationException($"PLC 字节序配置无效:{options.EndianFormat}。支持值:ABCD、BADC、CDAB、DCBA。"); + } + + return endianFormat; } /// @@ -206,15 +276,15 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable } /// - /// 读取单个 D 区无符号整数。 + /// 读取单个 D 区有符号 16 位整数。 /// /// FINS 地址。 /// 读取到的寄存器值。 - private ushort ReadUInt16(string address) + private short ReadInt16(string address) { var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。"); - var result = client.ReadUInt16(address); - EnsureSuccess(result.IsSucceed, result.Err, $"读取 FINS UInt16 失败,地址={address}"); + var result = client.ReadInt16(address); + EnsureSuccess(result.IsSucceed, result.Err, $"读取 FINS Int16 失败,地址={address}"); return result.Value; } @@ -225,9 +295,10 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable private void WriteChangedState(PlcProcessState state) { var previous = _lastWrittenState; + short? previousResultCode = previous is null ? null : previous.ResultCode; WriteBooleanIfChanged(previous?.PcBusy, state.PcBusy, ToDataMemoryAddress(_options.Outputs.PcBusy)); WriteBooleanIfChanged(previous?.ReleasePermit, state.ReleasePermit, ToDataMemoryAddress(_options.Outputs.ReleasePermit)); - WriteUInt16IfChanged(previous?.ResultCode, state.ResultCode, ToDataMemoryAddress(_options.Registers.ResultCode)); + WriteInt16IfChanged(previousResultCode, state.ResultCode, ToDataMemoryAddress(_options.Registers.ResultCode)); } /// @@ -249,12 +320,12 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable } /// - /// 仅当值发生变化时写入单个 D 区无符号整数。 + /// 仅当值发生变化时写入单个 D 区有符号 16 位整数。 /// /// 上一值。 /// 当前值。 /// FINS 地址。 - private void WriteUInt16IfChanged(ushort? previous, ushort current, string address) + private void WriteInt16IfChanged(short? previous, short current, string address) { if (previous.HasValue && previous.Value == current) { @@ -263,7 +334,148 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。"); var result = client.Write(address, current); - EnsureSuccess(result.IsSucceed, result.Err, $"写入 FINS UInt16 失败,地址={address},值={current}"); + EnsureSuccess(result.IsSucceed, result.Err, $"写入 FINS Int16 失败,地址={address},值={current}"); + } + + /// + /// 按变量类型将文本值解析为强类型对象。 + /// + /// 变量类型。 + /// 待解析的文本值。 + /// 解析成功时返回的强类型值。 + /// 解析失败时返回的错误说明。 + /// 解析成功返回 ;否则返回 。 + private static bool TryParseWriteValue(PlcWriteType type, string value, out object parsedValue, out string error) + { + parsedValue = default!; + error = string.Empty; + string text = value.Trim(); + if (string.IsNullOrEmpty(text)) + { + error = "写入值不能为空。"; + return false; + } + + switch (type) + { + case PlcWriteType.Bool: + if (TryParseBoolean(text, out bool boolValue)) + { + parsedValue = boolValue; + return true; + } + + error = "布尔值仅支持 1/0、true/false。"; + return false; + case PlcWriteType.Int16: + if (short.TryParse(text, out short shortValue)) + { + parsedValue = shortValue; + return true; + } + + error = $"无法将 '{text}' 解析为 Int16。"; + return false; + case PlcWriteType.UInt16: + if (ushort.TryParse(text, out ushort ushortValue)) + { + parsedValue = ushortValue; + return true; + } + + error = $"无法将 '{text}' 解析为 UInt16。"; + return false; + case PlcWriteType.Int32: + if (int.TryParse(text, out int intValue)) + { + parsedValue = intValue; + return true; + } + + error = $"无法将 '{text}' 解析为 Int32。"; + return false; + case PlcWriteType.UInt32: + if (uint.TryParse(text, out uint uintValue)) + { + parsedValue = uintValue; + return true; + } + + error = $"无法将 '{text}' 解析为 UInt32。"; + return false; + case PlcWriteType.Float: + if (float.TryParse(text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float floatValue)) + { + parsedValue = floatValue; + return true; + } + + error = $"无法将 '{text}' 解析为 Float。"; + return false; + default: + error = $"不支持的变量类型:{type}。"; + return false; + } + } + + /// + /// 解析布尔文本,接受 1/0 与 true/false(忽略大小写)。 + /// + /// 待解析文本。 + /// 解析结果。 + /// 解析成功返回 ;否则返回 。 + private static bool TryParseBoolean(string text, out bool result) + { + switch (text.ToLowerInvariant()) + { + case "1": + case "true": + result = true; + return true; + case "0": + case "false": + result = false; + return true; + default: + result = false; + return false; + } + } + + /// + /// 按强类型值调用 IoTClient 对应写入重载。 + /// + /// FINS 地址。 + /// 已解析的强类型值。 + private void WriteRawValueUnsafe(string address, object parsedValue) + { + var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。"); + var result = parsedValue switch + { + bool boolValue => client.Write(address, boolValue), + short shortValue => client.Write(address, shortValue), + ushort ushortValue => client.Write(address, ushortValue), + int intValue => client.Write(address, intValue), + uint uintValue => client.Write(address, uintValue), + float floatValue => client.Write(address, floatValue), + _ => throw new InvalidOperationException($"不支持的写入值类型:{parsedValue.GetType().Name}。") + }; + + EnsureSuccess(result.IsSucceed, result.Err, $"写入 FINS 值失败,地址={address}"); + } + + /// + /// 将强类型值格式化为用于展示的文本。 + /// + /// 已解析的强类型值。 + /// 文本表示。 + private static string FormatWrittenText(object parsedValue) + { + return parsedValue switch + { + float floatValue => floatValue.ToString(System.Globalization.CultureInfo.InvariantCulture), + _ => parsedValue.ToString() ?? string.Empty + }; } /// diff --git a/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs b/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs index f34f8a7..25c8d8f 100644 --- a/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs +++ b/src/AxiOmron.PcbCheck/Services/Implementations/WorkflowHostedService.cs @@ -848,7 +848,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS Barcode = _currentBarcode, ScanTryCount = _scanTryCount, SftpTryCount = _sftpTryCount, - ResultCode = (ushort)_resultCode, + ResultCode = (short)_resultCode, ResultDescription = GetResultDescription(_resultCode), ReleaseSent = _releaseSent, AlarmRaised = false, @@ -1082,7 +1082,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS lock (_stateSyncRoot) { updateAction(_plcState); - _plcState.ResultCode = (ushort)_resultCode; + _plcState.ResultCode = (short)_resultCode; snapshot = _plcState.Clone(); } @@ -1103,7 +1103,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS { DateTimeOffset? startedAt; string barcode; - ushort resultCode; + short resultCode; bool isBusy; string faultMessage; @@ -1111,7 +1111,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS { startedAt = _currentBoardStartedAt; barcode = _currentBarcode; - resultCode = (ushort)_resultCode; + resultCode = (short)_resultCode; isBusy = _plcState.PcBusy; faultMessage = _faultLatched ? description : string.Empty; } diff --git a/src/AxiOmron.PcbCheck/Services/Interfaces/CoreInterfaces.cs b/src/AxiOmron.PcbCheck/Services/Interfaces/CoreInterfaces.cs index e5a1bd2..a48befc 100644 --- a/src/AxiOmron.PcbCheck/Services/Interfaces/CoreInterfaces.cs +++ b/src/AxiOmron.PcbCheck/Services/Interfaces/CoreInterfaces.cs @@ -124,6 +124,17 @@ public interface IPlcService /// 取消令牌。 /// 表示重连完成的任务。 Task ForceReconnectAsync(CancellationToken cancellationToken); + + /// + /// 向指定地址写入一个原始值,供调试界面手动测试使用。 + /// 该写入复用与回读相同的 IO 串行化机制,且不会影响流程状态机的写入缓存,因此不会干扰后台轮询回读。 + /// + /// 完整的 FINS 地址字符串,例如 D100 或 D100.0。 + /// 写入的变量类型。 + /// 待写入值的文本表示,由实现按 解析。 + /// 取消令牌。取消后立即终止本次写入并返回已取消任务。 + /// 包含成功状态与说明的写入结果。 + Task WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken); } /// diff --git a/src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs b/src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs index 3f7d94b..1102cf1 100644 --- a/src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs +++ b/src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs @@ -150,7 +150,7 @@ public partial class MainWindowViewModel : ObservableObject /// 获取或设置结果代码。 /// [ObservableProperty] - private ushort _resultCode; + private short _resultCode; /// /// 获取或设置最近触发时间。 @@ -182,6 +182,12 @@ public partial class MainWindowViewModel : ObservableObject [ObservableProperty] private string _lastLogUpdateTime = "-"; + /// + /// 获取或设置运行日志区域是否展开;该状态仅影响首页布局,不参与业务流程。 + /// + [ObservableProperty] + private bool _isLogAreaExpanded; + /// /// 获取或设置今日异常日志条数。 /// @@ -367,6 +373,15 @@ public partial class MainWindowViewModel : ObservableObject LastLogUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); } + /// + /// 切换运行日志区域展开状态,收起或恢复上方状态概览区域。 + /// + [RelayCommand] + private void ToggleLogArea() + { + IsLogAreaExpanded = !IsLogAreaExpanded; + } + /// /// 打开追踪区命令占位。后续应由导航服务跳转至追踪页面。 /// diff --git a/src/AxiOmron.PcbCheck/ViewModels/SystemSettingViewModel.cs b/src/AxiOmron.PcbCheck/ViewModels/SystemSettingViewModel.cs index 332ec42..b7a3c32 100644 --- a/src/AxiOmron.PcbCheck/ViewModels/SystemSettingViewModel.cs +++ b/src/AxiOmron.PcbCheck/ViewModels/SystemSettingViewModel.cs @@ -4,7 +4,6 @@ using AxiOmron.PcbCheck.Options; using AxiOmron.PcbCheck.Services.Interfaces; using AxiOmron.PcbCheck.Models; using System.Collections.ObjectModel; -using System.Collections.Generic; namespace AxiOmron.PcbCheck.ViewModels; @@ -17,6 +16,7 @@ public partial class SystemSettingViewModel : ObservableObject private readonly IFileCheckService _fileCheckService; private readonly IAppStateStore _appStateStore; private readonly IDispatcherService _dispatcherService; + private readonly IPlcService _plcService; /// /// 初始化系统设置视图模型。 @@ -25,20 +25,24 @@ public partial class SystemSettingViewModel : ObservableObject /// 文件校验通道测试服务。 /// 运行态快照存储。 /// UI 线程调度服务。 + /// PLC 通信服务,用于调试界面的测试写入。 public SystemSettingViewModel( IAppConfigService appConfigService, IFileCheckService fileCheckService, IAppStateStore appStateStore, - IDispatcherService dispatcherService) + IDispatcherService dispatcherService, + IPlcService plcService) { _appConfigService = appConfigService ?? throw new ArgumentNullException(nameof(appConfigService)); _fileCheckService = fileCheckService ?? throw new ArgumentNullException(nameof(fileCheckService)); _appStateStore = appStateStore ?? throw new ArgumentNullException(nameof(appStateStore)); _dispatcherService = dispatcherService ?? throw new ArgumentNullException(nameof(dispatcherService)); + _plcService = plcService ?? throw new ArgumentNullException(nameof(plcService)); EditableConfig = _appConfigService.Load(); ConfigPath = _appConfigService.GetConfigPath(); PlcMonitorItems = new ObservableCollection(); FileCheckProviders = Enum.GetValues(); + PlcWriteTypes = Enum.GetValues(); ApplySnapshot(_appStateStore.GetSnapshot()); _appStateStore.SnapshotChanged += OnSnapshotChanged; StatusMessage = "已加载配置。"; @@ -54,6 +58,11 @@ public partial class SystemSettingViewModel : ObservableObject /// public IReadOnlyList FileCheckProviders { get; } + /// + /// 获取 PLC 测试写入支持的变量类型可选项。 + /// + public IReadOnlyList PlcWriteTypes { get; } + /// /// 获取或设置可编辑配置对象。 /// @@ -79,6 +88,37 @@ public partial class SystemSettingViewModel : ObservableObject [ObservableProperty] private bool _isTestingFileCheckConnection; + /// + /// 获取或设置测试写入的目标地址,例如 D100、D100.0。 + /// + [ObservableProperty] + private string _plcWriteAddress = string.Empty; + + /// + /// 获取或设置测试写入的变量类型。 + /// + [ObservableProperty] + private PlcWriteType _selectedPlcWriteType = PlcWriteType.Int16; + + /// + /// 获取或设置测试写入的值文本。 + /// + [ObservableProperty] + private string _plcWriteValue = string.Empty; + + /// + /// 获取或设置测试写入的状态提示文本。 + /// + [ObservableProperty] + private string _plcWriteStatusMessage = string.Empty; + + /// + /// 获取或设置当前是否正在执行 PLC 测试写入。 + /// + [NotifyCanExecuteChangedFor(nameof(WritePlcValueCommand))] + [ObservableProperty] + private bool _isWritingPlc; + /// /// 保存当前配置。 /// @@ -143,6 +183,44 @@ public partial class SystemSettingViewModel : ObservableObject /// private bool CanTestFileCheckConnection => !IsTestingFileCheckConnection; + /// + /// 执行一次 PLC 测试写入。该写入复用回读相同的 IO 串行化,不会干扰后台轮询回读。 + /// + /// 表示写入完成的任务。 + [RelayCommand(CanExecute = nameof(CanWritePlcValue))] + private async Task WritePlcValueAsync() + { + if (string.IsNullOrWhiteSpace(PlcWriteAddress)) + { + PlcWriteStatusMessage = "请先输入写入地址。"; + return; + } + + IsWritingPlc = true; + PlcWriteStatusMessage = "正在写入..."; + + try + { + PlcWriteResult result = await _plcService + .WriteRawValueAsync(PlcWriteAddress, SelectedPlcWriteType, PlcWriteValue, CancellationToken.None); + + PlcWriteStatusMessage = result.Message; + } + catch (Exception ex) + { + PlcWriteStatusMessage = $"写入失败:{ex.Message}"; + } + finally + { + IsWritingPlc = false; + } + } + + /// + /// 判断当前是否允许执行 PLC 测试写入。 + /// + private bool CanWritePlcValue => !IsWritingPlc; + /// /// 将运行态快照中的 PLC 监控数据同步到当前视图模型。 /// diff --git a/src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml b/src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml index bec7176..6073d05 100644 --- a/src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml +++ b/src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml @@ -639,11 +639,12 @@ - + - + @@ -775,6 +776,10 @@ + + @@ -809,17 +814,29 @@ + + +