✨ feat(*): 更新 PLC 相关配置与结果代码类型

- 修改 appConfig.json 中 PLC 和扫描仪的 IP 地址及字节序配置
- 将多个地方的 ResultCode 类型从 short 更新为 ushort,以确保与 PLC 通信一致
- 更新相关服务和模型以适应新的 ResultCode 类型
- 调整单元测试以验证新配置和类型的正确性
This commit is contained in:
2026-06-02 17:59:04 +08:00
parent 27e2c9d5d0
commit 17839a9e76
15 changed files with 155 additions and 153 deletions

View File

@@ -5,7 +5,7 @@ using AxiOmron.PcbCheck.Models;
namespace AxiOmron.PcbCheck.Converters; namespace AxiOmron.PcbCheck.Converters;
/// <summary> /// <summary>
/// 将 <see cref="WorkflowResultCode"/>(以 <see cref="short"/> 存储)映射为简短 Tag 文本。 /// 将 <see cref="WorkflowResultCode"/>(以 <see cref="ushort"/> 存储)映射为简短 Tag 文本。
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// 映射规则:Passed → "OK";Processing → "处理中";None → "-";其余视为 "NG"。 /// 映射规则:Passed → "OK";Processing → "处理中";None → "-";其余视为 "NG"。
@@ -22,19 +22,20 @@ public sealed class ResultCodeToTagTextConverter : IValueConverter
/// <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)
{ {
short code = value switch ushort code = value switch
{ {
short s => s, ushort s => s,
int i => (short)i, short s when s >= 0 => (ushort)s,
WorkflowResultCode rc => (short)rc, int i when i >= 0 && i <= ushort.MaxValue => (ushort)i,
WorkflowResultCode rc => (ushort)rc,
_ => 0 _ => 0
}; };
return code switch return code switch
{ {
(short)WorkflowResultCode.Passed => "OK", (ushort)WorkflowResultCode.Passed => "OK",
(short)WorkflowResultCode.Processing => "处理中", (ushort)WorkflowResultCode.Processing => "处理中",
(short)WorkflowResultCode.None => "-", (ushort)WorkflowResultCode.None => "-",
_ => "NG" _ => "NG"
}; };
} }

View File

