Compare commits
2 Commits
ce356bffff
...
17839a9e76
| Author | SHA1 | Date | |
|---|---|---|---|
| 17839a9e76 | |||
| 27e2c9d5d0 |
BIN
src/AxiOmron.PcbCheck/Assets/AppIcon.ico
Normal file
BIN
src/AxiOmron.PcbCheck/Assets/AppIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
src/AxiOmron.PcbCheck/Assets/AppIcon.png
Normal file
BIN
src/AxiOmron.PcbCheck/Assets/AppIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -5,6 +5,8 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
|
<ApplicationIcon>Assets\AppIcon.ico</ApplicationIcon>
|
||||||
|
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration</InterceptorsNamespaces>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -18,6 +20,10 @@
|
|||||||
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Resource Include="Assets\AppIcon.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="appConfig.json">
|
<None Update="appConfig.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
|||||||
@@ -20,14 +20,15 @@ public sealed class ResultCodeToTagTextConverter : IValueConverter
|
|||||||
/// <param name="parameter">扩展参数。</param>
|
/// <param name="parameter">扩展参数。</param>
|
||||||
/// <param name="culture">当前区域信息。</param>
|
/// <param name="culture">当前区域信息。</param>
|
||||||
/// <returns>简短标签文本。</returns>
|
/// <returns>简短标签文本。</returns>
|
||||||
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
|
ushort code = value switch
|
||||||
{
|
{
|
||||||
ushort u => u,
|
ushort s => s,
|
||||||
int i => (ushort)i,
|
short s when s >= 0 => (ushort)s,
|
||||||
|
int i when i >= 0 && i <= ushort.MaxValue => (ushort)i,
|
||||||
WorkflowResultCode rc => (ushort)rc,
|
WorkflowResultCode rc => (ushort)rc,
|
||||||
_ => (ushort)0
|
_ => 0
|
||||||
};
|
};
|
||||||
|
|
||||||
return code switch
|
return code switch
|
||||||
@@ -47,7 +48,7 @@ public sealed class ResultCodeToTagTextConverter : IValueConverter
|
|||||||
/// <param name="parameter">扩展参数。</param>
|
/// <param name="parameter">扩展参数。</param>
|
||||||
/// <param name="culture">当前区域信息。</param>
|
/// <param name="culture">当前区域信息。</param>
|
||||||
/// <returns>抛出不支持异常。</returns>
|
/// <returns>抛出不支持异常。</returns>
|
||||||
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();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/AxiOmron.PcbCheck/DesignTime/DesignTimePlcService.cs
Normal file
64
src/AxiOmron.PcbCheck/DesignTime/DesignTimePlcService.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using AxiOmron.PcbCheck.Models;
|
||||||
|
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace AxiOmron.PcbCheck.DesignTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为设计器提供 PLC 通信能力的设计时占位服务,所有方法均为无副作用实现。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DesignTimePlcService : IPlcService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 返回设计时输入信号快照。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>空的输入信号快照。</returns>
|
||||||
|
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new PlcSignalSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回设计时监控镜像快照。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>空的监控镜像快照。</returns>
|
||||||
|
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new PlcMonitorSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟写入流程状态,不执行实际设备通信。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state">待写入状态。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>已完成任务。</returns>
|
||||||
|
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟 PLC 重连,不执行实际设备通信。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>已完成任务。</returns>
|
||||||
|
public Task ForceReconnectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回设计时测试写入成功结果,不执行实际设备通信。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">目标地址。</param>
|
||||||
|
/// <param name="type">变量类型。</param>
|
||||||
|
/// <param name="value">写入值文本。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>固定成功结果。</returns>
|
||||||
|
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(PlcWriteResult.Success(address, type, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ public sealed class DesignTimeViewModelLocator
|
|||||||
private readonly DesignTimeAppConfigService _appConfigService = new();
|
private readonly DesignTimeAppConfigService _appConfigService = new();
|
||||||
private readonly DesignTimeAdminUnlockDialogService _adminUnlockDialogService = new();
|
private readonly DesignTimeAdminUnlockDialogService _adminUnlockDialogService = new();
|
||||||
private readonly DesignTimeSftpLookupService _sftpLookupService = new();
|
private readonly DesignTimeSftpLookupService _sftpLookupService = new();
|
||||||
|
private readonly DesignTimePlcService _plcService = new();
|
||||||
private MainWindowViewModel? _mainWindowViewModel;
|
private MainWindowViewModel? _mainWindowViewModel;
|
||||||
private SystemSettingViewModel? _systemSettingViewModel;
|
private SystemSettingViewModel? _systemSettingViewModel;
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ public sealed class DesignTimeViewModelLocator
|
|||||||
/// 获取系统设置设计时视图模型。
|
/// 获取系统设置设计时视图模型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SystemSettingViewModel SystemSettingViewModel
|
public SystemSettingViewModel SystemSettingViewModel
|
||||||
=> _systemSettingViewModel ??= new SystemSettingViewModel(_appConfigService, _sftpLookupService, _appStateStore, _dispatcherService);
|
=> _systemSettingViewModel ??= new SystemSettingViewModel(_appConfigService, _sftpLookupService, _appStateStore, _dispatcherService, _plcService);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建首页设计时视图模型。
|
/// 创建首页设计时视图模型。
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
Height="950"
|
Height="950"
|
||||||
MinWidth="1400"
|
MinWidth="1400"
|
||||||
MinHeight="860"
|
MinHeight="860"
|
||||||
|
Icon="Assets/AppIcon.ico"
|
||||||
Background="{DynamicResource {x:Static hc:ResourceToken.BackgroundBrush}}"
|
Background="{DynamicResource {x:Static hc:ResourceToken.BackgroundBrush}}"
|
||||||
WindowStartupLocation="CenterScreen">
|
WindowStartupLocation="CenterScreen">
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
105
src/AxiOmron.PcbCheck/Models/PlcWriteModels.cs
Normal file
105
src/AxiOmron.PcbCheck/Models/PlcWriteModels.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
namespace AxiOmron.PcbCheck.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示 PLC 测试写入支持的变量类型。
|
||||||
|
/// </summary>
|
||||||
|
public enum PlcWriteType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 布尔位。
|
||||||
|
/// </summary>
|
||||||
|
Bool = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有符号 16 位整数。
|
||||||
|
/// </summary>
|
||||||
|
Int16 = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无符号 16 位整数。
|
||||||
|
/// </summary>
|
||||||
|
UInt16 = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有符号 32 位整数。
|
||||||
|
/// </summary>
|
||||||
|
Int32 = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无符号 32 位整数。
|
||||||
|
/// </summary>
|
||||||
|
UInt32 = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单精度浮点数。
|
||||||
|
/// </summary>
|
||||||
|
Float = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示一次 PLC 测试写入的执行结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record PlcWriteResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取一个值,指示写入是否成功。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取面向用户的结果说明。
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本次写入的目标地址。
|
||||||
|
/// </summary>
|
||||||
|
public string Address { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本次写入的变量类型名称。
|
||||||
|
/// </summary>
|
||||||
|
public string TypeName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本次实际写入的文本值。
|
||||||
|
/// </summary>
|
||||||
|
public string WrittenText { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建一个表示写入成功的结果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">目标地址。</param>
|
||||||
|
/// <param name="type">变量类型。</param>
|
||||||
|
/// <param name="writtenText">实际写入的文本值。</param>
|
||||||
|
/// <returns>成功结果对象。</returns>
|
||||||
|
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})"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建一个表示写入失败的结果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">目标地址。</param>
|
||||||
|
/// <param name="type">变量类型。</param>
|
||||||
|
/// <param name="message">失败说明。</param>
|
||||||
|
/// <returns>失败结果对象。</returns>
|
||||||
|
public static PlcWriteResult Failure(string address, PlcWriteType type, string message)
|
||||||
|
{
|
||||||
|
return new PlcWriteResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
Address = address,
|
||||||
|
TypeName = type.ToString(),
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,6 +129,11 @@ public sealed class PlcOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public byte SourceNodeAddress { get; set; } = 25;
|
public byte SourceNodeAddress { get; set; } = 25;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 PLC 寄存器字节序,默认使用 ABCD 以避免 UInt16 结果代码发生高低字节交换。
|
||||||
|
/// </summary>
|
||||||
|
public string? EndianFormat { get; set; } = "ABCD";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置轮询周期,单位为毫秒。
|
/// 获取或设置轮询周期,单位为毫秒。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project>
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
<PropertyGroup>
|
<Project>
|
||||||
<Configuration>Release</Configuration>
|
<PropertyGroup>
|
||||||
<Platform>Any CPU</Platform>
|
<Configuration>Release</Configuration>
|
||||||
<PublishDir>bin\Release\net8.0-windows\publish\</PublishDir>
|
<Platform>Any CPU</Platform>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishDir>bin\Release\net8.0-windows\publish\</PublishDir>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<_TargetId>Folder</_TargetId>
|
||||||
<SelfContained>false</SelfContained>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<SelfContained>false</SelfContained>
|
||||||
<IncludeNativeLibrariesForSelfExtract>false</IncludeNativeLibrariesForSelfExtract>
|
</PropertyGroup>
|
||||||
<EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>
|
</Project>
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"AxiOmron.PcbCheck": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"environmentVariables": {
|
|
||||||
"DOTNET_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,6 @@ internal static class FileCheckPathMatcher
|
|||||||
public static bool IsMatch(string fileName, string candidateName, string barcode)
|
public static bool IsMatch(string fileName, string candidateName, string barcode)
|
||||||
{
|
{
|
||||||
return fileName.Equals(candidateName, StringComparison.OrdinalIgnoreCase)
|
return fileName.Equals(candidateName, StringComparison.OrdinalIgnoreCase)
|
||||||
|| WildcardMatcher.IsMatch(fileName, candidateName)
|
|| WildcardMatcher.IsMatch(fileName, candidateName);
|
||||||
|| fileName.Contains(barcode, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,6 +155,54 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向指定地址写入一个原始值,供调试界面手动测试使用。
|
||||||
|
/// 复用与回读相同的 <see cref="_ioLock"/> 串行化,且不修改 <see cref="_lastWrittenState"/>,因此不会干扰后台轮询回读与流程状态机写入。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">完整的 FINS 地址字符串,例如 <c>D100</c> 或 <c>D100.0</c>。</param>
|
||||||
|
/// <param name="type">写入的变量类型。</param>
|
||||||
|
/// <param name="value">待写入值的文本表示。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。取消后立即终止本次写入并返回已取消任务。</param>
|
||||||
|
/// <returns>包含成功状态与说明的写入结果。</returns>
|
||||||
|
public async Task<PlcWriteResult> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 释放 PLC 通信资源。
|
/// 释放 PLC 通信资源。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -181,7 +229,8 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
DisconnectUnsafe();
|
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,
|
UnitAddress = _options.UnitAddress,
|
||||||
SA1 = _options.SourceNodeAddress
|
SA1 = _options.SourceNodeAddress
|
||||||
@@ -189,7 +238,28 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
|
|||||||
var openResult = client.Open();
|
var openResult = client.Open();
|
||||||
EnsureSuccess(openResult.IsSucceed, openResult.Err, "连接 OMRON FINS PLC 失败");
|
EnsureSuccess(openResult.IsSucceed, openResult.Err, "连接 OMRON FINS PLC 失败");
|
||||||
_client = client;
|
_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析 PLC 字节序配置。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">PLC 通信配置。</param>
|
||||||
|
/// <returns>IoTClient 使用的字节序枚举。</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="options"/> 为空时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当字节序配置为空或无法识别时抛出。</exception>
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -206,7 +276,7 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取单个 D 区无符号整数。
|
/// 读取单个 D 区无符号 16 位整数。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">FINS 地址。</param>
|
/// <param name="address">FINS 地址。</param>
|
||||||
/// <returns>读取到的寄存器值。</returns>
|
/// <returns>读取到的寄存器值。</returns>
|
||||||
@@ -225,9 +295,10 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
|
|||||||
private void WriteChangedState(PlcProcessState state)
|
private void WriteChangedState(PlcProcessState state)
|
||||||
{
|
{
|
||||||
var previous = _lastWrittenState;
|
var previous = _lastWrittenState;
|
||||||
|
ushort? previousResultCode = previous is null ? null : previous.ResultCode;
|
||||||
WriteBooleanIfChanged(previous?.PcBusy, state.PcBusy, ToDataMemoryAddress(_options.Outputs.PcBusy));
|
WriteBooleanIfChanged(previous?.PcBusy, state.PcBusy, ToDataMemoryAddress(_options.Outputs.PcBusy));
|
||||||
WriteBooleanIfChanged(previous?.ReleasePermit, state.ReleasePermit, ToDataMemoryAddress(_options.Outputs.ReleasePermit));
|
WriteBooleanIfChanged(previous?.ReleasePermit, state.ReleasePermit, ToDataMemoryAddress(_options.Outputs.ReleasePermit));
|
||||||
WriteUInt16IfChanged(previous?.ResultCode, state.ResultCode, ToDataMemoryAddress(_options.Registers.ResultCode));
|
WriteUInt16IfChanged(previousResultCode, state.ResultCode, ToDataMemoryAddress(_options.Registers.ResultCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -249,7 +320,7 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 仅当值发生变化时写入单个 D 区无符号整数。
|
/// 仅当值发生变化时写入单个 D 区无符号 16 位整数。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="previous">上一值。</param>
|
/// <param name="previous">上一值。</param>
|
||||||
/// <param name="current">当前值。</param>
|
/// <param name="current">当前值。</param>
|
||||||
@@ -266,6 +337,147 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
|
|||||||
EnsureSuccess(result.IsSucceed, result.Err, $"写入 FINS UInt16 失败,地址={address},值={current}");
|
EnsureSuccess(result.IsSucceed, result.Err, $"写入 FINS UInt16 失败,地址={address},值={current}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按变量类型将文本值解析为强类型对象。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">变量类型。</param>
|
||||||
|
/// <param name="value">待解析的文本值。</param>
|
||||||
|
/// <param name="parsedValue">解析成功时返回的强类型值。</param>
|
||||||
|
/// <param name="error">解析失败时返回的错误说明。</param>
|
||||||
|
/// <returns>解析成功返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析布尔文本,接受 1/0 与 true/false(忽略大小写)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">待解析文本。</param>
|
||||||
|
/// <param name="result">解析结果。</param>
|
||||||
|
/// <returns>解析成功返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按强类型值调用 IoTClient 对应写入重载。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">FINS 地址。</param>
|
||||||
|
/// <param name="parsedValue">已解析的强类型值。</param>
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将强类型值格式化为用于展示的文本。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parsedValue">已解析的强类型值。</param>
|
||||||
|
/// <returns>文本表示。</returns>
|
||||||
|
private static string FormatWrittenText(object parsedValue)
|
||||||
|
{
|
||||||
|
return parsedValue switch
|
||||||
|
{
|
||||||
|
float floatValue => floatValue.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
_ => parsedValue.ToString() ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将数字形式 D 区地址转换为 IoTClient FINS 地址字符串。
|
/// 将数字形式 D 区地址转换为 IoTClient FINS 地址字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -124,6 +124,17 @@ public interface IPlcService
|
|||||||
/// <param name="cancellationToken">取消令牌。</param>
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
/// <returns>表示重连完成的任务。</returns>
|
/// <returns>表示重连完成的任务。</returns>
|
||||||
Task ForceReconnectAsync(CancellationToken cancellationToken);
|
Task ForceReconnectAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向指定地址写入一个原始值,供调试界面手动测试使用。
|
||||||
|
/// 该写入复用与回读相同的 IO 串行化机制,且不会影响流程状态机的写入缓存,因此不会干扰后台轮询回读。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">完整的 FINS 地址字符串,例如 <c>D100</c> 或 <c>D100.0</c>。</param>
|
||||||
|
/// <param name="type">写入的变量类型。</param>
|
||||||
|
/// <param name="value">待写入值的文本表示,由实现按 <paramref name="type"/> 解析。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。取消后立即终止本次写入并返回已取消任务。</param>
|
||||||
|
/// <returns>包含成功状态与说明的写入结果。</returns>
|
||||||
|
Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -182,6 +182,12 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _lastLogUpdateTime = "-";
|
private string _lastLogUpdateTime = "-";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置运行日志区域是否展开;该状态仅影响首页布局,不参与业务流程。
|
||||||
|
/// </summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isLogAreaExpanded;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置今日异常日志条数。
|
/// 获取或设置今日异常日志条数。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -367,6 +373,15 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
LastLogUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
LastLogUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换运行日志区域展开状态,收起或恢复上方状态概览区域。
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleLogArea()
|
||||||
|
{
|
||||||
|
IsLogAreaExpanded = !IsLogAreaExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 打开追踪区命令占位。后续应由导航服务跳转至追踪页面。
|
/// 打开追踪区命令占位。后续应由导航服务跳转至追踪页面。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -542,6 +557,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
await AppendLogOnUiAsync(entry).ConfigureAwait(false);
|
await AppendLogOnUiAsync(entry).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
/// 处理管理员解锁倒计时,超时后自动恢复锁定。
|
/// 处理管理员解锁倒计时,超时后自动恢复锁定。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sender">事件源。</param>
|
/// <param name="sender">事件源。</param>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using AxiOmron.PcbCheck.Options;
|
|||||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||||
using AxiOmron.PcbCheck.Models;
|
using AxiOmron.PcbCheck.Models;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace AxiOmron.PcbCheck.ViewModels;
|
namespace AxiOmron.PcbCheck.ViewModels;
|
||||||
|
|
||||||
@@ -17,6 +16,7 @@ public partial class SystemSettingViewModel : ObservableObject
|
|||||||
private readonly IFileCheckService _fileCheckService;
|
private readonly IFileCheckService _fileCheckService;
|
||||||
private readonly IAppStateStore _appStateStore;
|
private readonly IAppStateStore _appStateStore;
|
||||||
private readonly IDispatcherService _dispatcherService;
|
private readonly IDispatcherService _dispatcherService;
|
||||||
|
private readonly IPlcService _plcService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化系统设置视图模型。
|
/// 初始化系统设置视图模型。
|
||||||
@@ -25,20 +25,24 @@ public partial class SystemSettingViewModel : ObservableObject
|
|||||||
/// <param name="fileCheckService">文件校验通道测试服务。</param>
|
/// <param name="fileCheckService">文件校验通道测试服务。</param>
|
||||||
/// <param name="appStateStore">运行态快照存储。</param>
|
/// <param name="appStateStore">运行态快照存储。</param>
|
||||||
/// <param name="dispatcherService">UI 线程调度服务。</param>
|
/// <param name="dispatcherService">UI 线程调度服务。</param>
|
||||||
|
/// <param name="plcService">PLC 通信服务,用于调试界面的测试写入。</param>
|
||||||
public SystemSettingViewModel(
|
public SystemSettingViewModel(
|
||||||
IAppConfigService appConfigService,
|
IAppConfigService appConfigService,
|
||||||
IFileCheckService fileCheckService,
|
IFileCheckService fileCheckService,
|
||||||
IAppStateStore appStateStore,
|
IAppStateStore appStateStore,
|
||||||
IDispatcherService dispatcherService)
|
IDispatcherService dispatcherService,
|
||||||
|
IPlcService plcService)
|
||||||
{
|
{
|
||||||
_appConfigService = appConfigService ?? throw new ArgumentNullException(nameof(appConfigService));
|
_appConfigService = appConfigService ?? throw new ArgumentNullException(nameof(appConfigService));
|
||||||
_fileCheckService = fileCheckService ?? throw new ArgumentNullException(nameof(fileCheckService));
|
_fileCheckService = fileCheckService ?? throw new ArgumentNullException(nameof(fileCheckService));
|
||||||
_appStateStore = appStateStore ?? throw new ArgumentNullException(nameof(appStateStore));
|
_appStateStore = appStateStore ?? throw new ArgumentNullException(nameof(appStateStore));
|
||||||
_dispatcherService = dispatcherService ?? throw new ArgumentNullException(nameof(dispatcherService));
|
_dispatcherService = dispatcherService ?? throw new ArgumentNullException(nameof(dispatcherService));
|
||||||
|
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
|
||||||
EditableConfig = _appConfigService.Load();
|
EditableConfig = _appConfigService.Load();
|
||||||
ConfigPath = _appConfigService.GetConfigPath();
|
ConfigPath = _appConfigService.GetConfigPath();
|
||||||
PlcMonitorItems = new ObservableCollection<PlcMonitorItem>();
|
PlcMonitorItems = new ObservableCollection<PlcMonitorItem>();
|
||||||
FileCheckProviders = Enum.GetValues<FileCheckProvider>();
|
FileCheckProviders = Enum.GetValues<FileCheckProvider>();
|
||||||
|
PlcWriteTypes = Enum.GetValues<PlcWriteType>();
|
||||||
ApplySnapshot(_appStateStore.GetSnapshot());
|
ApplySnapshot(_appStateStore.GetSnapshot());
|
||||||
_appStateStore.SnapshotChanged += OnSnapshotChanged;
|
_appStateStore.SnapshotChanged += OnSnapshotChanged;
|
||||||
StatusMessage = "已加载配置。";
|
StatusMessage = "已加载配置。";
|
||||||
@@ -54,6 +58,11 @@ public partial class SystemSettingViewModel : ObservableObject
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<FileCheckProvider> FileCheckProviders { get; }
|
public IReadOnlyList<FileCheckProvider> FileCheckProviders { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 PLC 测试写入支持的变量类型可选项。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PlcWriteType> PlcWriteTypes { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置可编辑配置对象。
|
/// 获取或设置可编辑配置对象。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -79,6 +88,37 @@ public partial class SystemSettingViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isTestingFileCheckConnection;
|
private bool _isTestingFileCheckConnection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置测试写入的目标地址,例如 D100、D100.0。
|
||||||
|
/// </summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _plcWriteAddress = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置测试写入的变量类型。
|
||||||
|
/// </summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private PlcWriteType _selectedPlcWriteType = PlcWriteType.Int16;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置测试写入的值文本。
|
||||||
|
/// </summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _plcWriteValue = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置测试写入的状态提示文本。
|
||||||
|
/// </summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _plcWriteStatusMessage = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置当前是否正在执行 PLC 测试写入。
|
||||||
|
/// </summary>
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(WritePlcValueCommand))]
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isWritingPlc;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 保存当前配置。
|
/// 保存当前配置。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -143,6 +183,44 @@ public partial class SystemSettingViewModel : ObservableObject
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private bool CanTestFileCheckConnection => !IsTestingFileCheckConnection;
|
private bool CanTestFileCheckConnection => !IsTestingFileCheckConnection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行一次 PLC 测试写入。该写入复用回读相同的 IO 串行化,不会干扰后台轮询回读。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>表示写入完成的任务。</returns>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断当前是否允许执行 PLC 测试写入。
|
||||||
|
/// </summary>
|
||||||
|
private bool CanWritePlcValue => !IsWritingPlc;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将运行态快照中的 PLC 监控数据同步到当前视图模型。
|
/// 将运行态快照中的 PLC 监控数据同步到当前视图模型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -639,11 +639,12 @@
|
|||||||
<Grid Grid.Row="2" Grid.Column="0">
|
<Grid Grid.Row="2" Grid.Column="0">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="16" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Grid Grid.Row="0">
|
<Grid Grid.Row="0"
|
||||||
|
Visibility="{Binding IsLogAreaExpanded, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Invert}">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="1.12*" />
|
<ColumnDefinition Width="1.12*" />
|
||||||
<ColumnDefinition Width="12" />
|
<ColumnDefinition Width="12" />
|
||||||
@@ -775,6 +776,10 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Border Grid.Row="1"
|
||||||
|
Height="16"
|
||||||
|
Visibility="{Binding IsLogAreaExpanded, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Invert}" />
|
||||||
|
|
||||||
<Border Grid.Row="2" Style="{StaticResource ModernCardStyle}">
|
<Border Grid.Row="2" Style="{StaticResource ModernCardStyle}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -809,17 +814,29 @@
|
|||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="8" />
|
<ColumnDefinition Width="8" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="8" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Button Grid.Column="0"
|
<Button Grid.Column="0"
|
||||||
|
Style="{StaticResource ToolbarButtonStyle}"
|
||||||
|
Command="{Binding ToggleLogAreaCommand}">
|
||||||
|
<Grid>
|
||||||
|
<TextBlock Text="展开日志"
|
||||||
|
Visibility="{Binding IsLogAreaExpanded, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Invert}" />
|
||||||
|
<TextBlock Text="恢复概览"
|
||||||
|
Visibility="{Binding IsLogAreaExpanded, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="2"
|
||||||
Content="全部"
|
Content="全部"
|
||||||
Style="{StaticResource ToolbarButtonStyle}"
|
Style="{StaticResource ToolbarButtonStyle}"
|
||||||
Command="{Binding ShowAllLogsCommand}" />
|
Command="{Binding ShowAllLogsCommand}" />
|
||||||
<Button Grid.Column="2"
|
<Button Grid.Column="4"
|
||||||
Content="仅错误"
|
Content="仅错误"
|
||||||
Style="{StaticResource ToolbarButtonStyle}"
|
Style="{StaticResource ToolbarButtonStyle}"
|
||||||
Command="{Binding ShowErrorLogsCommand}" />
|
Command="{Binding ShowErrorLogsCommand}" />
|
||||||
<Button Grid.Column="4"
|
<Button Grid.Column="6"
|
||||||
Content="导出"
|
Content="导出"
|
||||||
Style="{StaticResource ToolbarButtonStyle}"
|
Style="{StaticResource ToolbarButtonStyle}"
|
||||||
Command="{Binding ExportLogsCommand}" />
|
Command="{Binding ExportLogsCommand}" />
|
||||||
|
|||||||
@@ -140,6 +140,8 @@
|
|||||||
<TabItem Header="PLC监控">
|
<TabItem Header="PLC监控">
|
||||||
<Grid Margin="16">
|
<Grid Margin="16">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="12" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="12" />
|
<RowDefinition Height="12" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
@@ -153,6 +155,60 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Border Grid.Row="2"
|
<Border Grid.Row="2"
|
||||||
|
Background="White"
|
||||||
|
BorderBrush="#E8ECF3"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="10"
|
||||||
|
Padding="16,14">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="测试写入" FontSize="15" FontWeight="SemiBold" Foreground="#1F2937" />
|
||||||
|
<TextBlock Text="手动向指定地址写入测试值,仅用于调试,不影响实时回读。"
|
||||||
|
Margin="0,4,0,0"
|
||||||
|
Foreground="#6B7280" />
|
||||||
|
<Grid Margin="0,12,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="2*" />
|
||||||
|
<ColumnDefinition Width="12" />
|
||||||
|
<ColumnDefinition Width="1.4*" />
|
||||||
|
<ColumnDefinition Width="12" />
|
||||||
|
<ColumnDefinition Width="2*" />
|
||||||
|
<ColumnDefinition Width="12" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0"
|
||||||
|
Style="{StaticResource TextBoxExtend}"
|
||||||
|
hc:InfoElement.Title="地址"
|
||||||
|
hc:InfoElement.TitlePlacement="Top"
|
||||||
|
hc:InfoElement.Placeholder="如 D100、D100.0"
|
||||||
|
Text="{Binding PlcWriteAddress, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<ComboBox Grid.Column="2"
|
||||||
|
Style="{StaticResource ComboBoxExtend}"
|
||||||
|
hc:InfoElement.Title="变量类型"
|
||||||
|
hc:InfoElement.TitlePlacement="Top"
|
||||||
|
ItemsSource="{Binding PlcWriteTypes}"
|
||||||
|
SelectedItem="{Binding SelectedPlcWriteType}" />
|
||||||
|
<TextBox Grid.Column="4"
|
||||||
|
Style="{StaticResource TextBoxExtend}"
|
||||||
|
hc:InfoElement.Title="写入值"
|
||||||
|
hc:InfoElement.TitlePlacement="Top"
|
||||||
|
hc:InfoElement.Placeholder="Bool 用 1/0 或 true/false"
|
||||||
|
Text="{Binding PlcWriteValue, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<Button Grid.Column="6"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Style="{StaticResource ButtonPrimary}"
|
||||||
|
Padding="18,8"
|
||||||
|
MinHeight="36"
|
||||||
|
Content="写入"
|
||||||
|
Command="{Binding WritePlcValueCommand}" />
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding PlcWriteStatusMessage}"
|
||||||
|
Margin="0,10,0,0"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="#2563EB" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="4"
|
||||||
Background="White"
|
Background="White"
|
||||||
BorderBrush="#E8ECF3"
|
BorderBrush="#E8ECF3"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
|
|||||||
@@ -1,91 +1,92 @@
|
|||||||
{
|
{
|
||||||
"Plc": {
|
"Plc": {
|
||||||
"Host": "127.0.0.1",
|
"Host": "192.168.2.100",
|
||||||
"Port": 9600,
|
"Port": 9600,
|
||||||
"UnitAddress": 0,
|
"UnitAddress": 0,
|
||||||
"SourceNodeAddress": 25,
|
"SourceNodeAddress": 0,
|
||||||
"PollIntervalMs": 200,
|
"EndianFormat": "ABCD",
|
||||||
"ConnectTimeoutMs": 3000,
|
"PollIntervalMs": 200,
|
||||||
"ReleasePulseMs": 500,
|
"ConnectTimeoutMs": 3000,
|
||||||
"ReleaseAckTimeoutMs": 2000,
|
"ReleasePulseMs": 500,
|
||||||
"Inputs": {
|
"ReleaseAckTimeoutMs": 2000,
|
||||||
"PcbArrived": 1001,
|
"Inputs": {
|
||||||
"PlcReset": 1002,
|
"PcbArrived": 101,
|
||||||
"PlcAckRelease": 1003
|
"PlcReset": 102,
|
||||||
},
|
"PlcAckRelease": 103
|
||||||
"Outputs": {
|
},
|
||||||
"PcBusy": 1051,
|
"Outputs": {
|
||||||
"ReleasePermit": 1052
|
"PcBusy": 51,
|
||||||
},
|
"ReleasePermit": 52
|
||||||
"Registers": {
|
},
|
||||||
"ResultCode": 1100
|
"Registers": {
|
||||||
}
|
"ResultCode": 401
|
||||||
},
|
}
|
||||||
"Scanner": {
|
},
|
||||||
"Scanner1": {
|
"Scanner": {
|
||||||
"Name": "扫码枪1",
|
"Scanner1": {
|
||||||
"Host": "127.0.0.1",
|
"Name": "\u626B\u7801\u67AA1",
|
||||||
"Port": 9001
|
"Host": "192.168.2.13",
|
||||||
},
|
"Port": 2001
|
||||||
"Scanner2": {
|
},
|
||||||
"Name": "扫码枪2",
|
"Scanner2": {
|
||||||
"Host": "127.0.0.1",
|
"Name": "\u626B\u7801\u67AA2",
|
||||||
"Port": 9002
|
"Host": "192.168.2.14",
|
||||||
},
|
"Port": 2001
|
||||||
"ConnectTimeoutMs": 3000,
|
},
|
||||||
"ReadTimeoutMs": 3000,
|
"ConnectTimeoutMs": 3000,
|
||||||
"TriggerCommand": "T",
|
"TriggerCommand": "T",
|
||||||
"ResponseTerminator": "\\r",
|
"ResponseTerminator": "",
|
||||||
"TriggerInParallel": true
|
"TriggerInParallel": true,
|
||||||
},
|
"ReadTimeoutMs": 5000
|
||||||
"FileCheck": {
|
},
|
||||||
"Provider": "Sftp",
|
"FileCheck": {
|
||||||
"FileNamePattern": "${barcode}.txt",
|
"Provider": "Local",
|
||||||
"RetryIntervalSeconds": 2,
|
"FileNamePattern": "${barcode}.txt",
|
||||||
"MaxRetryCount": 3,
|
"RetryIntervalSeconds": 2,
|
||||||
"Local": {
|
"MaxRetryCount": 3,
|
||||||
"RootPath": "D:\\PcbCheck\\Files"
|
"Local": {
|
||||||
}
|
"RootPath": "D:\\test"
|
||||||
},
|
}
|
||||||
"Sftp": {
|
},
|
||||||
"Host": "127.0.0.1",
|
"Sftp": {
|
||||||
"Port": 22,
|
"Host": "127.0.0.1",
|
||||||
"Username": "uaesadmin",
|
"Port": 22,
|
||||||
"Password": "uaes,123",
|
"Username": "uaesadmin",
|
||||||
"PrivateKeyPath": "",
|
"Password": "uaes,123",
|
||||||
"PrivateKeyPassphrase": "",
|
"PrivateKeyPath": "",
|
||||||
"RootPath": "/home/uaesadmin/",
|
"PrivateKeyPassphrase": "",
|
||||||
"FileNamePattern": "${barcode}.txt",
|
"RootPath": "/home/uaesadmin/",
|
||||||
"RetryIntervalSeconds": 2,
|
"FileNamePattern": "${barcode}.txt",
|
||||||
"MaxRetryCount": 3,
|
"RetryIntervalSeconds": 2,
|
||||||
"ConnectTimeoutMs": 3000
|
"MaxRetryCount": 3,
|
||||||
},
|
"ConnectTimeoutMs": 3000
|
||||||
"Andon": {
|
},
|
||||||
"Enable": true,
|
"Andon": {
|
||||||
"Url": "https://shp.prod.aliyun.mit.uaes.com/api/mit/MAE/Station/Status/alarm",
|
"Enable": false,
|
||||||
"Method": "POST",
|
"Url": "https://shp.prod.aliyun.mit.uaes.com/api/mit/MAE/Station/Status/alarm",
|
||||||
"TimeoutMs": 3000,
|
"Method": "POST",
|
||||||
"AndonTheme": "SS7_Auto_Andon",
|
"TimeoutMs": 3000,
|
||||||
"Eid": "PR1965269806334783488",
|
"AndonTheme": "SS7_Auto_Andon",
|
||||||
"FromSign": "IOT",
|
"Eid": "PR1965269806334783488",
|
||||||
"ServiceId": "4dccd822-86b2-47dd-89bd-ca472ba0d14d",
|
"FromSign": "IOT",
|
||||||
"EnableScanFailAlarm": true,
|
"ServiceId": "4dccd822-86b2-47dd-89bd-ca472ba0d14d",
|
||||||
"EnableFileNotFoundAlarm": false,
|
"EnableScanFailAlarm": false,
|
||||||
"Headers": {
|
"EnableFileNotFoundAlarm": false,
|
||||||
"Accept": "application/json"
|
"Headers": {
|
||||||
}
|
"Accept": "application/json"
|
||||||
},
|
}
|
||||||
"Workflow": {
|
},
|
||||||
"RequireManualResetAfterFault": true,
|
"Workflow": {
|
||||||
"MaxUiLogEntries": 200,
|
"RequireManualResetAfterFault": true,
|
||||||
"MaxBoardRecords": 100
|
"MaxUiLogEntries": 200,
|
||||||
},
|
"MaxBoardRecords": 100
|
||||||
"RecordPersistence": {
|
},
|
||||||
"DirectoryPath": "Data\\Records",
|
"RecordPersistence": {
|
||||||
"DayChangeCheckIntervalSeconds": 30
|
"DirectoryPath": "Data\\Records",
|
||||||
},
|
"DayChangeCheckIntervalSeconds": 30
|
||||||
"Security": {
|
},
|
||||||
"AdminPassword": "AxiOmron@123",
|
"Security": {
|
||||||
"AdminSessionTimeoutMinutes": 15
|
"AdminPassword": "AxiOmron@123",
|
||||||
}
|
"AdminSessionTimeoutMinutes": 15
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -89,20 +89,20 @@ public sealed class LocalFileCheckProviderTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件名包含条码时,应返回命中文件路径。
|
/// 文件名只包含条码但不等于模板生成文件名时,不应命中。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>异步测试任务。</returns>
|
/// <returns>异步测试任务。</returns>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckFileAsync_ShouldMatchFileNameContainingBarcode()
|
public async Task CheckFileAsync_ShouldNotMatchFileNameContainingBarcodeOnly()
|
||||||
{
|
{
|
||||||
string filePath = Path.Combine(_rootPath, "prefix-PCB-002-result.dat");
|
string filePath = Path.Combine(_rootPath, "abcd10.txt");
|
||||||
await File.WriteAllTextAsync(filePath, "ok");
|
await File.WriteAllTextAsync(filePath, "ok");
|
||||||
var provider = CreateProvider();
|
var provider = CreateProvider();
|
||||||
|
|
||||||
FileCheckOutcome outcome = await provider.CheckFileAsync("PCB-002", CreateConfig(_rootPath), CancellationToken.None);
|
FileCheckOutcome outcome = await provider.CheckFileAsync("abcd1", CreateConfig(_rootPath), CancellationToken.None);
|
||||||
|
|
||||||
Assert.True(outcome.Exists);
|
Assert.False(outcome.Exists);
|
||||||
Assert.Equal(filePath, outcome.MatchedFilePath);
|
Assert.Empty(outcome.MatchedFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -131,10 +131,31 @@ public sealed class MainWindowViewModelTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 日志区域展开命令应只切换界面折叠状态,不影响运行数据。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ToggleLogArea_ShouldSwitchExpandedState()
|
||||||
|
{
|
||||||
|
var stateStore = new AppStateStore();
|
||||||
|
var viewModel = CreateViewModel(stateStore);
|
||||||
|
|
||||||
|
Assert.False(viewModel.IsLogAreaExpanded);
|
||||||
|
|
||||||
|
viewModel.ToggleLogAreaCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.True(viewModel.IsLogAreaExpanded);
|
||||||
|
|
||||||
|
viewModel.ToggleLogAreaCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.False(viewModel.IsLogAreaExpanded);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建测试用首页视图模型。
|
/// 创建测试用首页视图模型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stateStore">运行态存储。</param>
|
/// <param name="stateStore">运行态存储。</param>
|
||||||
|
/// <param name="dispatcherService">可选的 UI 调度服务。</param>
|
||||||
/// <returns>首页视图模型。</returns>
|
/// <returns>首页视图模型。</returns>
|
||||||
private static MainWindowViewModel CreateViewModel(IAppStateStore stateStore, IDispatcherService? dispatcherService = null)
|
private static MainWindowViewModel CreateViewModel(IAppStateStore stateStore, IDispatcherService? dispatcherService = null)
|
||||||
{
|
{
|
||||||
|
|||||||
41
tests/AxiOmron.PcbCheck.Tests/OmronFinsPlcServiceTests.cs
Normal file
41
tests/AxiOmron.PcbCheck.Tests/OmronFinsPlcServiceTests.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using AxiOmron.PcbCheck.Options;
|
||||||
|
using AxiOmron.PcbCheck.Services.Implementations;
|
||||||
|
using IoTClient.Enums;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AxiOmron.PcbCheck.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证欧姆龙 FINS PLC 服务的通信参数解析行为。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OmronFinsPlcServiceTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 默认字节序应使用 ABCD,避免 UInt16 结果代码发生高低字节交换。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ResolveEndianFormat_ShouldUseAbcd_WhenConfigurationIsDefault()
|
||||||
|
{
|
||||||
|
var options = new PlcOptions();
|
||||||
|
|
||||||
|
EndianFormat result = OmronFinsPlcService.ResolveEndianFormat(options);
|
||||||
|
|
||||||
|
Assert.Equal(EndianFormat.ABCD, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非法字节序配置应抛出明确异常,避免使用错误端序静默读写 PLC 寄存器。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ResolveEndianFormat_ShouldThrow_WhenConfigurationIsInvalid()
|
||||||
|
{
|
||||||
|
var options = new PlcOptions
|
||||||
|
{
|
||||||
|
EndianFormat = "XYZ"
|
||||||
|
};
|
||||||
|
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => OmronFinsPlcService.ResolveEndianFormat(options));
|
||||||
|
|
||||||
|
Assert.Contains("PLC 字节序配置无效", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
tests/AxiOmron.PcbCheck.Tests/ResultCodeTypeTests.cs
Normal file
33
tests/AxiOmron.PcbCheck.Tests/ResultCodeTypeTests.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using AxiOmron.PcbCheck.Models;
|
||||||
|
using AxiOmron.PcbCheck.ViewModels;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AxiOmron.PcbCheck.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证结果代码在模型与界面层统一使用 16 位无符号整数。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ResultCodeTypeTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 结果代码枚举应以 UInt16 作为底层类型,匹配 PLC 的 ResultCode 定义。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void WorkflowResultCode_ShouldUseUInt16UnderlyingType()
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(ushort), Enum.GetUnderlyingType(typeof(WorkflowResultCode)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结果代码属性应统一使用 UInt16,避免经过界面或快照时被误建模为 Int16 或 Int32。
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(RuntimeSnapshot))]
|
||||||
|
[InlineData(typeof(PlcProcessState))]
|
||||||
|
[InlineData(typeof(BoardProcessRecord))]
|
||||||
|
[InlineData(typeof(MainWindowViewModel))]
|
||||||
|
public void ResultCodeProperties_ShouldUseUInt16(Type ownerType)
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(ushort), ownerType.GetProperty("ResultCode")?.PropertyType);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ public sealed class SystemSettingViewModelTests
|
|||||||
StatusMessage = "SFTP 连接成功,根目录可访问。"
|
StatusMessage = "SFTP 连接成功,根目录可访问。"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var viewModel = new SystemSettingViewModel(configService, fileCheckService, stateStore, dispatcherService);
|
var viewModel = new SystemSettingViewModel(configService, fileCheckService, stateStore, dispatcherService, new FakePlcService());
|
||||||
|
|
||||||
await viewModel.TestFileCheckConnectionCommand.ExecuteAsync(null);
|
await viewModel.TestFileCheckConnectionCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ public sealed class SystemSettingViewModelTests
|
|||||||
StatusMessage = "SFTP 连接失败: timeout"
|
StatusMessage = "SFTP 连接失败: timeout"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var viewModel = new SystemSettingViewModel(configService, fileCheckService, stateStore, dispatcherService);
|
var viewModel = new SystemSettingViewModel(configService, fileCheckService, stateStore, dispatcherService, new FakePlcService());
|
||||||
|
|
||||||
await viewModel.TestFileCheckConnectionCommand.ExecuteAsync(null);
|
await viewModel.TestFileCheckConnectionCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ public sealed class SystemSettingViewModelTests
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var viewModel = new SystemSettingViewModel(configService, new FakeFileCheckService(), stateStore, dispatcherService);
|
var viewModel = new SystemSettingViewModel(configService, new FakeFileCheckService(), stateStore, dispatcherService, new FakePlcService());
|
||||||
|
|
||||||
Assert.Single(viewModel.PlcMonitorItems);
|
Assert.Single(viewModel.PlcMonitorItems);
|
||||||
Assert.Equal("PcbArrived", viewModel.PlcMonitorItems[0].Name);
|
Assert.Equal("PcbArrived", viewModel.PlcMonitorItems[0].Name);
|
||||||
@@ -104,7 +104,7 @@ public sealed class SystemSettingViewModelTests
|
|||||||
var configService = new FakeAppConfigService();
|
var configService = new FakeAppConfigService();
|
||||||
var stateStore = new AppStateStore();
|
var stateStore = new AppStateStore();
|
||||||
var dispatcherService = new CapturedDispatcherService(Dispatcher.CurrentDispatcher);
|
var dispatcherService = new CapturedDispatcherService(Dispatcher.CurrentDispatcher);
|
||||||
var viewModel = new SystemSettingViewModel(configService, new FakeFileCheckService(), stateStore, dispatcherService);
|
var viewModel = new SystemSettingViewModel(configService, new FakeFileCheckService(), stateStore, dispatcherService, new FakePlcService());
|
||||||
_ = CollectionViewSource.GetDefaultView(viewModel.PlcMonitorItems);
|
_ = CollectionViewSource.GetDefaultView(viewModel.PlcMonitorItems);
|
||||||
|
|
||||||
Exception? capturedException = null;
|
Exception? capturedException = null;
|
||||||
@@ -160,7 +160,8 @@ public sealed class SystemSettingViewModelTests
|
|||||||
configService,
|
configService,
|
||||||
new FakeFileCheckService(),
|
new FakeFileCheckService(),
|
||||||
stateStore,
|
stateStore,
|
||||||
new CanceledDispatcherService());
|
new CanceledDispatcherService(),
|
||||||
|
new FakePlcService());
|
||||||
var synchronizationContext = new ExceptionCapturingSynchronizationContext();
|
var synchronizationContext = new ExceptionCapturingSynchronizationContext();
|
||||||
SynchronizationContext? originalContext = SynchronizationContext.Current;
|
SynchronizationContext? originalContext = SynchronizationContext.Current;
|
||||||
|
|
||||||
@@ -296,6 +297,37 @@ public sealed class SystemSettingViewModelTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供系统设置页测试用 PLC 服务替身。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class FakePlcService : IPlcService
|
||||||
|
{
|
||||||
|
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new PlcSignalSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new PlcMonitorSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ForceReconnectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(PlcWriteResult.Success(address, type, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 提供立即执行的测试用 Dispatcher,实现与设计时调度一致的同步行为。
|
/// 提供立即执行的测试用 Dispatcher,实现与设计时调度一致的同步行为。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -418,6 +418,7 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken) => Task.FromResult(new PlcMonitorSnapshot());
|
public Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken) => Task.FromResult(new PlcMonitorSnapshot());
|
||||||
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken) => Task.FromResult(new PlcSignalSnapshot());
|
public Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken) => Task.FromResult(new PlcSignalSnapshot());
|
||||||
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken) => Task.FromResult(PlcWriteResult.Success(address, type, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ProbingPlcService : IPlcService
|
private sealed class ProbingPlcService : IPlcService
|
||||||
@@ -460,6 +461,11 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(PlcWriteResult.Success(address, type, value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FailingPlcService : IPlcService
|
private sealed class FailingPlcService : IPlcService
|
||||||
@@ -483,6 +489,11 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("PLC unreachable");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class SequencedPlcService : IPlcService
|
private sealed class SequencedPlcService : IPlcService
|
||||||
@@ -529,6 +540,11 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
WrittenStates.Add(state.Clone());
|
WrittenStates.Add(state.Clone());
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<PlcWriteResult> WriteRawValueAsync(string address, PlcWriteType type, string value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(PlcWriteResult.Success(address, type, value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FakeScannerService : IScannerService
|
private sealed class FakeScannerService : IScannerService
|
||||||
|
|||||||
Reference in New Issue
Block a user