✨ feat(*): 更新 PLC 相关功能与配置
- 在 appConfig.json 中添加 EndianFormat 配置项并更新相关输入输出地址 - 修改 ResultCode 类型为 short,确保与 PLC 通信一致 - 新增 DesignTimePlcService 以支持设计时的 PLC 测试写入功能 - 更新 SystemSettingViewModel 和 DashboardPage 以支持手动测试写入 - 调整多个视图模型和服务以适应新的 PLC 写入逻辑 - 更新相关单元测试以验证新功能的正确性
This commit is contained in:
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>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using AxiOmron.PcbCheck.Models;
|
|||||||
namespace AxiOmron.PcbCheck.Converters;
|
namespace AxiOmron.PcbCheck.Converters;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 <see cref="WorkflowResultCode"/>(以 <see cref="ushort"/> 存储)映射为简短 Tag 文本。
|
/// 将 <see cref="WorkflowResultCode"/>(以 <see cref="short"/> 存储)映射为简短 Tag 文本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// 映射规则:Passed → "OK";Processing → "处理中";None → "-";其余视为 "NG"。
|
/// 映射规则:Passed → "OK";Processing → "处理中";None → "-";其余视为 "NG"。
|
||||||
@@ -20,21 +20,21 @@ 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
|
short code = value switch
|
||||||
{
|
{
|
||||||
ushort u => u,
|
short s => s,
|
||||||
int i => (ushort)i,
|
int i => (short)i,
|
||||||
WorkflowResultCode rc => (ushort)rc,
|
WorkflowResultCode rc => (short)rc,
|
||||||
_ => (ushort)0
|
_ => 0
|
||||||
};
|
};
|
||||||
|
|
||||||
return code switch
|
return code switch
|
||||||
{
|
{
|
||||||
(ushort)WorkflowResultCode.Passed => "OK",
|
(short)WorkflowResultCode.Passed => "OK",
|
||||||
(ushort)WorkflowResultCode.Processing => "处理中",
|
(short)WorkflowResultCode.Processing => "处理中",
|
||||||
(ushort)WorkflowResultCode.None => "-",
|
(short)WorkflowResultCode.None => "-",
|
||||||
_ => "NG"
|
_ => "NG"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
|
|||||||
CurrentBarcode = "PCB240417000128",
|
CurrentBarcode = "PCB240417000128",
|
||||||
ResultDescription = "已扫码,等待 SFTP 文件确认",
|
ResultDescription = "已扫码,等待 SFTP 文件确认",
|
||||||
FaultMessage = string.Empty,
|
FaultMessage = string.Empty,
|
||||||
ResultCode = (ushort)WorkflowResultCode.Processing,
|
ResultCode = (short)WorkflowResultCode.Processing,
|
||||||
LastTriggeredAt = now.AddSeconds(-18),
|
LastTriggeredAt = now.AddSeconds(-18),
|
||||||
LastCompletedAt = now.AddMinutes(-2),
|
LastCompletedAt = now.AddMinutes(-2),
|
||||||
IsBusy = true,
|
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 = "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 = "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 = "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<UiLogEntry>
|
_logs = new List<UiLogEntry>
|
||||||
{
|
{
|
||||||
@@ -64,7 +64,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
|
|||||||
Barcode = "PCB240417000125",
|
Barcode = "PCB240417000125",
|
||||||
ScanTryCount = 1,
|
ScanTryCount = 1,
|
||||||
SftpTryCount = 1,
|
SftpTryCount = 1,
|
||||||
ResultCode = (ushort)WorkflowResultCode.Passed,
|
ResultCode = (short)WorkflowResultCode.Passed,
|
||||||
ResultDescription = "OK 放行",
|
ResultDescription = "OK 放行",
|
||||||
ReleaseSent = true,
|
ReleaseSent = true,
|
||||||
ExceptionSummary = string.Empty
|
ExceptionSummary = string.Empty
|
||||||
@@ -76,7 +76,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
|
|||||||
Barcode = "PCB240417000126",
|
Barcode = "PCB240417000126",
|
||||||
ScanTryCount = 3,
|
ScanTryCount = 3,
|
||||||
SftpTryCount = 0,
|
SftpTryCount = 0,
|
||||||
ResultCode = (ushort)WorkflowResultCode.NgReleased,
|
ResultCode = (short)WorkflowResultCode.NgReleased,
|
||||||
ResultDescription = "扫码失败后放行",
|
ResultDescription = "扫码失败后放行",
|
||||||
ReleaseSent = true,
|
ReleaseSent = true,
|
||||||
ExceptionSummary = "扫码连续失败三次"
|
ExceptionSummary = "扫码连续失败三次"
|
||||||
@@ -88,7 +88,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
|
|||||||
Barcode = "PCB240417000127",
|
Barcode = "PCB240417000127",
|
||||||
ScanTryCount = 1,
|
ScanTryCount = 1,
|
||||||
SftpTryCount = 3,
|
SftpTryCount = 3,
|
||||||
ResultCode = (ushort)WorkflowResultCode.NgReleased,
|
ResultCode = (short)WorkflowResultCode.NgReleased,
|
||||||
ResultDescription = "处理结果 NG,随后放行",
|
ResultDescription = "处理结果 NG,随后放行",
|
||||||
ReleaseSent = true,
|
ReleaseSent = true,
|
||||||
ExceptionSummary = "SFTP 文件查询超时"
|
ExceptionSummary = "SFTP 文件查询超时"
|
||||||
@@ -97,8 +97,8 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
|
|||||||
|
|
||||||
_snapshot.RecordStatisticDate = DateOnly.FromDateTime(now.LocalDateTime.Date);
|
_snapshot.RecordStatisticDate = DateOnly.FromDateTime(now.LocalDateTime.Date);
|
||||||
_snapshot.TodayProcessCount = _records.Count;
|
_snapshot.TodayProcessCount = _records.Count;
|
||||||
_snapshot.TodayOkCount = _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 != (ushort)WorkflowResultCode.Passed);
|
_snapshot.TodayNgCount = _records.Count(record => record.ResultCode != (short)WorkflowResultCode.Passed);
|
||||||
_snapshot.RecordsLastUpdatedAt = now;
|
_snapshot.RecordsLastUpdatedAt = now;
|
||||||
foreach (BoardProcessRecord record in _records)
|
foreach (BoardProcessRecord record in _records)
|
||||||
{
|
{
|
||||||
|
|||||||
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ public sealed class RuntimeSnapshot
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置结果代码。
|
/// 获取或设置结果代码。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort ResultCode { get; set; }
|
public short ResultCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置上次触发时间。
|
/// 获取或设置上次触发时间。
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public enum WorkflowState
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示最终结果代码定义。
|
/// 表示最终结果代码定义。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum WorkflowResultCode : ushort
|
public enum WorkflowResultCode : short
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 无结果。
|
/// 无结果。
|
||||||
@@ -182,7 +182,7 @@ public sealed class PlcProcessState
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置结果代码寄存器值。
|
/// 获取或设置结果代码寄存器值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort ResultCode { get; set; }
|
public short ResultCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建当前状态对象的浅拷贝。
|
/// 创建当前状态对象的浅拷贝。
|
||||||
@@ -496,7 +496,7 @@ public sealed class BoardProcessRecord
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置结果代码。
|
/// 获取或设置结果代码。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort ResultCode { get; set; }
|
public short ResultCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置结果描述。
|
/// 获取或设置结果描述。
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ public sealed class PlcOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public byte SourceNodeAddress { get; set; } = 25;
|
public byte SourceNodeAddress { get; set; } = 25;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 PLC 寄存器字节序,默认使用 BADC 以匹配现场 D 区 Int16 高低字节顺序。
|
||||||
|
/// </summary>
|
||||||
|
public string? EndianFormat { get; set; } = "BADC";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置轮询周期,单位为毫秒。
|
/// 获取或设置轮询周期,单位为毫秒。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration>Release</Configuration>
|
<Configuration>Release</Configuration>
|
||||||
<Platform>Any CPU</Platform>
|
<Platform>Any CPU</Platform>
|
||||||
<PublishDir>bin\Release\net8.0-windows\publish\</PublishDir>
|
<PublishDir>bin\Release\net8.0-windows\publish\</PublishDir>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
|
||||||
<SelfContained>false</SelfContained>
|
<SelfContained>false</SelfContained>
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
|
||||||
<IncludeNativeLibrariesForSelfExtract>false</IncludeNativeLibrariesForSelfExtract>
|
|
||||||
<EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"AxiOmron.PcbCheck": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"environmentVariables": {
|
|
||||||
"DOTNET_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -247,10 +247,10 @@ public sealed class CsvBoardRecordRepository : IBoardRecordRepository
|
|||||||
{
|
{
|
||||||
StatisticDate = date,
|
StatisticDate = date,
|
||||||
TotalCount = records.Count,
|
TotalCount = records.Count,
|
||||||
OkCount = records.Count(record => record.ResultCode == (ushort)WorkflowResultCode.Passed),
|
OkCount = records.Count(record => record.ResultCode == (short)WorkflowResultCode.Passed),
|
||||||
NgCount = records.Count(record => record.ResultCode != (ushort)WorkflowResultCode.Passed
|
NgCount = records.Count(record => record.ResultCode != (short)WorkflowResultCode.Passed
|
||||||
&& record.ResultCode != (ushort)WorkflowResultCode.Processing
|
&& record.ResultCode != (short)WorkflowResultCode.Processing
|
||||||
&& record.ResultCode != (ushort)WorkflowResultCode.None),
|
&& record.ResultCode != (short)WorkflowResultCode.None),
|
||||||
LastUpdatedAt = _timeProvider.GetLocalNow()
|
LastUpdatedAt = _timeProvider.GetLocalNow()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -400,7 +400,7 @@ public sealed class CsvBoardRecordRepository : IBoardRecordRepository
|
|||||||
|| !DateTimeOffset.TryParse(fields[1], out DateTimeOffset completedAt)
|
|| !DateTimeOffset.TryParse(fields[1], out DateTimeOffset completedAt)
|
||||||
|| !int.TryParse(fields[3], out int scanTryCount)
|
|| !int.TryParse(fields[3], out int scanTryCount)
|
||||||
|| !int.TryParse(fields[4], out int sftpTryCount)
|
|| !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[7], out bool releaseSent)
|
||||||
|| !bool.TryParse(fields[8], out bool alarmRaised))
|
|| !bool.TryParse(fields[8], out bool alarmRaised))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
|
|||||||
{
|
{
|
||||||
PcBusy = ReadBoolean(ToDataMemoryAddress(_options.Outputs.PcBusy)),
|
PcBusy = ReadBoolean(ToDataMemoryAddress(_options.Outputs.PcBusy)),
|
||||||
ReleasePermit = ReadBoolean(ToDataMemoryAddress(_options.Outputs.ReleasePermit)),
|
ReleasePermit = ReadBoolean(ToDataMemoryAddress(_options.Outputs.ReleasePermit)),
|
||||||
ResultCode = ReadUInt16(ToDataMemoryAddress(_options.Registers.ResultCode))
|
ResultCode = ReadInt16(ToDataMemoryAddress(_options.Registers.ResultCode))
|
||||||
},
|
},
|
||||||
CapturedAt = capturedAt
|
CapturedAt = capturedAt
|
||||||
};
|
};
|
||||||
@@ -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,15 +276,15 @@ 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>
|
||||||
private ushort ReadUInt16(string address)
|
private short ReadInt16(string address)
|
||||||
{
|
{
|
||||||
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
||||||
var result = client.ReadUInt16(address);
|
var result = client.ReadInt16(address);
|
||||||
EnsureSuccess(result.IsSucceed, result.Err, $"读取 FINS UInt16 失败,地址={address}");
|
EnsureSuccess(result.IsSucceed, result.Err, $"读取 FINS Int16 失败,地址={address}");
|
||||||
return result.Value;
|
return result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
short? 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));
|
WriteInt16IfChanged(previousResultCode, state.ResultCode, ToDataMemoryAddress(_options.Registers.ResultCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -249,12 +320,12 @@ 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>
|
||||||
/// <param name="address">FINS 地址。</param>
|
/// <param name="address">FINS 地址。</param>
|
||||||
private void WriteUInt16IfChanged(ushort? previous, ushort current, string address)
|
private void WriteInt16IfChanged(short? previous, short current, string address)
|
||||||
{
|
{
|
||||||
if (previous.HasValue && previous.Value == current)
|
if (previous.HasValue && previous.Value == current)
|
||||||
{
|
{
|
||||||
@@ -263,7 +334,148 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
|
|||||||
|
|
||||||
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
||||||
var result = client.Write(address, current);
|
var result = client.Write(address, current);
|
||||||
EnsureSuccess(result.IsSucceed, result.Err, $"写入 FINS UInt16 失败,地址={address},值={current}");
|
EnsureSuccess(result.IsSucceed, result.Err, $"写入 FINS Int16 失败,地址={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>
|
||||||
|
|||||||
@@ -848,7 +848,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
Barcode = _currentBarcode,
|
Barcode = _currentBarcode,
|
||||||
ScanTryCount = _scanTryCount,
|
ScanTryCount = _scanTryCount,
|
||||||
SftpTryCount = _sftpTryCount,
|
SftpTryCount = _sftpTryCount,
|
||||||
ResultCode = (ushort)_resultCode,
|
ResultCode = (short)_resultCode,
|
||||||
ResultDescription = GetResultDescription(_resultCode),
|
ResultDescription = GetResultDescription(_resultCode),
|
||||||
ReleaseSent = _releaseSent,
|
ReleaseSent = _releaseSent,
|
||||||
AlarmRaised = false,
|
AlarmRaised = false,
|
||||||
@@ -1082,7 +1082,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
lock (_stateSyncRoot)
|
lock (_stateSyncRoot)
|
||||||
{
|
{
|
||||||
updateAction(_plcState);
|
updateAction(_plcState);
|
||||||
_plcState.ResultCode = (ushort)_resultCode;
|
_plcState.ResultCode = (short)_resultCode;
|
||||||
snapshot = _plcState.Clone();
|
snapshot = _plcState.Clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,7 +1103,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
{
|
{
|
||||||
DateTimeOffset? startedAt;
|
DateTimeOffset? startedAt;
|
||||||
string barcode;
|
string barcode;
|
||||||
ushort resultCode;
|
short resultCode;
|
||||||
bool isBusy;
|
bool isBusy;
|
||||||
string faultMessage;
|
string faultMessage;
|
||||||
|
|
||||||
@@ -1111,7 +1111,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
|
|||||||
{
|
{
|
||||||
startedAt = _currentBoardStartedAt;
|
startedAt = _currentBoardStartedAt;
|
||||||
barcode = _currentBarcode;
|
barcode = _currentBarcode;
|
||||||
resultCode = (ushort)_resultCode;
|
resultCode = (short)_resultCode;
|
||||||
isBusy = _plcState.PcBusy;
|
isBusy = _plcState.PcBusy;
|
||||||
faultMessage = _faultLatched ? description : string.Empty;
|
faultMessage = _faultLatched ? description : string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
/// 获取或设置结果代码。
|
/// 获取或设置结果代码。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ushort _resultCode;
|
private short _resultCode;
|
||||||
|
|
||||||
/// <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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -4,33 +4,34 @@
|
|||||||
"Port": 9600,
|
"Port": 9600,
|
||||||
"UnitAddress": 0,
|
"UnitAddress": 0,
|
||||||
"SourceNodeAddress": 25,
|
"SourceNodeAddress": 25,
|
||||||
|
"EndianFormat": "BADC",
|
||||||
"PollIntervalMs": 200,
|
"PollIntervalMs": 200,
|
||||||
"ConnectTimeoutMs": 3000,
|
"ConnectTimeoutMs": 3000,
|
||||||
"ReleasePulseMs": 500,
|
"ReleasePulseMs": 500,
|
||||||
"ReleaseAckTimeoutMs": 2000,
|
"ReleaseAckTimeoutMs": 2000,
|
||||||
"Inputs": {
|
"Inputs": {
|
||||||
"PcbArrived": 1001,
|
"PcbArrived": 101,
|
||||||
"PlcReset": 1002,
|
"PlcReset": 102,
|
||||||
"PlcAckRelease": 1003
|
"PlcAckRelease": 103
|
||||||
},
|
},
|
||||||
"Outputs": {
|
"Outputs": {
|
||||||
"PcBusy": 1051,
|
"PcBusy": 51,
|
||||||
"ReleasePermit": 1052
|
"ReleasePermit": 52
|
||||||
},
|
},
|
||||||
"Registers": {
|
"Registers": {
|
||||||
"ResultCode": 1100
|
"ResultCode": 401
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Scanner": {
|
"Scanner": {
|
||||||
"Scanner1": {
|
"Scanner1": {
|
||||||
"Name": "扫码枪1",
|
"Name": "扫码枪1",
|
||||||
"Host": "127.0.0.1",
|
"Host": "127.0.0.1",
|
||||||
"Port": 9001
|
"Port": 2001
|
||||||
},
|
},
|
||||||
"Scanner2": {
|
"Scanner2": {
|
||||||
"Name": "扫码枪2",
|
"Name": "扫码枪2",
|
||||||
"Host": "127.0.0.1",
|
"Host": "127.0.0.1",
|
||||||
"Port": 9002
|
"Port": 2001
|
||||||
},
|
},
|
||||||
"ConnectTimeoutMs": 3000,
|
"ConnectTimeoutMs": 3000,
|
||||||
"ReadTimeoutMs": 3000,
|
"ReadTimeoutMs": 3000,
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"TriggerInParallel": true
|
"TriggerInParallel": true
|
||||||
},
|
},
|
||||||
"FileCheck": {
|
"FileCheck": {
|
||||||
"Provider": "Sftp",
|
"Provider": "Local",
|
||||||
"FileNamePattern": "${barcode}.txt",
|
"FileNamePattern": "${barcode}.txt",
|
||||||
"RetryIntervalSeconds": 2,
|
"RetryIntervalSeconds": 2,
|
||||||
"MaxRetryCount": 3,
|
"MaxRetryCount": 3,
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
"ConnectTimeoutMs": 3000
|
"ConnectTimeoutMs": 3000
|
||||||
},
|
},
|
||||||
"Andon": {
|
"Andon": {
|
||||||
"Enable": true,
|
"Enable": false,
|
||||||
"Url": "https://shp.prod.aliyun.mit.uaes.com/api/mit/MAE/Station/Status/alarm",
|
"Url": "https://shp.prod.aliyun.mit.uaes.com/api/mit/MAE/Station/Status/alarm",
|
||||||
"Method": "POST",
|
"Method": "POST",
|
||||||
"TimeoutMs": 3000,
|
"TimeoutMs": 3000,
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ public sealed class CsvBoardRecordRepositoryTests : IDisposable
|
|||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
await repository.AppendAsync(
|
await repository.AppendAsync(
|
||||||
CreateRecord("PCB-001", new DateTimeOffset(2026, 4, 19, 8, 0, 0, TimeSpan.FromHours(8)), (ushort)WorkflowResultCode.Passed),
|
CreateRecord("PCB-001", new DateTimeOffset(2026, 4, 19, 8, 0, 0, TimeSpan.FromHours(8)), (short)WorkflowResultCode.Passed),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
await repository.AppendAsync(
|
await repository.AppendAsync(
|
||||||
CreateRecord("PCB-002", new DateTimeOffset(2026, 4, 19, 8, 5, 0, TimeSpan.FromHours(8)), (ushort)WorkflowResultCode.NgReleased),
|
CreateRecord("PCB-002", new DateTimeOffset(2026, 4, 19, 8, 5, 0, TimeSpan.FromHours(8)), (short)WorkflowResultCode.NgReleased),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
BoardRecordLoadResult result = await repository.LoadCurrentDayAsync(CancellationToken.None);
|
BoardRecordLoadResult result = await repository.LoadCurrentDayAsync(CancellationToken.None);
|
||||||
@@ -100,7 +100,7 @@ public sealed class CsvBoardRecordRepositoryTests : IDisposable
|
|||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
await repository.AppendAsync(
|
await repository.AppendAsync(
|
||||||
CreateRecord("PCB-OLD", new DateTimeOffset(2026, 4, 19, 23, 59, 55, TimeSpan.FromHours(8)), (ushort)WorkflowResultCode.Passed),
|
CreateRecord("PCB-OLD", new DateTimeOffset(2026, 4, 19, 23, 59, 55, TimeSpan.FromHours(8)), (short)WorkflowResultCode.Passed),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
timeProvider.SetLocalNow(new DateTimeOffset(2026, 4, 20, 0, 0, 10, TimeSpan.FromHours(8)));
|
timeProvider.SetLocalNow(new DateTimeOffset(2026, 4, 20, 0, 0, 10, TimeSpan.FromHours(8)));
|
||||||
@@ -131,7 +131,7 @@ public sealed class CsvBoardRecordRepositoryTests : IDisposable
|
|||||||
/// <param name="completedAt">完成时间。</param>
|
/// <param name="completedAt">完成时间。</param>
|
||||||
/// <param name="resultCode">结果代码。</param>
|
/// <param name="resultCode">结果代码。</param>
|
||||||
/// <returns>测试记录。</returns>
|
/// <returns>测试记录。</returns>
|
||||||
private static BoardProcessRecord CreateRecord(string barcode, DateTimeOffset completedAt, ushort resultCode)
|
private static BoardProcessRecord CreateRecord(string barcode, DateTimeOffset completedAt, short resultCode)
|
||||||
{
|
{
|
||||||
return new BoardProcessRecord
|
return new BoardProcessRecord
|
||||||
{
|
{
|
||||||
@@ -141,7 +141,7 @@ public sealed class CsvBoardRecordRepositoryTests : IDisposable
|
|||||||
ScanTryCount = 1,
|
ScanTryCount = 1,
|
||||||
SftpTryCount = 1,
|
SftpTryCount = 1,
|
||||||
ResultCode = resultCode,
|
ResultCode = resultCode,
|
||||||
ResultDescription = resultCode == (ushort)WorkflowResultCode.Passed ? "OK" : "NG",
|
ResultDescription = resultCode == (short)WorkflowResultCode.Passed ? "OK" : "NG",
|
||||||
ReleaseSent = true,
|
ReleaseSent = true,
|
||||||
AlarmRaised = false,
|
AlarmRaised = false,
|
||||||
ExceptionSummary = string.Empty
|
ExceptionSummary = string.Empty
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public sealed class MainWindowViewModelTests
|
|||||||
Barcode = $"PCB-{index:D3}",
|
Barcode = $"PCB-{index:D3}",
|
||||||
ScanTryCount = 1,
|
ScanTryCount = 1,
|
||||||
SftpTryCount = 1,
|
SftpTryCount = 1,
|
||||||
ResultCode = index % 2 == 0 ? (ushort)WorkflowResultCode.Passed : (ushort)WorkflowResultCode.NgReleased,
|
ResultCode = index % 2 == 0 ? (short)WorkflowResultCode.Passed : (short)WorkflowResultCode.NgReleased,
|
||||||
ResultDescription = index % 2 == 0 ? "OK" : "NG",
|
ResultDescription = index % 2 == 0 ? "OK" : "NG",
|
||||||
ReleaseSent = true,
|
ReleaseSent = true,
|
||||||
AlarmRaised = false
|
AlarmRaised = false
|
||||||
@@ -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>
|
||||||
|
/// 默认字节序应使用 BADC,以匹配现场 D 区 Int16 的高低字节顺序。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ResolveEndianFormat_ShouldUseBadc_WhenConfigurationIsDefault()
|
||||||
|
{
|
||||||
|
var options = new PlcOptions();
|
||||||
|
|
||||||
|
EndianFormat result = OmronFinsPlcService.ResolveEndianFormat(options);
|
||||||
|
|
||||||
|
Assert.Equal(EndianFormat.BADC, 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>
|
||||||
|
/// 结果代码枚举应以 Int16 作为底层类型,匹配 PLC 的 ResultCode 定义。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void WorkflowResultCode_ShouldUseInt16UnderlyingType()
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(short), Enum.GetUnderlyingType(typeof(WorkflowResultCode)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结果代码属性应统一使用 Int16,避免经过界面或快照时被误建模为 UInt16 或 Int32。
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(RuntimeSnapshot))]
|
||||||
|
[InlineData(typeof(PlcProcessState))]
|
||||||
|
[InlineData(typeof(BoardProcessRecord))]
|
||||||
|
[InlineData(typeof(MainWindowViewModel))]
|
||||||
|
public void ResultCodeProperties_ShouldUseInt16(Type ownerType)
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(short), 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>
|
||||||
|
|||||||
@@ -148,12 +148,12 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
|
|
||||||
Assert.True(scannerService.TriggerCount > 0);
|
Assert.True(scannerService.TriggerCount > 0);
|
||||||
Assert.Contains(plcService.WrittenStates, state => state.ReleasePermit);
|
Assert.Contains(plcService.WrittenStates, state => state.ReleasePermit);
|
||||||
Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (ushort)WorkflowResultCode.Passed);
|
Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (short)WorkflowResultCode.Passed);
|
||||||
|
|
||||||
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
|
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
|
||||||
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcbArrived" && item.CurrentValue == "False");
|
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcbArrived" && item.CurrentValue == "False");
|
||||||
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcBusy" && item.CurrentValue == "False");
|
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "PcBusy" && item.CurrentValue == "False");
|
||||||
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "ResultCode" && item.CurrentValue == ((ushort)WorkflowResultCode.Passed).ToString());
|
Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "ResultCode" && item.CurrentValue == ((short)WorkflowResultCode.Passed).ToString());
|
||||||
Assert.All(snapshot.PlcMonitorItems, item => Assert.NotEqual(default, item.LastUpdatedAt));
|
Assert.All(snapshot.PlcMonitorItems, item => Assert.NotEqual(default, item.LastUpdatedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
|
|
||||||
Assert.Equal(1, scannerService.TriggerCount);
|
Assert.Equal(1, scannerService.TriggerCount);
|
||||||
Assert.Equal("PCB-MERGED", fileCheckService.LastBarcode);
|
Assert.Equal("PCB-MERGED", fileCheckService.LastBarcode);
|
||||||
Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (ushort)WorkflowResultCode.Passed);
|
Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (short)WorkflowResultCode.Passed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -304,7 +304,7 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
Barcode = "PCB-RESTORED",
|
Barcode = "PCB-RESTORED",
|
||||||
ScanTryCount = 1,
|
ScanTryCount = 1,
|
||||||
SftpTryCount = 1,
|
SftpTryCount = 1,
|
||||||
ResultCode = (ushort)WorkflowResultCode.Passed,
|
ResultCode = (short)WorkflowResultCode.Passed,
|
||||||
ResultDescription = "OK",
|
ResultDescription = "OK",
|
||||||
ReleaseSent = true
|
ReleaseSent = true
|
||||||
}
|
}
|
||||||
@@ -409,7 +409,7 @@ public sealed class WorkflowHostedServiceTests
|
|||||||
|
|
||||||
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
|
RuntimeSnapshot snapshot = stateStore.GetSnapshot();
|
||||||
Assert.NotEqual(WorkflowState.Faulted, snapshot.WorkflowState);
|
Assert.NotEqual(WorkflowState.Faulted, snapshot.WorkflowState);
|
||||||
Assert.Equal((ushort)WorkflowResultCode.Passed, snapshot.ResultCode);
|
Assert.Equal((short)WorkflowResultCode.Passed, snapshot.ResultCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FakePlcService : IPlcService
|
private sealed class FakePlcService : IPlcService
|
||||||
@@ -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