@@ -32,7 +32,7 @@ public sealed class DesignTimeAppStateStore : IAppStateStore
CurrentBarcode = "PCB240417000128", CurrentBarcode = "PCB240417000128",
ResultDescription = "已扫码,等待 SFTP 文件确认", ResultDescription = "已扫码,等待 SFTP 文件确认",
FaultMessage = string.Empty, FaultMessage = string.Empty,
ResultCode = (short)WorkflowResultCode.Processing, ResultCode = (ushort)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 = ((short)WorkflowResultCode.Processing).ToString(), LastUpdatedAt = now.AddSeconds(-1) }); _snapshot.PlcMonitorItems.Add(new PlcMonitorItem { GroupName = "Registers", Name = "ResultCode", Address = "40001", CurrentValue = ((ushort)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 = (short)WorkflowResultCode.Passed, ResultCode = (ushort)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 = (short)WorkflowResultCode.NgReleased, ResultCode = (ushort)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 = (short)WorkflowResultCode.NgReleased, ResultCode = (ushort)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 == (short)WorkflowResultCode.Passed); _snapshot.TodayOkCount = _records.Count(record => record.ResultCode == (ushort)WorkflowResultCode.Passed);
_snapshot.TodayNgCount = _records.Count(record => record.ResultCode != (short)WorkflowResultCode.Passed); _snapshot.TodayNgCount = _records.Count(record => record.ResultCode != (ushort)WorkflowResultCode.Passed);
_snapshot.RecordsLastUpdatedAt = now; _snapshot.RecordsLastUpdatedAt = now;
foreach (BoardProcessRecord record in _records) foreach (BoardProcessRecord record in _records)
{ {

View File

@@ -63,7 +63,7 @@ public sealed class RuntimeSnapshot
/// <summary> /// <summary>
/// 获取或设置结果代码。 /// 获取或设置结果代码。
/// </summary> /// </summary>
public short ResultCode { get; set; } public ushort ResultCode { get; set; }
/// <summary> /// <summary>
/// 获取或设置上次触发时间。 /// 获取或设置上次触发时间。

View File

@@ -69,7 +69,7 @@ public enum WorkflowState
/// <summary> /// <summary>
/// 表示最终结果代码定义。 /// 表示最终结果代码定义。
/// </summary> /// </summary>
public enum WorkflowResultCode : short public enum WorkflowResultCode : ushort
{ {
/// <summary> /// <summary>
/// 无结果。 /// 无结果。
@@ -182,7 +182,7 @@ public sealed class PlcProcessState
/// <summary> /// <summary>
/// 获取或设置结果代码寄存器值。 /// 获取或设置结果代码寄存器值。
/// </summary> /// </summary>
public short ResultCode { get; set; } public ushort ResultCode { get; set; }
/// <summary> /// <summary>
/// 创建当前状态对象的浅拷贝。 /// 创建当前状态对象的浅拷贝。
@@ -496,7 +496,7 @@ public sealed class BoardProcessRecord
/// <summary> /// <summary>
/// 获取或设置结果代码。 /// 获取或设置结果代码。
/// </summary> /// </summary>
public short ResultCode { get; set; } public ushort ResultCode { get; set; }
/// <summary> /// <summary>
/// 获取或设置结果描述。 /// 获取或设置结果描述。

View File

@@ -130,9 +130,9 @@ public sealed class PlcOptions
public byte SourceNodeAddress { get; set; } = 25; public byte SourceNodeAddress { get; set; } = 25;
/// <summary> /// <summary>
/// 获取或设置 PLC 寄存器字节序,默认使用 BADC 以匹配现场 D 区 Int16 高低字节顺序。 /// 获取或设置 PLC 寄存器字节序,默认使用 ABCD 以避免 UInt16 结果代码发生高低字节交换。
/// </summary> /// </summary>
public string? EndianFormat { get; set; } = "BADC"; public string? EndianFormat { get; set; } = "ABCD";
/// <summary> /// <summary>
/// 获取或设置轮询周期,单位为毫秒。 /// 获取或设置轮询周期,单位为毫秒。

View File

@@ -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 == (short)WorkflowResultCode.Passed), OkCount = records.Count(record => record.ResultCode == (ushort)WorkflowResultCode.Passed),
NgCount = records.Count(record => record.ResultCode != (short)WorkflowResultCode.Passed NgCount = records.Count(record => record.ResultCode != (ushort)WorkflowResultCode.Passed
&& record.ResultCode != (short)WorkflowResultCode.Processing && record.ResultCode != (ushort)WorkflowResultCode.Processing
&& record.ResultCode != (short)WorkflowResultCode.None), && record.ResultCode != (ushort)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)
|| !short.TryParse(fields[5], out short resultCode) || !ushort.TryParse(fields[5], out ushort 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))
{ {

View File

@@ -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 = ReadInt16(ToDataMemoryAddress(_options.Registers.ResultCode)) ResultCode = ReadUInt16(ToDataMemoryAddress(_options.Registers.ResultCode))
}, },
CapturedAt = capturedAt CapturedAt = capturedAt
}; };
@@ -276,15 +276,15 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
} }
/// <summary> /// <summary>
/// 读取单个 D 区有符号 16 位整数。 /// 读取单个 D 区无符号 16 位整数。
/// </summary> /// </summary>
/// <param name="address">FINS 地址。</param> /// <param name="address">FINS 地址。</param>
/// <returns>读取到的寄存器值。</returns> /// <returns>读取到的寄存器值。</returns>
private short ReadInt16(string address) private ushort ReadUInt16(string address)
{ {
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。"); var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
var result = client.ReadInt16(address); var result = client.ReadUInt16(address);
EnsureSuccess(result.IsSucceed, result.Err, $"读取 FINS Int16 失败,地址={address}"); EnsureSuccess(result.IsSucceed, result.Err, $"读取 FINS UInt16 失败,地址={address}");
return result.Value; return result.Value;
} }
@@ -295,10 +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; 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));
WriteInt16IfChanged(previousResultCode, state.ResultCode, ToDataMemoryAddress(_options.Registers.ResultCode)); WriteUInt16IfChanged(previousResultCode, state.ResultCode, ToDataMemoryAddress(_options.Registers.ResultCode));
} }
/// <summary> /// <summary>
@@ -320,12 +320,12 @@ public sealed class OmronFinsPlcService : IPlcService, IDisposable
} }
/// <summary> /// <summary>
/// 仅当值发生变化时写入单个 D 区有符号 16 位整数。 /// 仅当值发生变化时写入单个 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 WriteInt16IfChanged(short? previous, short current, string address) private void WriteUInt16IfChanged(ushort? previous, ushort current, string address)
{ {
if (previous.HasValue && previous.Value == current) if (previous.HasValue && previous.Value == current)
{ {
@@ -334,7 +334,7 @@ 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 Int16 失败,地址={address},值={current}"); EnsureSuccess(result.IsSucceed, result.Err, $"写入 FINS UInt16 失败,地址={address},值={current}");
} }
/// <summary> /// <summary>

View File

@@ -848,7 +848,7 @@ public sealed class WorkflowHostedService : BackgroundService, IWorkflowControlS
Barcode = _currentBarcode, Barcode = _currentBarcode,
ScanTryCount = _scanTryCount, ScanTryCount = _scanTryCount,
SftpTryCount = _sftpTryCount, SftpTryCount = _sftpTryCount,
ResultCode = (short)_resultCode, ResultCode = (ushort)_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 = (short)_resultCode; _plcState.ResultCode = (ushort)_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;
short resultCode; ushort 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 = (short)_resultCode; resultCode = (ushort)_resultCode;
isBusy = _plcState.PcBusy; isBusy = _plcState.PcBusy;
faultMessage = _faultLatched ? description : string.Empty; faultMessage = _faultLatched ? description : string.Empty;
} }

View File

@@ -150,7 +150,7 @@ public partial class MainWindowViewModel : ObservableObject
/// 获取或设置结果代码。 /// 获取或设置结果代码。
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty]
private short _resultCode; private ushort _resultCode;
/// <summary> /// <summary>
/// 获取或设置最近触发时间。 /// 获取或设置最近触发时间。
@@ -557,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>

View File

@@ -1,92 +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,
"EndianFormat": "BADC", "EndianFormat": "ABCD",
"PollIntervalMs": 200, "PollIntervalMs": 200,
"ConnectTimeoutMs": 3000, "ConnectTimeoutMs": 3000,
"ReleasePulseMs": 500, "ReleasePulseMs": 500,
"ReleaseAckTimeoutMs": 2000, "ReleaseAckTimeoutMs": 2000,
"Inputs": { "Inputs": {
"PcbArrived": 101, "PcbArrived": 101,
"PlcReset": 102, "PlcReset": 102,
"PlcAckRelease": 103 "PlcAckRelease": 103
}, },
"Outputs": { "Outputs": {
"PcBusy": 51, "PcBusy": 51,
"ReleasePermit": 52 "ReleasePermit": 52
}, },
"Registers": { "Registers": {
"ResultCode": 401 "ResultCode": 401
} }
}, },
"Scanner": { "Scanner": {
"Scanner1": { "Scanner1": {
"Name": "扫码枪1", "Name": "\u626B\u7801\u67AA1",
"Host": "127.0.0.1", "Host": "192.168.2.13",
"Port": 2001 "Port": 2001
}, },
"Scanner2": { "Scanner2": {
"Name": "扫码枪2", "Name": "\u626B\u7801\u67AA2",
"Host": "127.0.0.1", "Host": "192.168.2.14",
"Port": 2001 "Port": 2001
}, },
"ConnectTimeoutMs": 3000, "ConnectTimeoutMs": 3000,
"ReadTimeoutMs": 3000, "TriggerCommand": "T",
"TriggerCommand": "T", "ResponseTerminator": "",
"ResponseTerminator": "\\r", "TriggerInParallel": true,
"TriggerInParallel": true "ReadTimeoutMs": 5000
}, },
"FileCheck": { "FileCheck": {
"Provider": "Local", "Provider": "Local",
"FileNamePattern": "${barcode}.txt", "FileNamePattern": "${barcode}.txt",
"RetryIntervalSeconds": 2, "RetryIntervalSeconds": 2,
"MaxRetryCount": 3, "MaxRetryCount": 3,
"Local": { "Local": {
"RootPath": "D:\\PcbCheck\\Files" "RootPath": "D:\\test"
} }
}, },
"Sftp": { "Sftp": {
"Host": "127.0.0.1", "Host": "127.0.0.1",
"Port": 22, "Port": 22,
"Username": "uaesadmin", "Username": "uaesadmin",
"Password": "uaes,123", "Password": "uaes,123",
"PrivateKeyPath": "", "PrivateKeyPath": "",
"PrivateKeyPassphrase": "", "PrivateKeyPassphrase": "",
"RootPath": "/home/uaesadmin/", "RootPath": "/home/uaesadmin/",
"FileNamePattern": "${barcode}.txt", "FileNamePattern": "${barcode}.txt",
"RetryIntervalSeconds": 2, "RetryIntervalSeconds": 2,
"MaxRetryCount": 3, "MaxRetryCount": 3,
"ConnectTimeoutMs": 3000 "ConnectTimeoutMs": 3000
}, },
"Andon": { "Andon": {
"Enable": false, "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,
"AndonTheme": "SS7_Auto_Andon", "AndonTheme": "SS7_Auto_Andon",
"Eid": "PR1965269806334783488", "Eid": "PR1965269806334783488",
"FromSign": "IOT", "FromSign": "IOT",
"ServiceId": "4dccd822-86b2-47dd-89bd-ca472ba0d14d", "ServiceId": "4dccd822-86b2-47dd-89bd-ca472ba0d14d",
"EnableScanFailAlarm": true, "EnableScanFailAlarm": false,
"EnableFileNotFoundAlarm": false, "EnableFileNotFoundAlarm": false,
"Headers": { "Headers": {
"Accept": "application/json" "Accept": "application/json"
} }
}, },
"Workflow": { "Workflow": {
"RequireManualResetAfterFault": true, "RequireManualResetAfterFault": true,
"MaxUiLogEntries": 200, "MaxUiLogEntries": 200,
"MaxBoardRecords": 100 "MaxBoardRecords": 100
}, },
"RecordPersistence": { "RecordPersistence": {
"DirectoryPath": "Data\\Records", "DirectoryPath": "Data\\Records",
"DayChangeCheckIntervalSeconds": 30 "DayChangeCheckIntervalSeconds": 30
}, },
"Security": { "Security": {
"AdminPassword": "AxiOmron@123", "AdminPassword": "AxiOmron@123",
"AdminSessionTimeoutMinutes": 15 "AdminSessionTimeoutMinutes": 15
} }
} }

View File

@@ -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)), (short)WorkflowResultCode.Passed), CreateRecord("PCB-001", new DateTimeOffset(2026, 4, 19, 8, 0, 0, TimeSpan.FromHours(8)), (ushort)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)), (short)WorkflowResultCode.NgReleased), CreateRecord("PCB-002", new DateTimeOffset(2026, 4, 19, 8, 5, 0, TimeSpan.FromHours(8)), (ushort)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)), (short)WorkflowResultCode.Passed), CreateRecord("PCB-OLD", new DateTimeOffset(2026, 4, 19, 23, 59, 55, TimeSpan.FromHours(8)), (ushort)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, short resultCode) private static BoardProcessRecord CreateRecord(string barcode, DateTimeOffset completedAt, ushort 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 == (short)WorkflowResultCode.Passed ? "OK" : "NG", ResultDescription = resultCode == (ushort)WorkflowResultCode.Passed ? "OK" : "NG",
ReleaseSent = true, ReleaseSent = true,
AlarmRaised = false, AlarmRaised = false,
ExceptionSummary = string.Empty ExceptionSummary = string.Empty

View File

@@ -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 ? (short)WorkflowResultCode.Passed : (short)WorkflowResultCode.NgReleased, ResultCode = index % 2 == 0 ? (ushort)WorkflowResultCode.Passed : (ushort)WorkflowResultCode.NgReleased,
ResultDescription = index % 2 == 0 ? "OK" : "NG", ResultDescription = index % 2 == 0 ? "OK" : "NG",
ReleaseSent = true, ReleaseSent = true,
AlarmRaised = false AlarmRaised = false

View File

@@ -11,16 +11,16 @@ namespace AxiOmron.PcbCheck.Tests;
public sealed class OmronFinsPlcServiceTests public sealed class OmronFinsPlcServiceTests
{ {
/// <summary> /// <summary>
/// 默认字节序应使用 BADC,以匹配现场 D 区 Int16 的高低字节顺序。 /// 默认字节序应使用 ABCD,避免 UInt16 结果代码发生高低字节交换。
/// </summary> /// </summary>
[Fact] [Fact]
public void ResolveEndianFormat_ShouldUseBadc_WhenConfigurationIsDefault() public void ResolveEndianFormat_ShouldUseAbcd_WhenConfigurationIsDefault()
{ {
var options = new PlcOptions(); var options = new PlcOptions();
EndianFormat result = OmronFinsPlcService.ResolveEndianFormat(options); EndianFormat result = OmronFinsPlcService.ResolveEndianFormat(options);
Assert.Equal(EndianFormat.BADC, result); Assert.Equal(EndianFormat.ABCD, result);
} }
/// <summary> /// <summary>

View File

@@ -5,29 +5,29 @@ using Xunit;
namespace AxiOmron.PcbCheck.Tests; namespace AxiOmron.PcbCheck.Tests;
/// <summary> /// <summary>
/// 验证结果代码在模型与界面层统一使用 16 位有符号整数。 /// 验证结果代码在模型与界面层统一使用 16 位无符号整数。
/// </summary> /// </summary>
public sealed class ResultCodeTypeTests public sealed class ResultCodeTypeTests
{ {
/// <summary> /// <summary>
/// 结果代码枚举应以 Int16 作为底层类型,匹配 PLC 的 ResultCode 定义。 /// 结果代码枚举应以 UInt16 作为底层类型,匹配 PLC 的 ResultCode 定义。
/// </summary> /// </summary>
[Fact] [Fact]
public void WorkflowResultCode_ShouldUseInt16UnderlyingType() public void WorkflowResultCode_ShouldUseUInt16UnderlyingType()
{ {
Assert.Equal(typeof(short), Enum.GetUnderlyingType(typeof(WorkflowResultCode))); Assert.Equal(typeof(ushort), Enum.GetUnderlyingType(typeof(WorkflowResultCode)));
} }
/// <summary> /// <summary>
/// 结果代码属性应统一使用 Int16,避免经过界面或快照时被误建模为 UInt16 或 Int32。 /// 结果代码属性应统一使用 UInt16,避免经过界面或快照时被误建模为 Int16 或 Int32。
/// </summary> /// </summary>
[Theory] [Theory]
[InlineData(typeof(RuntimeSnapshot))] [InlineData(typeof(RuntimeSnapshot))]
[InlineData(typeof(PlcProcessState))] [InlineData(typeof(PlcProcessState))]
[InlineData(typeof(BoardProcessRecord))] [InlineData(typeof(BoardProcessRecord))]
[InlineData(typeof(MainWindowViewModel))] [InlineData(typeof(MainWindowViewModel))]
public void ResultCodeProperties_ShouldUseInt16(Type ownerType) public void ResultCodeProperties_ShouldUseUInt16(Type ownerType)
{ {
Assert.Equal(typeof(short), ownerType.GetProperty("ResultCode")?.PropertyType); Assert.Equal(typeof(ushort), ownerType.GetProperty("ResultCode")?.PropertyType);
} }
} }

View File

@@ -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 == (short)WorkflowResultCode.Passed); Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (ushort)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 == ((short)WorkflowResultCode.Passed).ToString()); Assert.Contains(snapshot.PlcMonitorItems, item => item.Name == "ResultCode" && item.CurrentValue == ((ushort)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 == (short)WorkflowResultCode.Passed); Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (ushort)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 = (short)WorkflowResultCode.Passed, ResultCode = (ushort)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((short)WorkflowResultCode.Passed, snapshot.ResultCode); Assert.Equal((ushort)WorkflowResultCode.Passed, snapshot.ResultCode);
} }
private sealed class FakePlcService : IPlcService private sealed class FakePlcService : IPlcService