✨ feat: 初始化 PCB 检测 WPF 应用程序
* 创建 AxiOmron.PcbCheck 项目主框架及解决方案 * 添加 Dashboard 和系统设置页面 * 实现 Modbus TCP PLC、扫码枪、SFTP 查询等核心服务 * 集成 Andon 报警、工作流托管服务与日志配置 * 补充项目文档和 UI 设计规范
This commit is contained in:
17
src/AxiOmron.PcbCheck/App.xaml
Normal file
17
src/AxiOmron.PcbCheck/App.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<Application x:Class="AxiOmron.PcbCheck.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:AxiOmron.PcbCheck"
|
||||
xmlns:viewModels="clr-namespace:AxiOmron.PcbCheck.ViewModels"
|
||||
xmlns:designTime="clr-namespace:AxiOmron.PcbCheck.DesignTime">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<viewModels:ViewModelLocator x:Key="Locator"/>
|
||||
<designTime:DesignTimeViewModelLocator x:Key="DesignTimeLocator"/>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
130
src/AxiOmron.PcbCheck/App.xaml.cs
Normal file
130
src/AxiOmron.PcbCheck/App.xaml.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Implementations;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
using AxiOmron.PcbCheck.ViewModels;
|
||||
using AxiOmron.PcbCheck.Views.Pages;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NLog.Extensions.Logging;
|
||||
|
||||
namespace AxiOmron.PcbCheck;
|
||||
|
||||
/// <summary>
|
||||
/// 表示 WPF 应用入口,负责 Host/DI、配置和日志初始化。
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private IHost? _host;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前应用服务容器。
|
||||
/// </summary>
|
||||
public static IServiceProvider Services { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 应用启动入口。
|
||||
/// </summary>
|
||||
/// <param name="e">启动事件参数。</param>
|
||||
protected override async void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
try
|
||||
{
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// WinExe 在没有控制台句柄时忽略
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_host = BuildHost();
|
||||
await _host.StartAsync().ConfigureAwait(true);
|
||||
Services = _host.Services;
|
||||
var mainWindow = Services.GetRequiredService<MainWindow>();
|
||||
MainWindow = mainWindow;
|
||||
mainWindow.Show();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"应用启动失败: {ex.Message}", "Axi Omron PCB Check", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Shutdown(-1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用退出入口。
|
||||
/// </summary>
|
||||
/// <param name="e">退出事件参数。</param>
|
||||
protected override async void OnExit(ExitEventArgs e)
|
||||
{
|
||||
if (_host is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _host.StopAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_host.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
base.OnExit(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建应用 Host 与依赖注入容器。
|
||||
/// </summary>
|
||||
/// <returns>已构建的 Host 实例。</returns>
|
||||
private static IHost BuildHost()
|
||||
{
|
||||
return Host.CreateDefaultBuilder()
|
||||
.ConfigureAppConfiguration((context, configurationBuilder) =>
|
||||
{
|
||||
configurationBuilder.SetBasePath(AppContext.BaseDirectory);
|
||||
configurationBuilder.AddJsonFile("appConfig.json", optional: true, reloadOnChange: false);
|
||||
configurationBuilder.AddJsonFile($"appConfig.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||
})
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
logging.ClearProviders();
|
||||
logging.SetMinimumLevel(LogLevel.Information);
|
||||
logging.AddNLog();
|
||||
})
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
var appConfig = new AppConfig();
|
||||
context.Configuration.Bind(appConfig);
|
||||
|
||||
services.AddSingleton<IConfiguration>(context.Configuration);
|
||||
services.AddSingleton<IAppConfigService, AppConfigService>();
|
||||
services.AddSingleton(appConfig);
|
||||
services.AddSingleton<IAppStateStore, AppStateStore>();
|
||||
services.AddSingleton(typeof(IAppLogger<>), typeof(AppLogger<>));
|
||||
services.AddSingleton<IDispatcherService, DispatcherService>();
|
||||
services.AddHttpClient(nameof(AndonService));
|
||||
|
||||
services.AddSingleton<IPlcService, ModbusTcpPlcService>();
|
||||
services.AddSingleton<IScannerService, SerialScannerService>();
|
||||
services.AddSingleton<ISftpLookupService, SftpLookupService>();
|
||||
services.AddSingleton<IAndonService, AndonService>();
|
||||
services.AddSingleton<WorkflowHostedService>();
|
||||
services.AddSingleton<IWorkflowControlService>(sp => sp.GetRequiredService<WorkflowHostedService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<WorkflowHostedService>());
|
||||
|
||||
services.AddSingleton<MainWindowViewModel>();
|
||||
services.AddSingleton<SystemSettingViewModel>();
|
||||
services.AddSingleton<DashboardPage>();
|
||||
services.AddSingleton<SystemSettingsPage>();
|
||||
services.AddSingleton<MainWindow>();
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
10
src/AxiOmron.PcbCheck/AssemblyInfo.cs
Normal file
10
src/AxiOmron.PcbCheck/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly:ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
33
src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj
Normal file
33
src/AxiOmron.PcbCheck/AxiOmron.PcbCheck.csproj
Normal file
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="HandyControl" Version="3.5.1" />
|
||||
<PackageReference Include="IoTClient" Version="1.0.42" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="NLog" Version="6.0.2" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="6.0.2" />
|
||||
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
||||
<PackageReference Include="System.IO.Ports" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appConfig.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="appConfig.Development.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="NLog.config">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
61
src/AxiOmron.PcbCheck/Converters/BooleanToBrushConverter.cs
Normal file
61
src/AxiOmron.PcbCheck/Converters/BooleanToBrushConverter.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// 将布尔状态转换为界面画刷,颜色与设计色板保持一致。
|
||||
/// </summary>
|
||||
public sealed class BooleanToBrushConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// true 状态使用的柔和绿色画刷(对应 Tag 前景色 #15803D)。
|
||||
/// </summary>
|
||||
private static readonly SolidColorBrush TrueBrush = CreateFrozen(0x15, 0x80, 0x3D);
|
||||
|
||||
/// <summary>
|
||||
/// false 状态使用的柔和红色画刷(对应 Tag 前景色 #B91C1C)。
|
||||
/// </summary>
|
||||
private static readonly SolidColorBrush FalseBrush = CreateFrozen(0xB9, 0x1C, 0x1C);
|
||||
|
||||
/// <summary>
|
||||
/// 将布尔值转换为画刷。
|
||||
/// </summary>
|
||||
/// <param name="value">源值。</param>
|
||||
/// <param name="targetType">目标类型。</param>
|
||||
/// <param name="parameter">扩展参数。</param>
|
||||
/// <param name="culture">当前区域信息。</param>
|
||||
/// <returns>状态画刷。</returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is true ? TrueBrush : FalseBrush;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建并冻结指定颜色的画刷,便于跨线程复用。
|
||||
/// </summary>
|
||||
/// <param name="r">红通道。</param>
|
||||
/// <param name="g">绿通道。</param>
|
||||
/// <param name="b">蓝通道。</param>
|
||||
/// <returns>已冻结的画刷实例。</returns>
|
||||
private static SolidColorBrush CreateFrozen(byte r, byte g, byte b)
|
||||
{
|
||||
SolidColorBrush brush = new(Color.FromRgb(r, g, b));
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不支持反向转换。
|
||||
/// </summary>
|
||||
/// <param name="value">源值。</param>
|
||||
/// <param name="targetType">目标类型。</param>
|
||||
/// <param name="parameter">扩展参数。</param>
|
||||
/// <param name="culture">当前区域信息。</param>
|
||||
/// <returns>抛出不支持异常。</returns>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// 将布尔值转换为 <see cref="Visibility"/>,支持通过 ConverterParameter 反转判定。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当 ConverterParameter 为 "Invert"、"Inverse" 或 "!" 时,布尔值的真假意义取反。
|
||||
/// </remarks>
|
||||
public sealed class BooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将布尔值转换为可见性。
|
||||
/// </summary>
|
||||
/// <param name="value">源布尔值。</param>
|
||||
/// <param name="targetType">目标类型。</param>
|
||||
/// <param name="parameter">若为 "Invert"/"!" 则反转判定。</param>
|
||||
/// <param name="culture">当前区域信息。</param>
|
||||
/// <returns><see cref="Visibility.Visible"/> 或 <see cref="Visibility.Collapsed"/>。</returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
bool flag = value is bool b && b;
|
||||
if (IsInvert(parameter))
|
||||
{
|
||||
flag = !flag;
|
||||
}
|
||||
return flag ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将可见性反向转换为布尔值。
|
||||
/// </summary>
|
||||
/// <param name="value">目标可见性。</param>
|
||||
/// <param name="targetType">目标类型。</param>
|
||||
/// <param name="parameter">若为 "Invert"/"!" 则反转判定。</param>
|
||||
/// <param name="culture">当前区域信息。</param>
|
||||
/// <returns>布尔值。</returns>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
bool flag = value is Visibility v && v == Visibility.Visible;
|
||||
if (IsInvert(parameter))
|
||||
{
|
||||
flag = !flag;
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断参数是否要求反转判定。
|
||||
/// </summary>
|
||||
/// <param name="parameter">参数值。</param>
|
||||
/// <returns>是否反转。</returns>
|
||||
private static bool IsInvert(object? parameter)
|
||||
{
|
||||
if (parameter is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
string token = parameter.ToString() ?? string.Empty;
|
||||
return token.Equals("Invert", StringComparison.OrdinalIgnoreCase)
|
||||
|| token.Equals("Inverse", StringComparison.OrdinalIgnoreCase)
|
||||
|| token.Equals("!", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// 将 <see cref="WorkflowResultCode"/>(以 <see cref="ushort"/> 存储)映射为简短 Tag 文本。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 映射规则:Passed → "OK";Processing → "处理中";None → "-";其余视为 "NG"。
|
||||
/// </remarks>
|
||||
public sealed class ResultCodeToTagTextConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将结果码转换为 Tag 文本。
|
||||
/// </summary>
|
||||
/// <param name="value">结果码值。</param>
|
||||
/// <param name="targetType">目标类型。</param>
|
||||
/// <param name="parameter">扩展参数。</param>
|
||||
/// <param name="culture">当前区域信息。</param>
|
||||
/// <returns>简短标签文本。</returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
ushort code = value switch
|
||||
{
|
||||
ushort u => u,
|
||||
int i => (ushort)i,
|
||||
WorkflowResultCode rc => (ushort)rc,
|
||||
_ => (ushort)0
|
||||
};
|
||||
|
||||
return code switch
|
||||
{
|
||||
(ushort)WorkflowResultCode.Passed => "OK",
|
||||
(ushort)WorkflowResultCode.Processing => "处理中",
|
||||
(ushort)WorkflowResultCode.None => "-",
|
||||
_ => "NG"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不支持反向转换。
|
||||
/// </summary>
|
||||
/// <param name="value">源值。</param>
|
||||
/// <param name="targetType">目标类型。</param>
|
||||
/// <param name="parameter">扩展参数。</param>
|
||||
/// <param name="culture">当前区域信息。</param>
|
||||
/// <returns>抛出不支持异常。</returns>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
120
src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppConfigService.cs
Normal file
120
src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppConfigService.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.DesignTime;
|
||||
|
||||
/// <summary>
|
||||
/// 提供设计时配置服务,返回固定的示例配置数据。
|
||||
/// </summary>
|
||||
public sealed class DesignTimeAppConfigService : IAppConfigService
|
||||
{
|
||||
private readonly AppConfig _config;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化设计时配置服务。
|
||||
/// </summary>
|
||||
public DesignTimeAppConfigService()
|
||||
{
|
||||
_config = CreateSampleConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取设计时配置副本。
|
||||
/// </summary>
|
||||
/// <returns>示例根配置对象。</returns>
|
||||
public AppConfig Load()
|
||||
{
|
||||
return CreateSampleConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存设计时配置,占位实现,仅更新内存中的副本。
|
||||
/// </summary>
|
||||
/// <param name="config">待保存的配置对象。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="config"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public void Save(AppConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_config.Plc = config.Plc;
|
||||
_config.Scanner = config.Scanner;
|
||||
_config.Sftp = config.Sftp;
|
||||
_config.Andon = config.Andon;
|
||||
_config.Workflow = config.Workflow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取设计时展示用的示例配置路径。
|
||||
/// </summary>
|
||||
/// <returns>固定的设计时配置路径文本。</returns>
|
||||
public string GetConfigPath()
|
||||
{
|
||||
return @"D:\DesignTime\appConfig.Development.json";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建设计器使用的示例配置对象。
|
||||
/// </summary>
|
||||
/// <returns>填充默认值后的配置对象。</returns>
|
||||
private static AppConfig CreateSampleConfig()
|
||||
{
|
||||
return new AppConfig
|
||||
{
|
||||
Plc = new PlcOptions
|
||||
{
|
||||
Host = "192.168.10.25",
|
||||
Port = 502,
|
||||
UnitId = 1,
|
||||
PollIntervalMs = 200,
|
||||
ConnectTimeoutMs = 3000,
|
||||
HeartbeatIntervalMs = 500,
|
||||
ReleasePulseMs = 450,
|
||||
ReleaseAckTimeoutMs = 2500
|
||||
},
|
||||
Scanner = new ScannerOptions
|
||||
{
|
||||
PortName = "COM3",
|
||||
BaudRate = 9600,
|
||||
DataBits = 8,
|
||||
Parity = "None",
|
||||
StopBits = "One",
|
||||
ReadTimeoutMs = 2500,
|
||||
TriggerCommand = "SCAN\\r",
|
||||
ResponseTerminator = "\\r",
|
||||
MaxScanAttempts = 3
|
||||
},
|
||||
Sftp = new SftpOptions
|
||||
{
|
||||
Host = "10.10.20.35",
|
||||
Port = 22,
|
||||
Username = "pcb_user",
|
||||
Password = "******",
|
||||
PrivateKeyPath = @"C:\Keys\pcb-check.ppk",
|
||||
RootPath = "/data/pcb",
|
||||
FileNamePattern = "${barcode}.txt",
|
||||
RetryIntervalSeconds = 2,
|
||||
MaxRetryCount = 3,
|
||||
ConnectTimeoutMs = 3000
|
||||
},
|
||||
Andon = new AndonOptions
|
||||
{
|
||||
Enable = true,
|
||||
Url = "http://10.10.20.50/api/andon/alarm",
|
||||
Method = "POST",
|
||||
TimeoutMs = 3000,
|
||||
StationCode = "OMRON-L01",
|
||||
StationName = "欧姆龙 PCB 检测",
|
||||
EnableScanFailAlarm = true,
|
||||
EnableFileNotFoundAlarm = true
|
||||
},
|
||||
Workflow = new WorkflowOptions
|
||||
{
|
||||
RequirePlcReady = true,
|
||||
RequireAutoMode = true,
|
||||
RequireStationEnable = true,
|
||||
RequireManualResetAfterFault = true,
|
||||
MaxUiLogEntries = 200,
|
||||
MaxBoardRecords = 100
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
206
src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs
Normal file
206
src/AxiOmron.PcbCheck/DesignTime/DesignTimeAppStateStore.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.DesignTime;
|
||||
|
||||
/// <summary>
|
||||
/// 提供设计时运行态存储,向真实 ViewModel 回放固定的演示数据。
|
||||
/// </summary>
|
||||
public sealed class DesignTimeAppStateStore : IAppStateStore
|
||||
{
|
||||
private readonly RuntimeSnapshot _snapshot;
|
||||
private readonly IReadOnlyList<UiLogEntry> _logs;
|
||||
private readonly IReadOnlyList<BoardProcessRecord> _records;
|
||||
private EventHandler<RuntimeSnapshot>? _snapshotChanged;
|
||||
private EventHandler<UiLogEntry>? _logAdded;
|
||||
private EventHandler<BoardProcessRecord>? _recordAdded;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化设计时运行态存储。
|
||||
/// </summary>
|
||||
public DesignTimeAppStateStore()
|
||||
{
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
_snapshot = new RuntimeSnapshot
|
||||
{
|
||||
PlcStatus = "已连接",
|
||||
ScannerStatus = "在线",
|
||||
SftpStatus = "可访问",
|
||||
AndonStatus = "接口正常",
|
||||
WorkflowState = WorkflowState.CheckingSftp,
|
||||
WorkflowStateText = WorkflowState.CheckingSftp.ToDisplayText(),
|
||||
CurrentBarcode = "PCB240417000128",
|
||||
ResultDescription = "已扫码,等待 SFTP 文件确认",
|
||||
FaultMessage = string.Empty,
|
||||
ScanTryCount = 1,
|
||||
SftpTryCount = 2,
|
||||
ResultCode = (ushort)WorkflowResultCode.Processing,
|
||||
AlarmCode = (ushort)AlarmCode.None,
|
||||
LastTriggeredAt = now.AddSeconds(-18),
|
||||
LastCompletedAt = now.AddMinutes(-2),
|
||||
IsBusy = true,
|
||||
ProcessDone = false,
|
||||
SystemFault = false,
|
||||
AlarmRaised = false,
|
||||
LastUpdatedAt = now
|
||||
};
|
||||
|
||||
_logs = new List<UiLogEntry>
|
||||
{
|
||||
new() { Timestamp = now.AddSeconds(-2), Level = "Info", Message = "SFTP 第 2 次查询:正在检查目标文件。" },
|
||||
new() { Timestamp = now.AddSeconds(-9), Level = "Warning", Message = "第 1 次 SFTP 查询未命中,准备重试。" },
|
||||
new() { Timestamp = now.AddSeconds(-16), Level = "Info", Message = "扫码成功,条码=PCB240417000128" },
|
||||
new() { Timestamp = now.AddSeconds(-21), Level = "Info", Message = "检测到 PCB 到位,流程开始执行。" },
|
||||
new() { Timestamp = now.AddMinutes(-1), Level = "Error", Message = "上一片文件查询超时,已按规则放行。" }
|
||||
};
|
||||
|
||||
_records = new List<BoardProcessRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
StartedAt = now.AddMinutes(-4),
|
||||
CompletedAt = now.AddMinutes(-3).AddSeconds(-18),
|
||||
Barcode = "PCB240417000125",
|
||||
ScanTryCount = 1,
|
||||
SftpTryCount = 1,
|
||||
ResultCode = (ushort)WorkflowResultCode.Passed,
|
||||
ResultDescription = "OK 放行",
|
||||
ReleaseSent = true,
|
||||
AlarmRaised = false,
|
||||
ExceptionSummary = string.Empty
|
||||
},
|
||||
new()
|
||||
{
|
||||
StartedAt = now.AddMinutes(-3),
|
||||
CompletedAt = now.AddMinutes(-2).AddSeconds(-12),
|
||||
Barcode = "PCB240417000126",
|
||||
ScanTryCount = 3,
|
||||
SftpTryCount = 0,
|
||||
ResultCode = (ushort)WorkflowResultCode.ScanFailedReleased,
|
||||
ResultDescription = "扫码失败后放行",
|
||||
ReleaseSent = true,
|
||||
AlarmRaised = true,
|
||||
ExceptionSummary = "扫码连续失败三次"
|
||||
},
|
||||
new()
|
||||
{
|
||||
StartedAt = now.AddMinutes(-2),
|
||||
CompletedAt = now.AddMinutes(-1).AddSeconds(-25),
|
||||
Barcode = "PCB240417000127",
|
||||
ScanTryCount = 1,
|
||||
SftpTryCount = 3,
|
||||
ResultCode = (ushort)WorkflowResultCode.FileNotFoundReleased,
|
||||
ResultDescription = "文件超时未找到后放行",
|
||||
ReleaseSent = true,
|
||||
AlarmRaised = true,
|
||||
ExceptionSummary = "SFTP 文件查询超时"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当运行态快照发生变化时触发。
|
||||
/// </summary>
|
||||
public event EventHandler<RuntimeSnapshot>? SnapshotChanged
|
||||
{
|
||||
add
|
||||
{
|
||||
_snapshotChanged += value;
|
||||
}
|
||||
remove
|
||||
{
|
||||
_snapshotChanged -= value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当新增日志时触发;订阅时会立即回放现有设计时日志。
|
||||
/// </summary>
|
||||
public event EventHandler<UiLogEntry>? LogAdded
|
||||
{
|
||||
add
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logAdded += value;
|
||||
foreach (UiLogEntry entry in _logs)
|
||||
{
|
||||
value(this, entry);
|
||||
}
|
||||
}
|
||||
remove
|
||||
{
|
||||
_logAdded -= value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当新增单板记录时触发;订阅时会立即回放现有设计时记录。
|
||||
/// </summary>
|
||||
public event EventHandler<BoardProcessRecord>? RecordAdded
|
||||
{
|
||||
add
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_recordAdded += value;
|
||||
foreach (BoardProcessRecord record in _records)
|
||||
{
|
||||
value(this, record);
|
||||
}
|
||||
}
|
||||
remove
|
||||
{
|
||||
_recordAdded -= value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前设计时快照副本。
|
||||
/// </summary>
|
||||
/// <returns>当前快照副本。</returns>
|
||||
public RuntimeSnapshot GetSnapshot()
|
||||
{
|
||||
return _snapshot.Clone();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新设计时快照并通知订阅者。
|
||||
/// </summary>
|
||||
/// <param name="updateAction">用于修改快照的委托。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="updateAction"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public void UpdateSnapshot(Action<RuntimeSnapshot> updateAction)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(updateAction);
|
||||
updateAction(_snapshot);
|
||||
_snapshotChanged?.Invoke(this, _snapshot.Clone());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加一条设计时日志并通知订阅者。
|
||||
/// </summary>
|
||||
/// <param name="entry">待追加的日志对象。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="entry"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public void AddLog(UiLogEntry entry)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entry);
|
||||
_logAdded?.Invoke(this, entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加一条设计时处理记录并通知订阅者。
|
||||
/// </summary>
|
||||
/// <param name="record">待追加的记录对象。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="record"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public void AddRecord(BoardProcessRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
_recordAdded?.Invoke(this, record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.DesignTime;
|
||||
|
||||
/// <summary>
|
||||
/// 提供设计时 Dispatcher 调度能力,直接在当前线程执行委托。
|
||||
/// </summary>
|
||||
public sealed class DesignTimeDispatcherService : IDispatcherService
|
||||
{
|
||||
/// <summary>
|
||||
/// 在当前线程中立即执行指定动作。
|
||||
/// </summary>
|
||||
/// <param name="action">待执行的动作。</param>
|
||||
/// <returns>表示执行完成的任务。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public Task InvokeAsync(Action action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
action();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.ViewModels;
|
||||
|
||||
namespace AxiOmron.PcbCheck.DesignTime;
|
||||
|
||||
/// <summary>
|
||||
/// 为 XAML 设计器提供基于真实 ViewModel 的设计时定位器。
|
||||
/// </summary>
|
||||
public sealed class DesignTimeViewModelLocator
|
||||
{
|
||||
private readonly DesignTimeAppStateStore _appStateStore = new();
|
||||
private readonly DesignTimeDispatcherService _dispatcherService = new();
|
||||
private readonly DesignTimeWorkflowControlService _workflowControlService = new();
|
||||
private readonly DesignTimeAppConfigService _appConfigService = new();
|
||||
private MainWindowViewModel? _mainWindowViewModel;
|
||||
private SystemSettingViewModel? _systemSettingViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 获取首页设计时视图模型。
|
||||
/// </summary>
|
||||
public MainWindowViewModel MainWindowViewModel
|
||||
=> _mainWindowViewModel ??= CreateMainWindowViewModel();
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统设置设计时视图模型。
|
||||
/// </summary>
|
||||
public SystemSettingViewModel SystemSettingViewModel
|
||||
=> _systemSettingViewModel ??= new SystemSettingViewModel(_appConfigService);
|
||||
|
||||
/// <summary>
|
||||
/// 创建首页设计时视图模型。
|
||||
/// </summary>
|
||||
/// <returns>填充了设计时演示数据的真实视图模型实例。</returns>
|
||||
private MainWindowViewModel CreateMainWindowViewModel()
|
||||
{
|
||||
AppConfig config = _appConfigService.Load();
|
||||
return new MainWindowViewModel(_appStateStore, _dispatcherService, _workflowControlService, config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.DesignTime;
|
||||
|
||||
/// <summary>
|
||||
/// 提供设计时流程控制服务,所有命令均为无副作用占位实现。
|
||||
/// </summary>
|
||||
public sealed class DesignTimeWorkflowControlService : IWorkflowControlService
|
||||
{
|
||||
/// <summary>
|
||||
/// 模拟手动复位流程命令,不执行实际业务操作。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌;若已取消则立即返回已取消任务。</param>
|
||||
/// <returns>表示命令已完成的任务。</returns>
|
||||
public Task ResetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return cancellationToken.IsCancellationRequested
|
||||
? Task.FromCanceled(cancellationToken)
|
||||
: Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 PLC 重连命令,不执行实际设备通信。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌;若已取消则立即返回已取消任务。</param>
|
||||
/// <returns>表示命令已完成的任务。</returns>
|
||||
public Task ReconnectPlcAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return cancellationToken.IsCancellationRequested
|
||||
? Task.FromCanceled(cancellationToken)
|
||||
: Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟扫码枪重连命令,不执行实际设备通信。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌;若已取消则立即返回已取消任务。</param>
|
||||
/// <returns>表示命令已完成的任务。</returns>
|
||||
public Task ReconnectScannerAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return cancellationToken.IsCancellationRequested
|
||||
? Task.FromCanceled(cancellationToken)
|
||||
: Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟安灯测试命令,不执行实际网络请求。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌;若已取消则立即返回已取消任务。</param>
|
||||
/// <returns>表示命令已完成的任务。</returns>
|
||||
public Task TestAndonAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return cancellationToken.IsCancellationRequested
|
||||
? Task.FromCanceled(cancellationToken)
|
||||
: Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
38
src/AxiOmron.PcbCheck/MainWindow.xaml
Normal file
38
src/AxiOmron.PcbCheck/MainWindow.xaml
Normal file
@@ -0,0 +1,38 @@
|
||||
<Window x:Class="AxiOmron.PcbCheck.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
mc:Ignorable="d"
|
||||
Title="Axi Omron PCB Check"
|
||||
Width="1600"
|
||||
Height="950"
|
||||
MinWidth="1400"
|
||||
MinHeight="860"
|
||||
Background="{DynamicResource {x:Static hc:ResourceToken.BackgroundBrush}}"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="80" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Background="{DynamicResource {x:Static hc:ResourceToken.PrimaryBrush}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Margin="20,0">
|
||||
<TextBlock Text="Axi Omron PCB Check" Foreground="White" FontSize="20" FontWeight="Bold" />
|
||||
<TextBlock Text="单工位串行状态机 / PLC + 扫码枪 + SFTP + 安灯" Foreground="#DBEAFE" FontSize="14" Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,20,0">
|
||||
<Button Style="{StaticResource ButtonPrimary}" x:Name="DashboardButton" Click="DashboardButton_OnClick" Content="首页" FontWeight="Bold" FontSize="18" Padding="18,10" Margin="0,0,12,0" Height="50"/>
|
||||
<Button Style="{StaticResource ButtonPrimary}" x:Name="SettingsButton" Click="SettingsButton_OnClick" Content="系统设置" FontWeight="Bold" FontSize="18" Padding="18,10" Height="50"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Frame x:Name="MainFrame" Grid.Row="1" NavigationUIVisibility="Hidden" />
|
||||
</Grid>
|
||||
</Window>
|
||||
46
src/AxiOmron.PcbCheck/MainWindow.xaml.cs
Normal file
46
src/AxiOmron.PcbCheck/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Windows;
|
||||
using AxiOmron.PcbCheck.Views.Pages;
|
||||
|
||||
namespace AxiOmron.PcbCheck;
|
||||
|
||||
/// <summary>
|
||||
/// 表示主窗口,负责页面导航装配。
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly DashboardPage _dashboardPage;
|
||||
private readonly SystemSettingsPage _systemSettingsPage;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化主窗口。
|
||||
/// </summary>
|
||||
/// <param name="dashboardPage">首页页面。</param>
|
||||
/// <param name="systemSettingsPage">系统设置页面。</param>
|
||||
public MainWindow(DashboardPage dashboardPage, SystemSettingsPage systemSettingsPage)
|
||||
{
|
||||
_dashboardPage = dashboardPage ?? throw new ArgumentNullException(nameof(dashboardPage));
|
||||
_systemSettingsPage = systemSettingsPage ?? throw new ArgumentNullException(nameof(systemSettingsPage));
|
||||
InitializeComponent();
|
||||
MainFrame.Navigate(_dashboardPage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航到首页。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="e">事件参数。</param>
|
||||
private void DashboardButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MainFrame.Navigate(_dashboardPage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航到系统设置页。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="e">事件参数。</param>
|
||||
private void SettingsButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MainFrame.Navigate(_systemSettingsPage);
|
||||
}
|
||||
}
|
||||
138
src/AxiOmron.PcbCheck/Models/RuntimeSnapshot.cs
Normal file
138
src/AxiOmron.PcbCheck/Models/RuntimeSnapshot.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
namespace AxiOmron.PcbCheck.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 表示当前应用运行态快照,用于界面展示与后台状态同步。
|
||||
/// </summary>
|
||||
public sealed class RuntimeSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 连接状态文本。
|
||||
/// </summary>
|
||||
public string PlcStatus { get; set; } = "未连接";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码枪连接状态文本。
|
||||
/// </summary>
|
||||
public string ScannerStatus { get; set; } = "未验证";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 连接状态文本。
|
||||
/// </summary>
|
||||
public string SftpStatus { get; set; } = "未验证";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置安灯接口状态文本。
|
||||
/// </summary>
|
||||
public string AndonStatus { get; set; } = "未验证";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前流程状态。
|
||||
/// </summary>
|
||||
public WorkflowState WorkflowState { get; set; } = WorkflowState.Idle;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前流程状态文本。
|
||||
/// </summary>
|
||||
public string WorkflowStateText { get; set; } = WorkflowState.Idle.ToDisplayText();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前条码。
|
||||
/// </summary>
|
||||
public string CurrentBarcode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前结果描述。
|
||||
/// </summary>
|
||||
public string ResultDescription { get; set; } = "等待触发";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前故障信息。
|
||||
/// </summary>
|
||||
public string FaultMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码次数。
|
||||
/// </summary>
|
||||
public int ScanTryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 查询次数。
|
||||
/// </summary>
|
||||
public int SftpTryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置结果代码。
|
||||
/// </summary>
|
||||
public ushort ResultCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置报警代码。
|
||||
/// </summary>
|
||||
public ushort AlarmCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置上次触发时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastTriggeredAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置上次完成时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastCompletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 是否忙碌。
|
||||
/// </summary>
|
||||
public bool IsBusy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否已完成。
|
||||
/// </summary>
|
||||
public bool ProcessDone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否存在系统故障。
|
||||
/// </summary>
|
||||
public bool SystemFault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否已触发报警。
|
||||
/// </summary>
|
||||
public bool AlarmRaised { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置上次状态刷新时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset LastUpdatedAt { get; set; } = DateTimeOffset.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 创建当前快照的副本。
|
||||
/// </summary>
|
||||
/// <returns>新的运行态快照副本。</returns>
|
||||
public RuntimeSnapshot Clone()
|
||||
{
|
||||
return new RuntimeSnapshot
|
||||
{
|
||||
PlcStatus = PlcStatus,
|
||||
ScannerStatus = ScannerStatus,
|
||||
SftpStatus = SftpStatus,
|
||||
AndonStatus = AndonStatus,
|
||||
WorkflowState = WorkflowState,
|
||||
WorkflowStateText = WorkflowStateText,
|
||||
CurrentBarcode = CurrentBarcode,
|
||||
ResultDescription = ResultDescription,
|
||||
FaultMessage = FaultMessage,
|
||||
ScanTryCount = ScanTryCount,
|
||||
SftpTryCount = SftpTryCount,
|
||||
ResultCode = ResultCode,
|
||||
AlarmCode = AlarmCode,
|
||||
LastTriggeredAt = LastTriggeredAt,
|
||||
LastCompletedAt = LastCompletedAt,
|
||||
IsBusy = IsBusy,
|
||||
ProcessDone = ProcessDone,
|
||||
SystemFault = SystemFault,
|
||||
AlarmRaised = AlarmRaised,
|
||||
LastUpdatedAt = LastUpdatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
579
src/AxiOmron.PcbCheck/Models/WorkflowModels.cs
Normal file
579
src/AxiOmron.PcbCheck/Models/WorkflowModels.cs
Normal file
@@ -0,0 +1,579 @@
|
||||
namespace AxiOmron.PcbCheck.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 表示流程状态机中的业务状态。
|
||||
/// </summary>
|
||||
public enum WorkflowState
|
||||
{
|
||||
/// <summary>
|
||||
/// 空闲状态。
|
||||
/// </summary>
|
||||
Idle = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 已收到触发信号。
|
||||
/// </summary>
|
||||
Triggered = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 正在扫码。
|
||||
/// </summary>
|
||||
Scanning = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 扫码重试中。
|
||||
/// </summary>
|
||||
ScanRetrying = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 扫码失败后放行。
|
||||
/// </summary>
|
||||
ScanFailedReleased = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 正在检查 SFTP。
|
||||
/// </summary>
|
||||
CheckingSftp = 5,
|
||||
|
||||
/// <summary>
|
||||
/// 正在等待 SFTP 重试。
|
||||
/// </summary>
|
||||
WaitingSftpRetry = 6,
|
||||
|
||||
/// <summary>
|
||||
/// SFTP 校验通过。
|
||||
/// </summary>
|
||||
SftpPassed = 7,
|
||||
|
||||
/// <summary>
|
||||
/// SFTP 超时后放行。
|
||||
/// </summary>
|
||||
SftpTimeoutReleased = 8,
|
||||
|
||||
/// <summary>
|
||||
/// 正在放行。
|
||||
/// </summary>
|
||||
Releasing = 9,
|
||||
|
||||
/// <summary>
|
||||
/// 流程已完成。
|
||||
/// </summary>
|
||||
Completed = 10,
|
||||
|
||||
/// <summary>
|
||||
/// 系统故障。
|
||||
/// </summary>
|
||||
Faulted = 11
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示最终结果代码定义。
|
||||
/// </summary>
|
||||
public enum WorkflowResultCode : ushort
|
||||
{
|
||||
/// <summary>
|
||||
/// 无结果。
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 处理中。
|
||||
/// </summary>
|
||||
Processing = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 正常放行。
|
||||
/// </summary>
|
||||
Passed = 10,
|
||||
|
||||
/// <summary>
|
||||
/// 扫码失败后放行。
|
||||
/// </summary>
|
||||
ScanFailedReleased = 20,
|
||||
|
||||
/// <summary>
|
||||
/// 文件未找到超时后放行。
|
||||
/// </summary>
|
||||
FileNotFoundReleased = 30,
|
||||
|
||||
/// <summary>
|
||||
/// PLC 通信异常。
|
||||
/// </summary>
|
||||
PlcCommunicationFault = 40,
|
||||
|
||||
/// <summary>
|
||||
/// 串口异常。
|
||||
/// </summary>
|
||||
ScannerFault = 41,
|
||||
|
||||
/// <summary>
|
||||
/// SFTP 连接或认证异常。
|
||||
/// </summary>
|
||||
SftpFault = 42,
|
||||
|
||||
/// <summary>
|
||||
/// 安灯接口调用异常。
|
||||
/// </summary>
|
||||
AndonFault = 43,
|
||||
|
||||
/// <summary>
|
||||
/// 配置异常。
|
||||
/// </summary>
|
||||
ConfigurationFault = 44
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示报警代码定义。
|
||||
/// </summary>
|
||||
public enum AlarmCode : ushort
|
||||
{
|
||||
/// <summary>
|
||||
/// 未报警。
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 扫码连续失败三次。
|
||||
/// </summary>
|
||||
ScanFailed = 1001,
|
||||
|
||||
/// <summary>
|
||||
/// SFTP 文件超时未找到。
|
||||
/// </summary>
|
||||
FileNotFound = 1002,
|
||||
|
||||
/// <summary>
|
||||
/// SFTP 连接异常。
|
||||
/// </summary>
|
||||
SftpFault = 1003,
|
||||
|
||||
/// <summary>
|
||||
/// 串口设备异常。
|
||||
/// </summary>
|
||||
ScannerFault = 1004,
|
||||
|
||||
/// <summary>
|
||||
/// PLC 通信异常。
|
||||
/// </summary>
|
||||
PlcFault = 1005
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示 PLC 读取到的输入信号快照。
|
||||
/// </summary>
|
||||
public sealed class PlcSignalSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 是否就绪。
|
||||
/// </summary>
|
||||
public bool PlcReady { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PCB 是否到位。
|
||||
/// </summary>
|
||||
public bool PcbArrived { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 是否请求复位。
|
||||
/// </summary>
|
||||
public bool PlcReset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 是否已应答放行。
|
||||
/// </summary>
|
||||
public bool PlcAckRelease { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否为自动模式。
|
||||
/// </summary>
|
||||
public bool AutoMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置工位是否使能。
|
||||
/// </summary>
|
||||
public bool StationEnable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置本次快照采集时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset CapturedAt { get; set; } = DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示上位机要写入 PLC 的输出状态与寄存器数据。
|
||||
/// </summary>
|
||||
public sealed class PlcProcessState
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 PC 在线位。
|
||||
/// </summary>
|
||||
public bool PcOnline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PC 忙碌位。
|
||||
/// </summary>
|
||||
public bool PcBusy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码成功位。
|
||||
/// </summary>
|
||||
public bool ScanOk { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码失败位。
|
||||
/// </summary>
|
||||
public bool ScanNg { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置文件找到位。
|
||||
/// </summary>
|
||||
public bool FileFound { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置文件未找到位。
|
||||
/// </summary>
|
||||
public bool FileNotFound { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置报警位。
|
||||
/// </summary>
|
||||
public bool AlarmRaised { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置放行位。
|
||||
/// </summary>
|
||||
public bool ReleasePermit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置流程完成位。
|
||||
/// </summary>
|
||||
public bool ProcessDone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置系统故障位。
|
||||
/// </summary>
|
||||
public bool SystemFault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置结果代码寄存器值。
|
||||
/// </summary>
|
||||
public ushort ResultCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码次数寄存器值。
|
||||
/// </summary>
|
||||
public ushort ScanTryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 查询次数寄存器值。
|
||||
/// </summary>
|
||||
public ushort SftpTryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置报警代码寄存器值。
|
||||
/// </summary>
|
||||
public ushort AlarmCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置流程状态代码寄存器值。
|
||||
/// </summary>
|
||||
public ushort FlowStateCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建当前状态对象的浅拷贝。
|
||||
/// </summary>
|
||||
/// <returns>新的 PLC 输出状态对象。</returns>
|
||||
public PlcProcessState Clone()
|
||||
{
|
||||
return new PlcProcessState
|
||||
{
|
||||
PcOnline = PcOnline,
|
||||
PcBusy = PcBusy,
|
||||
ScanOk = ScanOk,
|
||||
ScanNg = ScanNg,
|
||||
FileFound = FileFound,
|
||||
FileNotFound = FileNotFound,
|
||||
AlarmRaised = AlarmRaised,
|
||||
ReleasePermit = ReleasePermit,
|
||||
ProcessDone = ProcessDone,
|
||||
SystemFault = SystemFault,
|
||||
ResultCode = ResultCode,
|
||||
ScanTryCount = ScanTryCount,
|
||||
SftpTryCount = SftpTryCount,
|
||||
AlarmCode = AlarmCode,
|
||||
FlowStateCode = FlowStateCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示扫码执行结果。
|
||||
/// </summary>
|
||||
public sealed class ScanOperationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置扫码是否成功。
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否为系统级异常。
|
||||
/// </summary>
|
||||
public bool IsSystemError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置设备连接是否正常。
|
||||
/// </summary>
|
||||
public bool DeviceConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置清洗后的条码值。
|
||||
/// </summary>
|
||||
public string Barcode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置原始报文。
|
||||
/// </summary>
|
||||
public string RawMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置错误描述。
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置耗时,单位为毫秒。
|
||||
/// </summary>
|
||||
public long DurationMs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一次 SFTP 文件校验结果。
|
||||
/// </summary>
|
||||
public sealed class SftpCheckOutcome
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置目标文件是否存在。
|
||||
/// </summary>
|
||||
public bool Exists { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否为系统级异常。
|
||||
/// </summary>
|
||||
public bool IsSystemError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否为配置级异常。
|
||||
/// </summary>
|
||||
public bool IsConfigurationError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置本次连接是否成功建立。
|
||||
/// </summary>
|
||||
public bool ConnectionSucceeded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置命中的文件路径。
|
||||
/// </summary>
|
||||
public string MatchedFilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置错误描述。
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一次安灯请求。
|
||||
/// </summary>
|
||||
public sealed class AndonAlarmRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置报警类型。
|
||||
/// </summary>
|
||||
public string AlarmType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置报警代码。
|
||||
/// </summary>
|
||||
public ushort AlarmCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置报警描述。
|
||||
/// </summary>
|
||||
public string AlarmMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置条码。
|
||||
/// </summary>
|
||||
public string Barcode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置触发时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset TriggeredAt { get; set; } = DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一次安灯接口调用结果。
|
||||
/// </summary>
|
||||
public sealed class AndonOperationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置调用是否成功。
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置终端是否成功到达。
|
||||
/// </summary>
|
||||
public bool EndpointReached { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 HTTP 状态码。
|
||||
/// </summary>
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置响应报文。
|
||||
/// </summary>
|
||||
public string ResponseBody { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置错误描述。
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示 UI 中的一条运行日志。
|
||||
/// </summary>
|
||||
public sealed class UiLogEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置日志时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置日志级别。
|
||||
/// </summary>
|
||||
public string Level { get; set; } = "Info";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置日志消息。
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示单板处理结果摘要。
|
||||
/// </summary>
|
||||
public sealed class BoardProcessRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置开始时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset StartedAt { get; set; } = DateTimeOffset.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置完成时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset CompletedAt { get; set; } = DateTimeOffset.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置条码。
|
||||
/// </summary>
|
||||
public string Barcode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码次数。
|
||||
/// </summary>
|
||||
public int ScanTryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 查询次数。
|
||||
/// </summary>
|
||||
public int SftpTryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置结果代码。
|
||||
/// </summary>
|
||||
public ushort ResultCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置结果描述。
|
||||
/// </summary>
|
||||
public string ResultDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否已发送放行。
|
||||
/// </summary>
|
||||
public bool ReleaseSent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否已触发报警。
|
||||
/// </summary>
|
||||
public bool AlarmRaised { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置异常摘要。
|
||||
/// </summary>
|
||||
public string ExceptionSummary { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供流程状态与 PLC 流程代码之间的映射方法。
|
||||
/// </summary>
|
||||
internal static class WorkflowStateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 将流程状态映射为 PLC 流程状态码。
|
||||
/// </summary>
|
||||
/// <param name="state">待映射的流程状态。</param>
|
||||
/// <returns>对应的流程状态码。</returns>
|
||||
public static ushort ToFlowStateCode(this WorkflowState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
WorkflowState.Idle => 0,
|
||||
WorkflowState.Triggered => 1,
|
||||
WorkflowState.Scanning => 2,
|
||||
WorkflowState.ScanRetrying => 3,
|
||||
WorkflowState.CheckingSftp => 4,
|
||||
WorkflowState.WaitingSftpRetry => 5,
|
||||
WorkflowState.Releasing => 6,
|
||||
WorkflowState.Completed => 7,
|
||||
WorkflowState.Faulted => 8,
|
||||
WorkflowState.ScanFailedReleased => 6,
|
||||
WorkflowState.SftpPassed => 6,
|
||||
WorkflowState.SftpTimeoutReleased => 6,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将流程状态转换为界面显示文本。
|
||||
/// </summary>
|
||||
/// <param name="state">待转换的流程状态。</param>
|
||||
/// <returns>中文状态描述。</returns>
|
||||
public static string ToDisplayText(this WorkflowState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
WorkflowState.Idle => "空闲等待",
|
||||
WorkflowState.Triggered => "已触发,准备启动流程",
|
||||
WorkflowState.Scanning => "正在扫码",
|
||||
WorkflowState.ScanRetrying => "扫码失败,等待重试",
|
||||
WorkflowState.ScanFailedReleased => "扫码失败放行",
|
||||
WorkflowState.CheckingSftp => "正在检查 SFTP 文件",
|
||||
WorkflowState.WaitingSftpRetry => "文件未命中,等待重试",
|
||||
WorkflowState.SftpPassed => "文件已找到,准备放行",
|
||||
WorkflowState.SftpTimeoutReleased => "文件未找到超时放行",
|
||||
WorkflowState.Releasing => "正在向 PLC 发送放行",
|
||||
WorkflowState.Completed => "流程已完成",
|
||||
WorkflowState.Faulted => "系统故障",
|
||||
_ => "未知状态"
|
||||
};
|
||||
}
|
||||
}
|
||||
16
src/AxiOmron.PcbCheck/NLog.config
Normal file
16
src/AxiOmron.PcbCheck/NLog.config
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
autoReload="true"
|
||||
internalLogLevel="Warn">
|
||||
<targets>
|
||||
<target xsi:type="File"
|
||||
name="file"
|
||||
fileName="${basedir}/logs/app-${shortdate}.log"
|
||||
layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
|
||||
<target xsi:type="Console" name="console" layout="${longdate}|${uppercase:${level}}|${logger}|${message}" />
|
||||
</targets>
|
||||
<rules>
|
||||
<logger name="*" minlevel="Info" writeTo="file,console" />
|
||||
</rules>
|
||||
</nlog>
|
||||
415
src/AxiOmron.PcbCheck/Options/AppConfig.cs
Normal file
415
src/AxiOmron.PcbCheck/Options/AppConfig.cs
Normal file
@@ -0,0 +1,415 @@
|
||||
namespace AxiOmron.PcbCheck.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 表示 PCB 目检上位机的根配置对象。
|
||||
/// </summary>
|
||||
public sealed class AppConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 通信配置。
|
||||
/// </summary>
|
||||
public PlcOptions Plc { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码枪配置。
|
||||
/// </summary>
|
||||
public ScannerOptions Scanner { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 校验配置。
|
||||
/// </summary>
|
||||
public SftpOptions Sftp { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置安灯接口配置。
|
||||
/// </summary>
|
||||
public AndonOptions Andon { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置流程控制配置。
|
||||
/// </summary>
|
||||
public WorkflowOptions Workflow { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示 PLC 通信参数与点位映射配置。
|
||||
/// </summary>
|
||||
public sealed class PlcOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 主机地址。
|
||||
/// </summary>
|
||||
public string Host { get; set; } = "127.0.0.1";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 端口。
|
||||
/// </summary>
|
||||
public int Port { get; set; } = 502;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 Modbus 从站号。
|
||||
/// </summary>
|
||||
public byte UnitId { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置轮询周期,单位为毫秒。
|
||||
/// </summary>
|
||||
public int PollIntervalMs { get; set; } = 200;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置连接超时,单位为毫秒。
|
||||
/// </summary>
|
||||
public int ConnectTimeoutMs { get; set; } = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PC 在线心跳翻转周期,单位为毫秒。
|
||||
/// </summary>
|
||||
public int HeartbeatIntervalMs { get; set; } = 500;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置放行脉冲持续时间,单位为毫秒。
|
||||
/// </summary>
|
||||
public int ReleasePulseMs { get; set; } = 500;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置放行应答超时,单位为毫秒。
|
||||
/// </summary>
|
||||
public int ReleaseAckTimeoutMs { get; set; } = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 输入点位配置。
|
||||
/// </summary>
|
||||
public PlcInputAddressOptions Inputs { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 输出点位配置。
|
||||
/// </summary>
|
||||
public PlcOutputAddressOptions Outputs { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 结果寄存器配置。
|
||||
/// </summary>
|
||||
public PlcRegisterAddressOptions Registers { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示扫码枪串口参数配置。
|
||||
/// </summary>
|
||||
public sealed class ScannerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置串口号。
|
||||
/// </summary>
|
||||
public string PortName { get; set; } = "COM1";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置波特率。
|
||||
/// </summary>
|
||||
public int BaudRate { get; set; } = 9600;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置数据位。
|
||||
/// </summary>
|
||||
public int DataBits { get; set; } = 8;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置校验位名称。
|
||||
/// </summary>
|
||||
public string Parity { get; set; } = "None";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置停止位名称。
|
||||
/// </summary>
|
||||
public string StopBits { get; set; } = "One";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置单次扫码超时,单位为毫秒。
|
||||
/// </summary>
|
||||
public int ReadTimeoutMs { get; set; } = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置触发命令,支持转义字符。
|
||||
/// </summary>
|
||||
public string TriggerCommand { get; set; } = "SCAN\\r";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置返回报文结束符,支持转义字符。
|
||||
/// </summary>
|
||||
public string ResponseTerminator { get; set; } = "\\r";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最大扫码尝试次数。
|
||||
/// </summary>
|
||||
public int MaxScanAttempts { get; set; } = 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示 SFTP 文件查找配置。
|
||||
/// </summary>
|
||||
public sealed class SftpOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 主机地址。
|
||||
/// </summary>
|
||||
public string Host { get; set; } = "127.0.0.1";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 端口。
|
||||
/// </summary>
|
||||
public int Port { get; set; } = 22;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置登录用户名。
|
||||
/// </summary>
|
||||
public string Username { get; set; } = "user";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置登录密码。
|
||||
/// </summary>
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置私钥文件路径。
|
||||
/// </summary>
|
||||
public string PrivateKeyPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置私钥口令。
|
||||
/// </summary>
|
||||
public string PrivateKeyPassphrase { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置根目录。
|
||||
/// </summary>
|
||||
public string RootPath { get; set; } = "/pcb";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置文件名匹配模板。
|
||||
/// </summary>
|
||||
public string FileNamePattern { get; set; } = "${barcode}.txt";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置首次未命中后的重试间隔,单位为秒。
|
||||
/// </summary>
|
||||
public int RetryIntervalSeconds { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置首次未命中后的最大重试次数。
|
||||
/// </summary>
|
||||
public int MaxRetryCount { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置连接超时,单位为毫秒。
|
||||
/// </summary>
|
||||
public int ConnectTimeoutMs { get; set; } = 3000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示安灯 HTTP 接口配置。
|
||||
/// </summary>
|
||||
public sealed class AndonOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置是否启用安灯接口。
|
||||
/// </summary>
|
||||
public bool Enable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置安灯接口地址。
|
||||
/// </summary>
|
||||
public string Url { get; set; } = "http://127.0.0.1:5000/api/andon";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置请求方法。
|
||||
/// </summary>
|
||||
public string Method { get; set; } = "POST";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置请求超时,单位为毫秒。
|
||||
/// </summary>
|
||||
public int TimeoutMs { get; set; } = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置工位编码。
|
||||
/// </summary>
|
||||
public string StationCode { get; set; } = "OMRON-01";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置工位名称。
|
||||
/// </summary>
|
||||
public string StationName { get; set; } = "PCB 目检工位";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码失败报警是否启用。
|
||||
/// </summary>
|
||||
public bool EnableScanFailAlarm { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置文件未找到报警是否启用。
|
||||
/// </summary>
|
||||
public bool EnableFileNotFoundAlarm { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置附加请求头。
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Headers { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示流程控制公共配置。
|
||||
/// </summary>
|
||||
public sealed class WorkflowOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置启动流程前是否要求 PLC 就绪。
|
||||
/// </summary>
|
||||
public bool RequirePlcReady { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置启动流程前是否要求自动模式。
|
||||
/// </summary>
|
||||
public bool RequireAutoMode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置启动流程前是否要求工位使能。
|
||||
/// </summary>
|
||||
public bool RequireStationEnable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置故障后是否必须人工复位。
|
||||
/// </summary>
|
||||
public bool RequireManualResetAfterFault { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 UI 日志最大保留条数。
|
||||
/// </summary>
|
||||
public int MaxUiLogEntries { get; set; } = 200;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最近处理记录最大保留条数。
|
||||
/// </summary>
|
||||
public int MaxBoardRecords { get; set; } = 100;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示 PLC 输入点位地址配置。
|
||||
/// </summary>
|
||||
public sealed class PlcInputAddressOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 就绪点位地址。
|
||||
/// </summary>
|
||||
public int PlcReady { get; set; } = 10001;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PCB 到位点位地址。
|
||||
/// </summary>
|
||||
public int PcbArrived { get; set; } = 10002;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 复位点位地址。
|
||||
/// </summary>
|
||||
public int PlcReset { get; set; } = 10003;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 放行应答点位地址。
|
||||
/// </summary>
|
||||
public int PlcAckRelease { get; set; } = 10004;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置自动模式点位地址。
|
||||
/// </summary>
|
||||
public int AutoMode { get; set; } = 10005;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置工位使能点位地址。
|
||||
/// </summary>
|
||||
public int StationEnable { get; set; } = 10006;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示 PLC 输出线圈地址配置。
|
||||
/// </summary>
|
||||
public sealed class PlcOutputAddressOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 PC 在线心跳位地址。
|
||||
/// </summary>
|
||||
public int PcOnline { get; set; } = 51;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PC 忙碌位地址。
|
||||
/// </summary>
|
||||
public int PcBusy { get; set; } = 52;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码成功位地址。
|
||||
/// </summary>
|
||||
public int ScanOk { get; set; } = 53;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码失败位地址。
|
||||
/// </summary>
|
||||
public int ScanNg { get; set; } = 54;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置文件存在位地址。
|
||||
/// </summary>
|
||||
public int FileFound { get; set; } = 55;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置文件未找到位地址。
|
||||
/// </summary>
|
||||
public int FileNotFound { get; set; } = 56;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置报警位地址。
|
||||
/// </summary>
|
||||
public int AlarmRaised { get; set; } = 57;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置放行位地址。
|
||||
/// </summary>
|
||||
public int ReleasePermit { get; set; } = 58;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置流程完成位地址。
|
||||
/// </summary>
|
||||
public int ProcessDone { get; set; } = 59;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置系统故障位地址。
|
||||
/// </summary>
|
||||
public int SystemFault { get; set; } = 60;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示 PLC 寄存器地址配置。
|
||||
/// </summary>
|
||||
public sealed class PlcRegisterAddressOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置结果代码寄存器地址。
|
||||
/// </summary>
|
||||
public int ResultCode { get; set; } = 40001;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码次数寄存器地址。
|
||||
/// </summary>
|
||||
public int ScanTryCount { get; set; } = 40002;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 查询次数寄存器地址。
|
||||
/// </summary>
|
||||
public int SftpTryCount { get; set; } = 40003;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置报警代码寄存器地址。
|
||||
/// </summary>
|
||||
public int AlarmCode { get; set; } = 40004;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置流程状态代码寄存器地址。
|
||||
/// </summary>
|
||||
public int FlowStateCode { get; set; } = 40005;
|
||||
}
|
||||
118
src/AxiOmron.PcbCheck/Services/Implementations/AndonService.cs
Normal file
118
src/AxiOmron.PcbCheck/Services/Implementations/AndonService.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
namespace AxiOmron.PcbCheck.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// 提供安灯 HTTP 接口调用能力。
|
||||
/// </summary>
|
||||
public sealed class AndonService : IAndonService
|
||||
{
|
||||
private readonly AndonOptions _options;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IAppLogger<AndonService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化安灯服务。
|
||||
/// </summary>
|
||||
/// <param name="config">应用根配置。</param>
|
||||
/// <param name="httpClientFactory">HttpClient 工厂。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
public AndonService(AppConfig config, IHttpClientFactory httpClientFactory, IAppLogger<AndonService> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_options = config.Andon;
|
||||
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送安灯报警。
|
||||
/// </summary>
|
||||
/// <param name="request">报警请求对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>报警调用结果。</returns>
|
||||
public async Task<AndonOperationResult> RaiseAlarmAsync(AndonAlarmRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (!_options.Enable || string.IsNullOrWhiteSpace(_options.Url))
|
||||
{
|
||||
return new AndonOperationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
EndpointReached = false,
|
||||
ErrorMessage = "安灯接口未启用或 URL 未配置。"
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var client = _httpClientFactory.CreateClient(nameof(AndonService));
|
||||
client.Timeout = TimeSpan.FromMilliseconds(_options.TimeoutMs);
|
||||
using var message = new HttpRequestMessage(new HttpMethod(_options.Method), _options.Url)
|
||||
{
|
||||
Content = JsonContent.Create(new
|
||||
{
|
||||
stationCode = _options.StationCode,
|
||||
stationName = _options.StationName,
|
||||
alarmType = request.AlarmType,
|
||||
alarmCode = request.AlarmCode,
|
||||
alarmMessage = request.AlarmMessage,
|
||||
barcode = request.Barcode,
|
||||
triggeredAt = request.TriggeredAt,
|
||||
machineName = Environment.MachineName
|
||||
})
|
||||
};
|
||||
|
||||
foreach (var header in _options.Headers)
|
||||
{
|
||||
message.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
var response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
return new AndonOperationResult
|
||||
{
|
||||
IsSuccess = response.IsSuccessStatusCode,
|
||||
EndpointReached = true,
|
||||
StatusCode = (int)response.StatusCode,
|
||||
ResponseBody = body,
|
||||
ErrorMessage = response.IsSuccessStatusCode ? string.Empty : $"HTTP {(int)response.StatusCode}: {body}"
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "安灯接口调用失败");
|
||||
return new AndonOperationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
EndpointReached = false,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送一次测试报警请求。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>测试调用结果。</returns>
|
||||
public Task<AndonOperationResult> TestAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return RaiseAlarmAsync(new AndonAlarmRequest
|
||||
{
|
||||
AlarmType = "ManualTest",
|
||||
AlarmCode = (ushort)AlarmCode.ScanFailed,
|
||||
AlarmMessage = "手动测试安灯接口",
|
||||
Barcode = string.Empty,
|
||||
TriggeredAt = DateTimeOffset.Now
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// 提供应用配置文件的读取与保存能力。
|
||||
/// </summary>
|
||||
public sealed class AppConfigService : IAppConfigService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前应用配置。
|
||||
/// </summary>
|
||||
/// <returns>根配置对象。</returns>
|
||||
public AppConfig Load()
|
||||
{
|
||||
var configPath = GetConfigPath();
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
var defaultConfig = new AppConfig();
|
||||
Save(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(configPath);
|
||||
var config = JsonSerializer.Deserialize<AppConfig>(json, JsonOptions);
|
||||
return config ?? new AppConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前应用配置。
|
||||
/// </summary>
|
||||
/// <param name="config">待保存的配置对象。</param>
|
||||
public void Save(AppConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
var configPath = GetConfigPath();
|
||||
var directory = Path.GetDirectoryName(configPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(config, JsonOptions);
|
||||
File.WriteAllText(configPath, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取主配置文件路径。
|
||||
/// </summary>
|
||||
/// <returns>配置文件绝对路径。</returns>
|
||||
public string GetConfigPath()
|
||||
{
|
||||
return Path.Combine(AppContext.BaseDirectory, "appConfig.json");
|
||||
}
|
||||
}
|
||||
154
src/AxiOmron.PcbCheck/Services/Implementations/AppLogger.cs
Normal file
154
src/AxiOmron.PcbCheck/Services/Implementations/AppLogger.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System.Globalization;
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// 提供应用统一日志能力,并在需要时同步前台 UI 日志。
|
||||
/// </summary>
|
||||
/// <typeparam name="TCategoryName">日志分类类型。</typeparam>
|
||||
public sealed class AppLogger<TCategoryName> : IAppLogger<TCategoryName>
|
||||
{
|
||||
private readonly ILogger<TCategoryName> _logger;
|
||||
private readonly IAppStateStore _stateStore;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化统一日志服务。
|
||||
/// </summary>
|
||||
/// <param name="logger">底层标准日志记录器。</param>
|
||||
/// <param name="stateStore">运行态存储。</param>
|
||||
public AppLogger(ILogger<TCategoryName> logger, IAppStateStore stateStore)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_stateStore = stateStore ?? throw new ArgumentNullException(nameof(stateStore));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录一条信息日志。
|
||||
/// </summary>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
public void LogInformation(string message, bool showInUi = false, params object?[] args)
|
||||
{
|
||||
_logger.LogInformation(message, args);
|
||||
PublishUiLog(LogLevel.Information, message, null, showInUi, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录一条警告日志。
|
||||
/// </summary>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
public void LogWarning(string message, bool showInUi = false, params object?[] args)
|
||||
{
|
||||
_logger.LogWarning(message, args);
|
||||
PublishUiLog(LogLevel.Warning, message, null, showInUi, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录一条带异常的警告日志。
|
||||
/// </summary>
|
||||
/// <param name="exception">异常对象。</param>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public void LogWarning(Exception exception, string message, bool showInUi = false, params object?[] args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
_logger.LogWarning(exception, message, args);
|
||||
PublishUiLog(LogLevel.Warning, message, exception, showInUi, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录一条错误日志。
|
||||
/// </summary>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
public void LogError(string message, bool showInUi = false, params object?[] args)
|
||||
{
|
||||
_logger.LogError(message, args);
|
||||
PublishUiLog(LogLevel.Error, message, null, showInUi, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录一条带异常的错误日志。
|
||||
/// </summary>
|
||||
/// <param name="exception">异常对象。</param>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public void LogError(Exception exception, string message, bool showInUi = false, params object?[] args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
_logger.LogError(exception, message, args);
|
||||
PublishUiLog(LogLevel.Error, message, exception, showInUi, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按需向前台运行态发布日志。
|
||||
/// </summary>
|
||||
/// <param name="logLevel">日志级别。</param>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="exception">异常对象。</param>
|
||||
/// <param name="showInUi">是否显示到前台。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
private void PublishUiLog(LogLevel logLevel, string message, Exception? exception, bool showInUi, params object?[] args)
|
||||
{
|
||||
if (!showInUi)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var formattedMessage = FormatMessage(message, args);
|
||||
if (exception is not null)
|
||||
{
|
||||
formattedMessage = string.IsNullOrWhiteSpace(formattedMessage)
|
||||
? exception.Message
|
||||
: $"{formattedMessage}: {exception.Message}";
|
||||
}
|
||||
|
||||
_stateStore.AddLog(new UiLogEntry
|
||||
{
|
||||
Level = logLevel.ToString(),
|
||||
Message = formattedMessage,
|
||||
Timestamp = DateTimeOffset.Now
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将日志模板与参数格式化为可展示文本。
|
||||
/// </summary>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
/// <returns>格式化后的文本。</returns>
|
||||
private static string FormatMessage(string message, params object?[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
var formattedMessage = message;
|
||||
for (var index = 0; index < args.Length; index++)
|
||||
{
|
||||
var replacement = Convert.ToString(args[index], CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
var tokenStart = formattedMessage.IndexOf('{', StringComparison.Ordinal);
|
||||
var tokenEnd = tokenStart >= 0 ? formattedMessage.IndexOf('}', tokenStart + 1) : -1;
|
||||
if (tokenStart < 0 || tokenEnd <= tokenStart)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
formattedMessage = formattedMessage.Remove(tokenStart, tokenEnd - tokenStart + 1).Insert(tokenStart, replacement);
|
||||
}
|
||||
|
||||
return formattedMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// 提供运行态快照、日志与单板记录的线程安全存储能力。
|
||||
/// </summary>
|
||||
public sealed class AppStateStore : IAppStateStore
|
||||
{
|
||||
private readonly object _syncRoot = new();
|
||||
private RuntimeSnapshot _snapshot = new();
|
||||
|
||||
/// <summary>
|
||||
/// 当运行态快照变化时触发。
|
||||
/// </summary>
|
||||
public event EventHandler<RuntimeSnapshot>? SnapshotChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 当新增日志时触发。
|
||||
/// </summary>
|
||||
public event EventHandler<UiLogEntry>? LogAdded;
|
||||
|
||||
/// <summary>
|
||||
/// 当新增单板记录时触发。
|
||||
/// </summary>
|
||||
public event EventHandler<BoardProcessRecord>? RecordAdded;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前运行态快照副本。
|
||||
/// </summary>
|
||||
/// <returns>当前快照副本。</returns>
|
||||
public RuntimeSnapshot GetSnapshot()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _snapshot.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前运行态快照。
|
||||
/// </summary>
|
||||
/// <param name="updateAction">用于修改快照的更新委托。</param>
|
||||
public void UpdateSnapshot(Action<RuntimeSnapshot> updateAction)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(updateAction);
|
||||
|
||||
RuntimeSnapshot clonedSnapshot;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
updateAction(_snapshot);
|
||||
_snapshot.LastUpdatedAt = DateTimeOffset.Now;
|
||||
clonedSnapshot = _snapshot.Clone();
|
||||
}
|
||||
|
||||
SnapshotChanged?.Invoke(this, clonedSnapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加一条 UI 日志。
|
||||
/// </summary>
|
||||
/// <param name="entry">待追加的日志对象。</param>
|
||||
public void AddLog(UiLogEntry entry)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entry);
|
||||
LogAdded?.Invoke(this, entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加一条单板结果记录。
|
||||
/// </summary>
|
||||
/// <param name="record">待追加的记录对象。</param>
|
||||
public void AddRecord(BoardProcessRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
RecordAdded?.Invoke(this, record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Windows.Threading;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// 提供切回 WPF UI 线程的调度能力。
|
||||
/// </summary>
|
||||
public sealed class DispatcherService : IDispatcherService
|
||||
{
|
||||
/// <summary>
|
||||
/// 在 UI 线程中执行指定动作。
|
||||
/// </summary>
|
||||
/// <param name="action">待执行的动作。</param>
|
||||
/// <returns>表示调度完成的任务。</returns>
|
||||
public Task InvokeAsync(Action action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
|
||||
var dispatcher = System.Windows.Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||
return dispatcher.InvokeAsync(action).Task;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
using IoTClient.Clients.Modbus;
|
||||
using IoTClient.Enums;
|
||||
namespace AxiOmron.PcbCheck.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// 提供基于 IoTClient ModbusTcpClient 的 PLC 读写能力。
|
||||
/// </summary>
|
||||
public sealed class ModbusTcpPlcService : IPlcService, IDisposable
|
||||
{
|
||||
private readonly PlcOptions _options;
|
||||
private readonly IAppLogger<ModbusTcpPlcService> _logger;
|
||||
private readonly SemaphoreSlim _ioLock = new(1, 1);
|
||||
private ModbusTcpClient? _client;
|
||||
private PlcProcessState? _lastWrittenState;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 PLC 通信服务。
|
||||
/// </summary>
|
||||
/// <param name="config">应用根配置。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
public ModbusTcpPlcService(AppConfig config, IAppLogger<ModbusTcpPlcService> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_options = config.Plc;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 PLC 输入信号快照。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>输入信号快照。</returns>
|
||||
public async Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
EnsureConnected();
|
||||
|
||||
return new PlcSignalSnapshot
|
||||
{
|
||||
PlcReady = ReadDiscrete(_options.Inputs.PlcReady),
|
||||
PcbArrived = ReadDiscrete(_options.Inputs.PcbArrived),
|
||||
PlcReset = ReadDiscrete(_options.Inputs.PlcReset),
|
||||
PlcAckRelease = ReadDiscrete(_options.Inputs.PlcAckRelease),
|
||||
AutoMode = ReadDiscrete(_options.Inputs.AutoMode),
|
||||
StationEnable = ReadDiscrete(_options.Inputs.StationEnable),
|
||||
CapturedAt = DateTimeOffset.Now
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
DisconnectUnsafe();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ioLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入 PLC 输出状态与寄存器值。
|
||||
/// </summary>
|
||||
/// <param name="state">待写入的输出状态。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示写入完成的任务。</returns>
|
||||
public async Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
|
||||
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
EnsureConnected();
|
||||
WriteChangedState(state);
|
||||
_lastWrittenState = state.Clone();
|
||||
}
|
||||
catch
|
||||
{
|
||||
DisconnectUnsafe();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ioLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主动断开并重建 PLC 连接。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示重连完成的任务。</returns>
|
||||
public async Task ForceReconnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
DisconnectUnsafe();
|
||||
_lastWrittenState = null;
|
||||
EnsureConnected();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ioLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 PLC 通信资源。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
DisconnectUnsafe();
|
||||
_ioLock.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保 IoTClient Modbus TCP 客户端已连接。
|
||||
/// </summary>
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (_client is { Connected: true })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DisconnectUnsafe();
|
||||
var client = new ModbusTcpClient(_options.Host, _options.Port, _options.ConnectTimeoutMs, EndianFormat.ABCD, false);
|
||||
var openResult = client.Open();
|
||||
EnsureSuccess(openResult.IsSucceed, openResult.Err, "连接 PLC 失败");
|
||||
_client = client;
|
||||
_logger.LogInformation("已通过 IoTClient 连接 PLC {Host}:{Port}", false, _options.Host, _options.Port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取单个离散输入位。
|
||||
/// </summary>
|
||||
/// <param name="address">离散输入地址。</param>
|
||||
/// <returns>读取到的布尔值。</returns>
|
||||
private bool ReadDiscrete(int address)
|
||||
{
|
||||
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
||||
var result = client.ReadDiscrete(address, _options.UnitId, 2);
|
||||
EnsureSuccess(result.IsSucceed, result.Err, $"读取离散输入失败,地址={address}");
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入所有发生变化的线圈位与寄存器。
|
||||
/// </summary>
|
||||
/// <param name="state">目标状态。</param>
|
||||
private void WriteChangedState(PlcProcessState state)
|
||||
{
|
||||
var previous = _lastWrittenState;
|
||||
|
||||
WriteSingleCoilIfChanged(previous?.PcOnline, state.PcOnline, _options.Outputs.PcOnline);
|
||||
WriteSingleCoilIfChanged(previous?.PcBusy, state.PcBusy, _options.Outputs.PcBusy);
|
||||
WriteSingleCoilIfChanged(previous?.ScanOk, state.ScanOk, _options.Outputs.ScanOk);
|
||||
WriteSingleCoilIfChanged(previous?.ScanNg, state.ScanNg, _options.Outputs.ScanNg);
|
||||
WriteSingleCoilIfChanged(previous?.FileFound, state.FileFound, _options.Outputs.FileFound);
|
||||
WriteSingleCoilIfChanged(previous?.FileNotFound, state.FileNotFound, _options.Outputs.FileNotFound);
|
||||
WriteSingleCoilIfChanged(previous?.AlarmRaised, state.AlarmRaised, _options.Outputs.AlarmRaised);
|
||||
WriteSingleCoilIfChanged(previous?.ReleasePermit, state.ReleasePermit, _options.Outputs.ReleasePermit);
|
||||
WriteSingleCoilIfChanged(previous?.ProcessDone, state.ProcessDone, _options.Outputs.ProcessDone);
|
||||
WriteSingleCoilIfChanged(previous?.SystemFault, state.SystemFault, _options.Outputs.SystemFault);
|
||||
|
||||
WriteSingleRegisterIfChanged(previous?.ResultCode, state.ResultCode, _options.Registers.ResultCode);
|
||||
WriteSingleRegisterIfChanged(previous?.ScanTryCount, state.ScanTryCount, _options.Registers.ScanTryCount);
|
||||
WriteSingleRegisterIfChanged(previous?.SftpTryCount, state.SftpTryCount, _options.Registers.SftpTryCount);
|
||||
WriteSingleRegisterIfChanged(previous?.AlarmCode, state.AlarmCode, _options.Registers.AlarmCode);
|
||||
WriteSingleRegisterIfChanged(previous?.FlowStateCode, state.FlowStateCode, _options.Registers.FlowStateCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅当值发生变化时写入单个线圈位。
|
||||
/// </summary>
|
||||
/// <param name="previous">上一值。</param>
|
||||
/// <param name="current">当前值。</param>
|
||||
/// <param name="address">线圈地址。</param>
|
||||
private void WriteSingleCoilIfChanged(bool? previous, bool current, int address)
|
||||
{
|
||||
if (previous.HasValue && previous.Value == current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
||||
var result = client.Write(address.ToString(), current, _options.UnitId, 5);
|
||||
EnsureSuccess(result.IsSucceed, result.Err, $"写入线圈失败,地址={address},值={current}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅当值发生变化时写入单个保持寄存器。
|
||||
/// </summary>
|
||||
/// <param name="previous">上一值。</param>
|
||||
/// <param name="current">当前值。</param>
|
||||
/// <param name="address">保持寄存器地址。</param>
|
||||
private void WriteSingleRegisterIfChanged(ushort? previous, ushort current, int address)
|
||||
{
|
||||
if (previous.HasValue && previous.Value == current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
||||
var result = client.Write(address.ToString(), current, _options.UnitId, 6);
|
||||
EnsureSuccess(result.IsSucceed, result.Err, $"写入保持寄存器失败,地址={address},值={current}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 IoTClient 调用结果是否成功。
|
||||
/// </summary>
|
||||
/// <param name="isSucceed">调用是否成功。</param>
|
||||
/// <param name="error">错误消息。</param>
|
||||
/// <param name="message">异常消息前缀。</param>
|
||||
private static void EnsureSuccess(bool isSucceed, string? error, string message)
|
||||
{
|
||||
if (!isSucceed)
|
||||
{
|
||||
throw new InvalidOperationException(string.IsNullOrWhiteSpace(error) ? message : $"{message}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开当前 PLC 客户端连接。
|
||||
/// </summary>
|
||||
private void DisconnectUnsafe()
|
||||
{
|
||||
try
|
||||
{
|
||||
_client?.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略关闭异常。
|
||||
}
|
||||
finally
|
||||
{
|
||||
_client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Text;
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
using AxiOmron.PcbCheck.Utils;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// 提供串口扫码枪的触发与读取能力。
|
||||
/// </summary>
|
||||
public sealed class SerialScannerService : IScannerService, IDisposable
|
||||
{
|
||||
private readonly ScannerOptions _options;
|
||||
private readonly IAppLogger<SerialScannerService> _logger;
|
||||
private readonly SemaphoreSlim _ioLock = new(1, 1);
|
||||
private SerialPort? _serialPort;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化扫码枪服务。
|
||||
/// </summary>
|
||||
/// <param name="config">应用根配置。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
public SerialScannerService(AppConfig config, IAppLogger<SerialScannerService> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_options = config.Scanner;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发一次扫码。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>扫码结果。</returns>
|
||||
public async Task<ScanOperationResult> TriggerScanAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var lockTaken = false;
|
||||
try
|
||||
{
|
||||
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
lockTaken = true;
|
||||
return await TriggerScanInternalAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "扫码操作失败");
|
||||
return new ScanOperationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
IsSystemError = ex is not OperationCanceledException,
|
||||
DeviceConnected = false,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
{
|
||||
_ioLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试扫码枪连接。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>连接正常返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
||||
public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var lockTaken = false;
|
||||
try
|
||||
{
|
||||
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
lockTaken = true;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
EnsurePortOpen();
|
||||
return _serialPort is { IsOpen: true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "扫码枪连接测试失败");
|
||||
ClosePortUnsafe();
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
{
|
||||
_ioLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主动断开并重建扫码枪连接。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示重连完成的任务。</returns>
|
||||
public async Task ForceReconnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var lockTaken = false;
|
||||
try
|
||||
{
|
||||
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
lockTaken = true;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ClosePortUnsafe();
|
||||
EnsurePortOpen();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "扫码枪强制重连失败");
|
||||
ClosePortUnsafe();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
{
|
||||
_ioLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放扫码枪串口资源。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
ClosePortUnsafe();
|
||||
_ioLock.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在异步上下文中执行一次扫码流程。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>扫码结果。</returns>
|
||||
private async Task<ScanOperationResult> TriggerScanInternalAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
EnsurePortOpen();
|
||||
var port = _serialPort ?? throw new InvalidOperationException("扫码枪串口尚未初始化。");
|
||||
port.DiscardInBuffer();
|
||||
port.DiscardOutBuffer();
|
||||
port.Write(StringEscapeHelper.Unescape(_options.TriggerCommand));
|
||||
|
||||
var rawMessage = await ReadUntilTerminatorAsync(
|
||||
port,
|
||||
StringEscapeHelper.Unescape(_options.ResponseTerminator),
|
||||
_options.ReadTimeoutMs,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var barcode = BarcodeCleaner.Clean(rawMessage);
|
||||
if (string.IsNullOrEmpty(barcode))
|
||||
{
|
||||
return new ScanOperationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
DeviceConnected = true,
|
||||
RawMessage = rawMessage,
|
||||
ErrorMessage = "扫码返回空字符串或仅包含控制字符。",
|
||||
DurationMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
|
||||
return new ScanOperationResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
DeviceConnected = true,
|
||||
Barcode = barcode,
|
||||
RawMessage = rawMessage,
|
||||
DurationMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
catch (TimeoutException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "扫码等待超时");
|
||||
return new ScanOperationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
DeviceConnected = true,
|
||||
ErrorMessage = "扫码超时。",
|
||||
DurationMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "扫码枪执行失败");
|
||||
ClosePortUnsafe();
|
||||
return new ScanOperationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
IsSystemError = true,
|
||||
DeviceConnected = false,
|
||||
ErrorMessage = ex.Message,
|
||||
DurationMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保串口已打开并按当前配置初始化。
|
||||
/// </summary>
|
||||
private void EnsurePortOpen()
|
||||
{
|
||||
if (_serialPort is { IsOpen: true })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClosePortUnsafe();
|
||||
|
||||
var availablePorts = SerialPort.GetPortNames();
|
||||
if (!availablePorts.Contains(_options.PortName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning("本地不存在串口 {PortName},可用串口: {AvailablePorts}", false, _options.PortName, string.Join(", ", availablePorts));
|
||||
throw new IOException($"串口 {_options.PortName} 不存在。可用串口: {string.Join(", ", availablePorts)}");
|
||||
}
|
||||
|
||||
var parity = Enum.Parse<Parity>(_options.Parity, true);
|
||||
var stopBits = Enum.Parse<StopBits>(_options.StopBits, true);
|
||||
var serialPort = new SerialPort(_options.PortName, _options.BaudRate, parity, _options.DataBits, stopBits)
|
||||
{
|
||||
ReadTimeout = 200,
|
||||
WriteTimeout = 1000,
|
||||
Encoding = Encoding.ASCII,
|
||||
DtrEnable = true,
|
||||
RtsEnable = true,
|
||||
NewLine = StringEscapeHelper.Unescape(_options.ResponseTerminator)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
serialPort.Open();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "打开串口 {PortName} 失败", false, _options.PortName);
|
||||
serialPort.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
_serialPort = serialPort;
|
||||
_logger.LogInformation("已连接扫码枪串口 {PortName}", false, _options.PortName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从串口读取直到遇到终止符或超时。
|
||||
/// </summary>
|
||||
/// <param name="port">串口实例。</param>
|
||||
/// <param name="terminator">终止符。</param>
|
||||
/// <param name="timeoutMs">总超时时间,单位为毫秒。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>读取到的原始字符串。</returns>
|
||||
private static async Task<string> ReadUntilTerminatorAsync(SerialPort port, string terminator, int timeoutMs, CancellationToken cancellationToken)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (stopwatch.ElapsedMilliseconds < timeoutMs)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var fragment = port.ReadExisting();
|
||||
if (!string.IsNullOrEmpty(fragment))
|
||||
{
|
||||
buffer.Append(fragment);
|
||||
if (string.IsNullOrEmpty(terminator) || buffer.ToString().Contains(terminator, StringComparison.Ordinal))
|
||||
{
|
||||
return buffer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(20, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
throw new TimeoutException("扫码枪在规定时间内未返回完整报文。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭当前串口并释放资源。
|
||||
/// </summary>
|
||||
private void ClosePortUnsafe()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_serialPort?.IsOpen == true)
|
||||
{
|
||||
_serialPort.Close();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略关闭异常。
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serialPort?.Dispose();
|
||||
_serialPort = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
using AxiOmron.PcbCheck.Utils;
|
||||
using Renci.SshNet;
|
||||
using Renci.SshNet.Common;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 SFTP 文件存在性校验能力。
|
||||
/// </summary>
|
||||
public sealed class SftpLookupService : ISftpLookupService
|
||||
{
|
||||
private readonly SftpOptions _options;
|
||||
private readonly IAppLogger<SftpLookupService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 SFTP 校验服务。
|
||||
/// </summary>
|
||||
/// <param name="config">应用根配置。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
public SftpLookupService(AppConfig config, IAppLogger<SftpLookupService> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_options = config.Sftp;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按条码检查目标文件是否存在。
|
||||
/// </summary>
|
||||
/// <param name="barcode">条码内容。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>文件校验结果。</returns>
|
||||
public async Task<SftpCheckOutcome> CheckFileAsync(string barcode, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(barcode))
|
||||
{
|
||||
return new SftpCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsConfigurationError = true,
|
||||
ErrorMessage = "条码为空,无法执行 SFTP 查询。"
|
||||
};
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_options.Host) || string.IsNullOrWhiteSpace(_options.RootPath))
|
||||
{
|
||||
return new SftpCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsConfigurationError = true,
|
||||
ErrorMessage = "SFTP 配置缺失 Host 或 RootPath。"
|
||||
};
|
||||
}
|
||||
|
||||
return await Task.Run(() => CheckInternal(barcode, cancellationToken), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在同步上下文中执行 SFTP 查询。
|
||||
/// </summary>
|
||||
/// <param name="barcode">条码。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>查询结果。</returns>
|
||||
private SftpCheckOutcome CheckInternal(string barcode, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
using var client = CreateClient();
|
||||
client.ConnectionInfo.Timeout = TimeSpan.FromMilliseconds(_options.ConnectTimeoutMs);
|
||||
client.Connect();
|
||||
|
||||
if (!client.IsConnected)
|
||||
{
|
||||
return new SftpCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsSystemError = true,
|
||||
ErrorMessage = "SFTP 未能建立连接。"
|
||||
};
|
||||
}
|
||||
|
||||
var candidateName = BuildExpectedFileName(barcode);
|
||||
var rootPath = NormalizeDirectory(_options.RootPath);
|
||||
if (!client.Exists(rootPath))
|
||||
{
|
||||
return new SftpCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsConfigurationError = true,
|
||||
ConnectionSucceeded = true,
|
||||
ErrorMessage = $"SFTP 根目录不存在: {rootPath}"
|
||||
};
|
||||
}
|
||||
|
||||
var matched = client.ListDirectory(rootPath)
|
||||
.Where(entry => !entry.IsDirectory && !entry.IsSymbolicLink)
|
||||
.FirstOrDefault(entry =>
|
||||
entry.Name.Equals(candidateName, StringComparison.OrdinalIgnoreCase)
|
||||
|| WildcardMatcher.IsMatch(entry.Name, candidateName)
|
||||
|| entry.Name.Contains(barcode, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (matched is null)
|
||||
{
|
||||
return new SftpCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
ConnectionSucceeded = true,
|
||||
ErrorMessage = $"未找到与条码 {barcode} 匹配的文件。"
|
||||
};
|
||||
}
|
||||
|
||||
return new SftpCheckOutcome
|
||||
{
|
||||
Exists = true,
|
||||
ConnectionSucceeded = true,
|
||||
MatchedFilePath = matched.FullName
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (SshAuthenticationException ex)
|
||||
{
|
||||
_logger.LogError(ex, "SFTP 认证失败");
|
||||
return new SftpCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsSystemError = true,
|
||||
ErrorMessage = $"SFTP 认证失败: {ex.Message}"
|
||||
};
|
||||
}
|
||||
catch (SshConnectionException ex)
|
||||
{
|
||||
_logger.LogError(ex, "SFTP 连接失败");
|
||||
return new SftpCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsSystemError = true,
|
||||
ErrorMessage = $"SFTP 连接失败: {ex.Message}"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "SFTP 查询异常");
|
||||
return new SftpCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsSystemError = true,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前配置创建 SFTP 客户端。
|
||||
/// </summary>
|
||||
/// <returns>SFTP 客户端实例。</returns>
|
||||
private SftpClient CreateClient()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_options.PrivateKeyPath))
|
||||
{
|
||||
var privateKeyFile = string.IsNullOrWhiteSpace(_options.PrivateKeyPassphrase)
|
||||
? new PrivateKeyFile(_options.PrivateKeyPath)
|
||||
: new PrivateKeyFile(_options.PrivateKeyPath, _options.PrivateKeyPassphrase);
|
||||
|
||||
var keyAuth = new PrivateKeyAuthenticationMethod(_options.Username, privateKeyFile);
|
||||
var connectionInfo = new ConnectionInfo(_options.Host, _options.Port, _options.Username, keyAuth);
|
||||
return new SftpClient(connectionInfo);
|
||||
}
|
||||
|
||||
return new SftpClient(_options.Host, _options.Port, _options.Username, _options.Password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据条码和模板构建预期文件名。
|
||||
/// </summary>
|
||||
/// <param name="barcode">条码。</param>
|
||||
/// <returns>预期文件名或匹配模式。</returns>
|
||||
private string BuildExpectedFileName(string barcode)
|
||||
{
|
||||
var pattern = string.IsNullOrWhiteSpace(_options.FileNamePattern) ? "${barcode}.txt" : _options.FileNamePattern;
|
||||
return pattern.Replace("${barcode}", barcode, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一目录路径格式。
|
||||
/// </summary>
|
||||
/// <param name="path">原始目录路径。</param>
|
||||
/// <returns>标准化目录路径。</returns>
|
||||
private static string NormalizeDirectory(string path)
|
||||
{
|
||||
return path.Replace('\\', '/').TrimEnd('/');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
261
src/AxiOmron.PcbCheck/Services/Interfaces/CoreInterfaces.cs
Normal file
261
src/AxiOmron.PcbCheck/Services/Interfaces/CoreInterfaces.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 定义应用配置文件读写能力。
|
||||
/// </summary>
|
||||
public interface IAppConfigService
|
||||
{
|
||||
/// <summary>
|
||||
/// 读取当前应用配置。
|
||||
/// </summary>
|
||||
/// <returns>根配置对象。</returns>
|
||||
AppConfig Load();
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前应用配置。
|
||||
/// </summary>
|
||||
/// <param name="config">待保存的配置对象。</param>
|
||||
void Save(AppConfig config);
|
||||
|
||||
/// <summary>
|
||||
/// 获取主配置文件路径。
|
||||
/// </summary>
|
||||
/// <returns>配置文件绝对路径。</returns>
|
||||
string GetConfigPath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义 WPF Dispatcher 调度能力。
|
||||
/// </summary>
|
||||
public interface IDispatcherService
|
||||
{
|
||||
/// <summary>
|
||||
/// 在 UI 线程中执行指定动作。
|
||||
/// </summary>
|
||||
/// <param name="action">待执行的动作。</param>
|
||||
/// <returns>表示调度完成的任务。</returns>
|
||||
Task InvokeAsync(Action action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义应用统一日志能力,同时兼容持久化日志与前台 UI 日志分发。
|
||||
/// </summary>
|
||||
/// <typeparam name="TCategoryName">日志分类类型。</typeparam>
|
||||
public interface IAppLogger<TCategoryName>
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录一条信息日志。
|
||||
/// </summary>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
void LogInformation(string message, bool showInUi = false, params object?[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 记录一条警告日志。
|
||||
/// </summary>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
void LogWarning(string message, bool showInUi = false, params object?[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 记录一条带异常的警告日志。
|
||||
/// </summary>
|
||||
/// <param name="exception">异常对象。</param>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
void LogWarning(Exception exception, string message, bool showInUi = false, params object?[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 记录一条错误日志。
|
||||
/// </summary>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
void LogError(string message, bool showInUi = false, params object?[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 记录一条带异常的错误日志。
|
||||
/// </summary>
|
||||
/// <param name="exception">异常对象。</param>
|
||||
/// <param name="message">日志消息模板或文本。</param>
|
||||
/// <param name="showInUi">是否同步显示到前台日志区域。默认不显示。</param>
|
||||
/// <param name="args">日志模板参数。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="exception"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
void LogError(Exception exception, string message, bool showInUi = false, params object?[] args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义 PLC 通信能力。
|
||||
/// </summary>
|
||||
public interface IPlcService
|
||||
{
|
||||
/// <summary>
|
||||
/// 读取 PLC 输入信号快照。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>输入信号快照。</returns>
|
||||
Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 写入 PLC 输出状态与寄存器值。
|
||||
/// </summary>
|
||||
/// <param name="state">待写入的输出状态。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示写入完成的任务。</returns>
|
||||
Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 主动断开并重建 PLC 连接。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示重连完成的任务。</returns>
|
||||
Task ForceReconnectAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义扫码枪服务能力。
|
||||
/// </summary>
|
||||
public interface IScannerService
|
||||
{
|
||||
/// <summary>
|
||||
/// 触发一次扫码。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>扫码结果。</returns>
|
||||
Task<ScanOperationResult> TriggerScanAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 测试扫码枪连接。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>连接正常返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
||||
Task<bool> TestConnectionAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 主动断开并重建扫码枪连接。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示重连完成的任务。</returns>
|
||||
Task ForceReconnectAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义 SFTP 校验能力。
|
||||
/// </summary>
|
||||
public interface ISftpLookupService
|
||||
{
|
||||
/// <summary>
|
||||
/// 按条码检查目标文件是否存在。
|
||||
/// </summary>
|
||||
/// <param name="barcode">条码内容。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>文件校验结果。</returns>
|
||||
Task<SftpCheckOutcome> CheckFileAsync(string barcode, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义安灯接口调用能力。
|
||||
/// </summary>
|
||||
public interface IAndonService
|
||||
{
|
||||
/// <summary>
|
||||
/// 发送安灯报警。
|
||||
/// </summary>
|
||||
/// <param name="request">报警请求对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>报警调用结果。</returns>
|
||||
Task<AndonOperationResult> RaiseAlarmAsync(AndonAlarmRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 发送一次测试报警请求。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>测试调用结果。</returns>
|
||||
Task<AndonOperationResult> TestAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义流程控制手动操作能力。
|
||||
/// </summary>
|
||||
public interface IWorkflowControlService
|
||||
{
|
||||
/// <summary>
|
||||
/// 手动复位流程状态。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示复位完成的任务。</returns>
|
||||
Task ResetAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 手动重连 PLC。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示重连完成的任务。</returns>
|
||||
Task ReconnectPlcAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 手动重连扫码枪。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示重连完成的任务。</returns>
|
||||
Task ReconnectScannerAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 手动测试安灯接口。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>表示测试完成的任务。</returns>
|
||||
Task TestAndonAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义运行态快照与 UI 事件分发能力。
|
||||
/// </summary>
|
||||
public interface IAppStateStore
|
||||
{
|
||||
/// <summary>
|
||||
/// 当运行态快照变化时触发。
|
||||
/// </summary>
|
||||
event EventHandler<RuntimeSnapshot>? SnapshotChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 当新增日志时触发。
|
||||
/// </summary>
|
||||
event EventHandler<UiLogEntry>? LogAdded;
|
||||
|
||||
/// <summary>
|
||||
/// 当新增单板记录时触发。
|
||||
/// </summary>
|
||||
event EventHandler<BoardProcessRecord>? RecordAdded;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前运行态快照副本。
|
||||
/// </summary>
|
||||
/// <returns>当前快照副本。</returns>
|
||||
RuntimeSnapshot GetSnapshot();
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前运行态快照。
|
||||
/// </summary>
|
||||
/// <param name="updateAction">用于修改快照的更新委托。</param>
|
||||
void UpdateSnapshot(Action<RuntimeSnapshot> updateAction);
|
||||
|
||||
/// <summary>
|
||||
/// 追加一条 UI 日志。
|
||||
/// </summary>
|
||||
/// <param name="entry">待追加的日志对象。</param>
|
||||
void AddLog(UiLogEntry entry);
|
||||
|
||||
/// <summary>
|
||||
/// 追加一条单板结果记录。
|
||||
/// </summary>
|
||||
/// <param name="record">待追加的记录对象。</param>
|
||||
void AddRecord(BoardProcessRecord record);
|
||||
}
|
||||
135
src/AxiOmron.PcbCheck/Utils/TextAndPatternHelpers.cs
Normal file
135
src/AxiOmron.PcbCheck/Utils/TextAndPatternHelpers.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// 提供转义字符串处理能力。
|
||||
/// </summary>
|
||||
internal static class StringEscapeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 将配置字符串中的常见转义序列还原为实际字符。
|
||||
/// </summary>
|
||||
/// <param name="value">待还原的字符串。</param>
|
||||
/// <returns>还原后的字符串。</returns>
|
||||
public static string Unescape(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return value
|
||||
.Replace("\\r", "\r", StringComparison.Ordinal)
|
||||
.Replace("\\n", "\n", StringComparison.Ordinal)
|
||||
.Replace("\\t", "\t", StringComparison.Ordinal)
|
||||
.Replace("\\0", "\0", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供扫码字符串清洗能力。
|
||||
/// </summary>
|
||||
internal static class BarcodeCleaner
|
||||
{
|
||||
/// <summary>
|
||||
/// 去除条码中的首尾空白与控制字符。
|
||||
/// </summary>
|
||||
/// <param name="value">原始条码字符串。</param>
|
||||
/// <returns>清洗后的条码。</returns>
|
||||
public static string Clean(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(value.Length);
|
||||
foreach (var character in value.Trim())
|
||||
{
|
||||
if (!char.IsControl(character))
|
||||
{
|
||||
builder.Append(character);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString().Trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供通配符匹配能力。
|
||||
/// </summary>
|
||||
internal static class WildcardMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断给定文本是否匹配通配符模式。
|
||||
/// </summary>
|
||||
/// <param name="text">待匹配文本。</param>
|
||||
/// <param name="pattern">通配符模式。</param>
|
||||
/// <returns>匹配成功返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
||||
public static bool IsMatch(string text, string pattern)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(text);
|
||||
ArgumentNullException.ThrowIfNull(pattern);
|
||||
|
||||
var regexPattern = "^" + Regex.Escape(pattern)
|
||||
.Replace("\\*", ".*", StringComparison.Ordinal)
|
||||
.Replace("\\?", ".", StringComparison.Ordinal) + "$";
|
||||
|
||||
return Regex.IsMatch(text, regexPattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供 Modbus 地址转换能力。
|
||||
/// </summary>
|
||||
internal static class ModbusAddressConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将离散输入地址转换为零基偏移。
|
||||
/// </summary>
|
||||
/// <param name="address">离散输入地址。</param>
|
||||
/// <returns>零基偏移。</returns>
|
||||
public static ushort ToDiscreteInputOffset(int address)
|
||||
{
|
||||
return ConvertToOffset(address, 10001);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将线圈地址转换为零基偏移。
|
||||
/// </summary>
|
||||
/// <param name="address">线圈地址。</param>
|
||||
/// <returns>零基偏移。</returns>
|
||||
public static ushort ToCoilOffset(int address)
|
||||
{
|
||||
return ConvertToOffset(address, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将保持寄存器地址转换为零基偏移。
|
||||
/// </summary>
|
||||
/// <param name="address">保持寄存器地址。</param>
|
||||
/// <returns>零基偏移。</returns>
|
||||
public static ushort ToHoldingRegisterOffset(int address)
|
||||
{
|
||||
return ConvertToOffset(address, 40001);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据基地址执行统一偏移换算。
|
||||
/// </summary>
|
||||
/// <param name="address">原始地址。</param>
|
||||
/// <param name="baseAddress">地址段起始基值。</param>
|
||||
/// <returns>零基偏移。</returns>
|
||||
private static ushort ConvertToOffset(int address, int baseAddress)
|
||||
{
|
||||
if (address < baseAddress)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(address), $"地址 {address} 小于基地址 {baseAddress}。");
|
||||
}
|
||||
|
||||
return checked((ushort)(address - baseAddress));
|
||||
}
|
||||
}
|
||||
505
src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs
Normal file
505
src/AxiOmron.PcbCheck/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,505 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 提供主窗口与首页共享的运行状态、日志和命令绑定能力。
|
||||
/// </summary>
|
||||
public partial class MainWindowViewModel : ObservableObject
|
||||
{
|
||||
private readonly IAppStateStore _stateStore;
|
||||
private readonly IDispatcherService _dispatcherService;
|
||||
private readonly IWorkflowControlService _workflowControlService;
|
||||
private readonly WorkflowOptions _workflowOptions;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化主窗口视图模型。
|
||||
/// </summary>
|
||||
/// <param name="stateStore">运行态存储。</param>
|
||||
/// <param name="dispatcherService">Dispatcher 调度服务。</param>
|
||||
/// <param name="workflowControlService">流程控制服务。</param>
|
||||
/// <param name="config">应用配置。</param>
|
||||
public MainWindowViewModel(
|
||||
IAppStateStore stateStore,
|
||||
IDispatcherService dispatcherService,
|
||||
IWorkflowControlService workflowControlService,
|
||||
AppConfig config)
|
||||
{
|
||||
_stateStore = stateStore ?? throw new ArgumentNullException(nameof(stateStore));
|
||||
_dispatcherService = dispatcherService ?? throw new ArgumentNullException(nameof(dispatcherService));
|
||||
_workflowControlService = workflowControlService ?? throw new ArgumentNullException(nameof(workflowControlService));
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_workflowOptions = config.Workflow;
|
||||
|
||||
Title = "Axi Omron PCB Check";
|
||||
Logs = new ObservableCollection<UiLogEntry>();
|
||||
RecentBoards = new ObservableCollection<BoardProcessRecord>();
|
||||
|
||||
Logs.CollectionChanged += OnLogsCollectionChanged;
|
||||
RecentBoards.CollectionChanged += OnRecentBoardsCollectionChanged;
|
||||
|
||||
_stateStore.SnapshotChanged += OnSnapshotChanged;
|
||||
_stateStore.LogAdded += OnLogAdded;
|
||||
_stateStore.RecordAdded += OnRecordAdded;
|
||||
ApplySnapshot(_stateStore.GetSnapshot());
|
||||
RecalculateLogStatistics();
|
||||
RecalculateProcessStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取运行日志集合。
|
||||
/// </summary>
|
||||
public ObservableCollection<UiLogEntry> Logs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近处理记录集合。
|
||||
/// </summary>
|
||||
public ObservableCollection<BoardProcessRecord> RecentBoards { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近运行日志集合(与 <see cref="Logs"/> 为同一集合,供 UI 绑定语义更清晰)。
|
||||
/// </summary>
|
||||
public ObservableCollection<UiLogEntry> RecentLogs => Logs;
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近处理记录集合(与 <see cref="RecentBoards"/> 为同一集合,供 UI 绑定语义更清晰)。
|
||||
/// </summary>
|
||||
public ObservableCollection<BoardProcessRecord> RecentProcessRecords => RecentBoards;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置主窗口标题。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _title = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 PLC 状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _plcStatus = "未连接";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码枪状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _scannerStatus = "未验证";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _sftpStatus = "未验证";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置安灯状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _andonStatus = "未验证";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前流程状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _workflowStateText = "空闲等待";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前条码。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _currentBarcode = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前结果描述。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _resultDescription = "等待触发";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前故障信息。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _faultMessage = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置扫码次数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _scanTryCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 SFTP 查询次数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _sftpTryCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置结果代码。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private ushort _resultCode;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置报警代码。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private ushort _alarmCode;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最近触发时间。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastTriggeredAt = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最近完成时间。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastCompletedAt = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否忙碌。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _isBusy;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否存在系统故障。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _isFaulted;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否已完成。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _isDone;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否已触发报警。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _isAlarmRaised;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最近更新时间。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastUpdatedAt = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置运行日志区的最后刷新时间文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastLogUpdateTime = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置今日异常日志条数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _todayErrorCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前活跃告警数量。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _activeAlarmCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最近一次异常日志的时间文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastErrorTime = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前是否存在处理记录,供空状态显示切换使用。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _hasProcessRecords;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置今日处理总数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _todayProcessCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置今日 OK 数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _todayOkCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置今日 NG 数。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int _todayNgCount;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置处理记录区的最后刷新时间文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _lastProcessUpdateTime = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 执行手动复位命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
private Task ResetAsync()
|
||||
{
|
||||
return _workflowControlService.ResetAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行 PLC 重连命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
private Task ReconnectPlcAsync()
|
||||
{
|
||||
return _workflowControlService.ReconnectPlcAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行扫码枪重连命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
private Task ReconnectScannerAsync()
|
||||
{
|
||||
return _workflowControlService.ReconnectScannerAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行安灯测试命令。
|
||||
/// </summary>
|
||||
/// <returns>表示命令执行完成的任务。</returns>
|
||||
[RelayCommand]
|
||||
private Task TestAndonAsync()
|
||||
{
|
||||
return _workflowControlService.TestAndonAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查看全部运行日志命令占位。由工具栏触发,暂时只刷新统计与时间戳。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ShowAllLogs()
|
||||
{
|
||||
RecalculateLogStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅显示错误日志命令占位。后续可接入筛选视图,当前刷新统计与时间戳。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ShowErrorLogs()
|
||||
{
|
||||
RecalculateLogStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出运行日志命令占位。后续可接入日志文件导出逻辑。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ExportLogs()
|
||||
{
|
||||
LastLogUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开追踪区命令占位。后续应由导航服务跳转至追踪页面。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void OpenTrackingArea()
|
||||
{
|
||||
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新处理记录命令占位。重新计算统计并更新最后刷新时间。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void RefreshProcessRecords()
|
||||
{
|
||||
RecalculateProcessStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将快照应用到当前视图模型状态。
|
||||
/// </summary>
|
||||
/// <param name="snapshot">运行态快照。</param>
|
||||
private void ApplySnapshot(RuntimeSnapshot snapshot)
|
||||
{
|
||||
PlcStatus = snapshot.PlcStatus;
|
||||
ScannerStatus = snapshot.ScannerStatus;
|
||||
SftpStatus = snapshot.SftpStatus;
|
||||
AndonStatus = snapshot.AndonStatus;
|
||||
WorkflowStateText = snapshot.WorkflowStateText;
|
||||
CurrentBarcode = snapshot.CurrentBarcode;
|
||||
ResultDescription = snapshot.ResultDescription;
|
||||
FaultMessage = snapshot.FaultMessage;
|
||||
ScanTryCount = snapshot.ScanTryCount;
|
||||
SftpTryCount = snapshot.SftpTryCount;
|
||||
ResultCode = snapshot.ResultCode;
|
||||
AlarmCode = snapshot.AlarmCode;
|
||||
LastTriggeredAt = snapshot.LastTriggeredAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||
LastCompletedAt = snapshot.LastCompletedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||
IsBusy = snapshot.IsBusy;
|
||||
IsFaulted = snapshot.SystemFault;
|
||||
IsDone = snapshot.ProcessDone;
|
||||
IsAlarmRaised = snapshot.AlarmRaised;
|
||||
ActiveAlarmCount = snapshot.AlarmRaised ? 1 : 0;
|
||||
LastUpdatedAt = snapshot.LastUpdatedAt.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 <see cref="IsAlarmRaised"/> 发生变化时同步活跃告警计数。
|
||||
/// </summary>
|
||||
/// <param name="value">最新告警状态。</param>
|
||||
partial void OnIsAlarmRaisedChanged(bool value)
|
||||
{
|
||||
ActiveAlarmCount = value ? 1 : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理日志集合变化事件,刷新日志区统计字段。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="e">集合变化参数。</param>
|
||||
private void OnLogsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RecalculateLogStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理处理记录集合变化事件,刷新处理区统计字段。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="e">集合变化参数。</param>
|
||||
private void OnRecentBoardsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RecalculateProcessStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前日志集合重新计算日志区的统计信息与最后刷新时间。
|
||||
/// </summary>
|
||||
private void RecalculateLogStatistics()
|
||||
{
|
||||
DateTime today = DateTime.Today;
|
||||
int errorToday = 0;
|
||||
DateTimeOffset? lastErrorAt = null;
|
||||
|
||||
foreach (UiLogEntry entry in Logs)
|
||||
{
|
||||
bool isError = string.Equals(entry.Level, "Error", StringComparison.OrdinalIgnoreCase);
|
||||
if (!isError)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.Timestamp.LocalDateTime.Date == today)
|
||||
{
|
||||
errorToday++;
|
||||
}
|
||||
|
||||
if (lastErrorAt is null || entry.Timestamp > lastErrorAt.Value)
|
||||
{
|
||||
lastErrorAt = entry.Timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
TodayErrorCount = errorToday;
|
||||
LastErrorTime = lastErrorAt.HasValue
|
||||
? lastErrorAt.Value.ToString("MM-dd HH:mm:ss")
|
||||
: "-";
|
||||
LastLogUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前处理记录集合重新计算处理区的统计信息与最后刷新时间。
|
||||
/// </summary>
|
||||
private void RecalculateProcessStatistics()
|
||||
{
|
||||
DateTime today = DateTime.Today;
|
||||
int totalToday = 0;
|
||||
int okToday = 0;
|
||||
int ngToday = 0;
|
||||
|
||||
foreach (BoardProcessRecord record in RecentBoards)
|
||||
{
|
||||
if (record.CompletedAt.LocalDateTime.Date != today)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
totalToday++;
|
||||
if (record.ResultCode == (ushort)WorkflowResultCode.Passed)
|
||||
{
|
||||
okToday++;
|
||||
}
|
||||
else if (record.ResultCode != (ushort)WorkflowResultCode.Processing
|
||||
&& record.ResultCode != (ushort)WorkflowResultCode.None)
|
||||
{
|
||||
ngToday++;
|
||||
}
|
||||
}
|
||||
|
||||
TodayProcessCount = totalToday;
|
||||
TodayOkCount = okToday;
|
||||
TodayNgCount = ngToday;
|
||||
HasProcessRecords = RecentBoards.Count > 0;
|
||||
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理运行态快照变化事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="snapshot">最新快照。</param>
|
||||
private async void OnSnapshotChanged(object? sender, RuntimeSnapshot snapshot)
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() => ApplySnapshot(snapshot)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理新增日志事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="entry">新增日志。</param>
|
||||
private async void OnLogAdded(object? sender, UiLogEntry entry)
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() =>
|
||||
{
|
||||
Logs.Insert(0, entry);
|
||||
while (Logs.Count > _workflowOptions.MaxUiLogEntries)
|
||||
{
|
||||
Logs.RemoveAt(Logs.Count - 1);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理新增单板记录事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="record">新增单板记录。</param>
|
||||
private async void OnRecordAdded(object? sender, BoardProcessRecord record)
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() =>
|
||||
{
|
||||
RecentBoards.Insert(0, record);
|
||||
while (RecentBoards.Count > _workflowOptions.MaxBoardRecords)
|
||||
{
|
||||
RecentBoards.RemoveAt(RecentBoards.Count - 1);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
80
src/AxiOmron.PcbCheck/ViewModels/SystemSettingViewModel.cs
Normal file
80
src/AxiOmron.PcbCheck/ViewModels/SystemSettingViewModel.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 提供系统配置编辑、保存与重载能力。
|
||||
/// </summary>
|
||||
public partial class SystemSettingViewModel : ObservableObject
|
||||
{
|
||||
private readonly IAppConfigService _appConfigService;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化系统设置视图模型。
|
||||
/// </summary>
|
||||
/// <param name="appConfigService">配置读写服务。</param>
|
||||
public SystemSettingViewModel(IAppConfigService appConfigService)
|
||||
{
|
||||
_appConfigService = appConfigService ?? throw new ArgumentNullException(nameof(appConfigService));
|
||||
EditableConfig = _appConfigService.Load();
|
||||
ConfigPath = _appConfigService.GetConfigPath();
|
||||
StatusMessage = "已加载配置。";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置可编辑配置对象。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private AppConfig _editableConfig = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置状态文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _statusMessage = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置配置文件路径。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _configPath = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前配置。
|
||||
/// </summary>
|
||||
private void SaveConfig()
|
||||
{
|
||||
_appConfigService.Save(EditableConfig);
|
||||
StatusMessage = "配置已保存,重启应用后完全生效。";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新加载配置文件。
|
||||
/// </summary>
|
||||
private void ReloadConfig()
|
||||
{
|
||||
EditableConfig = _appConfigService.Load();
|
||||
StatusMessage = "配置已重新加载。";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存配置命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重载配置命令。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void Reload()
|
||||
{
|
||||
ReloadConfig();
|
||||
}
|
||||
}
|
||||
21
src/AxiOmron.PcbCheck/ViewModels/ViewModelLocator.cs
Normal file
21
src/AxiOmron.PcbCheck/ViewModels/ViewModelLocator.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace AxiOmron.PcbCheck.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 为 XAML 提供视图模型定位能力。
|
||||
/// </summary>
|
||||
public class ViewModelLocator
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取主窗口视图模型。
|
||||
/// </summary>
|
||||
public MainWindowViewModel MainWindowViewModel
|
||||
=> App.Services.GetRequiredService<MainWindowViewModel>();
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统设置视图模型。
|
||||
/// </summary>
|
||||
public SystemSettingViewModel SystemSettingViewModel
|
||||
=> App.Services.GetRequiredService<SystemSettingViewModel>();
|
||||
}
|
||||
1130
src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml
Normal file
1130
src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml
Normal file
File diff suppressed because it is too large
Load Diff
17
src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml.cs
Normal file
17
src/AxiOmron.PcbCheck/Views/Pages/DashboardPage.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Views.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// 表示系统运行总览页。
|
||||
/// </summary>
|
||||
public partial class DashboardPage : Page
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化总览页。
|
||||
/// </summary>
|
||||
public DashboardPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
100
src/AxiOmron.PcbCheck/Views/Pages/SystemSettingsPage.xaml
Normal file
100
src/AxiOmron.PcbCheck/Views/Pages/SystemSettingsPage.xaml
Normal file
@@ -0,0 +1,100 @@
|
||||
<Page x:Class="AxiOmron.PcbCheck.Views.Pages.SystemSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="1400"
|
||||
d:DesignHeight="860"
|
||||
Title="SystemSettingsPage"
|
||||
d:DataContext="{Binding Source={StaticResource DesignTimeLocator}, Path=SystemSettingViewModel}"
|
||||
DataContext="{Binding Source={StaticResource Locator}, Path=SystemSettingViewModel}">
|
||||
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TabControl Grid.Row="0" Style="{StaticResource TabControlInLine}">
|
||||
<TabItem Header="PLC">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="16">
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="PLC 主机" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.Host, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="端口" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.Port, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="从站号" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.UnitId, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="轮询周期(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.PollIntervalMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="连接超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.ConnectTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="心跳周期(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.HeartbeatIntervalMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="放行脉冲(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.ReleasePulseMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="放行应答超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Plc.ReleaseAckTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="扫码枪">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="16">
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="串口号" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.PortName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="波特率" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.BaudRate, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="数据位" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.DataBits, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="校验位" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.Parity, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="停止位" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.StopBits, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.ReadTimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="触发命令" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.TriggerCommand, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="返回结束符" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Scanner.ResponseTerminator, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="SFTP">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="16">
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="主机" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Host, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="端口" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Port, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="用户名" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Username, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="密码" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.Password, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="私钥路径" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.PrivateKeyPath, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="根目录" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.RootPath, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="文件名模板" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.FileNamePattern, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="重试间隔(秒)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.RetryIntervalSeconds, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="最大重试次数" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Sftp.MaxRetryCount, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="安灯 & 流程">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="16">
|
||||
<CheckBox Content="启用安灯接口" IsChecked="{Binding EditableConfig.Andon.Enable}" Margin="0,6,0,0" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="安灯 URL" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.Url, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="请求方法" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.Method, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="超时(ms)" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.TimeoutMs, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="工位编码" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.StationCode, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Style="{StaticResource TextBoxExtend}" hc:InfoElement.Title="工位名称" hc:InfoElement.TitlePlacement="Left" Margin="0,6,0,0" Text="{Binding EditableConfig.Andon.StationName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<CheckBox Content="扫码失败时报警" IsChecked="{Binding EditableConfig.Andon.EnableScanFailAlarm}" Margin="0,6,0,0" />
|
||||
<CheckBox Content="文件未找到时报警" IsChecked="{Binding EditableConfig.Andon.EnableFileNotFoundAlarm}" Margin="0,6,0,0" />
|
||||
<CheckBox Content="要求 PLC Ready" IsChecked="{Binding EditableConfig.Workflow.RequirePlcReady}" Margin="0,6,0,0" />
|
||||
<CheckBox Content="要求 AutoMode & StationEnable" IsChecked="{Binding EditableConfig.Workflow.RequireAutoMode}" Margin="0,6,0,0" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,16,0,0" MinHeight="60">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Vertical" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding StatusMessage}" FontWeight="SemiBold" />
|
||||
<TextBlock Margin="0,4,0,0" Foreground="#A33A00" Text="保存后需重启应用生效。" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource ButtonDefault}" Margin="0,0,8,0" Padding="18,8" MinHeight="36" Content="重新加载" Command="{Binding ReloadCommand}" />
|
||||
<Button Style="{StaticResource ButtonPrimary}" Padding="18,8" MinHeight="36" Content="保存配置" Command="{Binding SaveCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
17
src/AxiOmron.PcbCheck/Views/Pages/SystemSettingsPage.xaml.cs
Normal file
17
src/AxiOmron.PcbCheck/Views/Pages/SystemSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Views.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// 表示系统配置页面。
|
||||
/// </summary>
|
||||
public partial class SystemSettingsPage : Page
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化系统配置页面。
|
||||
/// </summary>
|
||||
public SystemSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
15
src/AxiOmron.PcbCheck/appConfig.Development.json
Normal file
15
src/AxiOmron.PcbCheck/appConfig.Development.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Plc": {
|
||||
"Host": "127.0.0.1"
|
||||
},
|
||||
"Scanner": {
|
||||
"PortName": "COM3"
|
||||
},
|
||||
"Sftp": {
|
||||
"Host": "127.0.0.1",
|
||||
"RootPath": "/tmp/pcb"
|
||||
},
|
||||
"Andon": {
|
||||
"Url": "http://127.0.0.1:5000/api/andon/test"
|
||||
}
|
||||
}
|
||||
84
src/AxiOmron.PcbCheck/appConfig.json
Normal file
84
src/AxiOmron.PcbCheck/appConfig.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"Plc": {
|
||||
"Host": "127.0.0.1",
|
||||
"Port": 502,
|
||||
"UnitId": 1,
|
||||
"PollIntervalMs": 200,
|
||||
"ConnectTimeoutMs": 3000,
|
||||
"HeartbeatIntervalMs": 500,
|
||||
"ReleasePulseMs": 500,
|
||||
"ReleaseAckTimeoutMs": 2000,
|
||||
"Inputs": {
|
||||
"PlcReady": 10001,
|
||||
"PcbArrived": 10002,
|
||||
"PlcReset": 10003,
|
||||
"PlcAckRelease": 10004,
|
||||
"AutoMode": 10005,
|
||||
"StationEnable": 10006
|
||||
},
|
||||
"Outputs": {
|
||||
"PcOnline": 51,
|
||||
"PcBusy": 52,
|
||||
"ScanOk": 53,
|
||||
"ScanNg": 54,
|
||||
"FileFound": 55,
|
||||
"FileNotFound": 56,
|
||||
"AlarmRaised": 57,
|
||||
"ReleasePermit": 58,
|
||||
"ProcessDone": 59,
|
||||
"SystemFault": 60
|
||||
},
|
||||
"Registers": {
|
||||
"ResultCode": 40001,
|
||||
"ScanTryCount": 40002,
|
||||
"SftpTryCount": 40003,
|
||||
"AlarmCode": 40004,
|
||||
"FlowStateCode": 40005
|
||||
}
|
||||
},
|
||||
"Scanner": {
|
||||
"PortName": "COM1",
|
||||
"BaudRate": 9600,
|
||||
"DataBits": 8,
|
||||
"Parity": "None",
|
||||
"StopBits": "One",
|
||||
"ReadTimeoutMs": 3000,
|
||||
"TriggerCommand": "SCAN\\r",
|
||||
"ResponseTerminator": "\\r",
|
||||
"MaxScanAttempts": 3
|
||||
},
|
||||
"Sftp": {
|
||||
"Host": "127.0.0.1",
|
||||
"Port": 22,
|
||||
"Username": "user",
|
||||
"Password": "",
|
||||
"PrivateKeyPath": "",
|
||||
"PrivateKeyPassphrase": "",
|
||||
"RootPath": "/pcb",
|
||||
"FileNamePattern": "${barcode}.txt",
|
||||
"RetryIntervalSeconds": 2,
|
||||
"MaxRetryCount": 3,
|
||||
"ConnectTimeoutMs": 3000
|
||||
},
|
||||
"Andon": {
|
||||
"Enable": true,
|
||||
"Url": "http://127.0.0.1:5000/api/andon",
|
||||
"Method": "POST",
|
||||
"TimeoutMs": 3000,
|
||||
"StationCode": "OMRON-01",
|
||||
"StationName": "PCB目检工位",
|
||||
"EnableScanFailAlarm": true,
|
||||
"EnableFileNotFoundAlarm": false,
|
||||
"Headers": {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"Workflow": {
|
||||
"RequirePlcReady": true,
|
||||
"RequireAutoMode": true,
|
||||
"RequireStationEnable": true,
|
||||
"RequireManualResetAfterFault": true,
|
||||
"MaxUiLogEntries": 200,
|
||||
"MaxBoardRecords": 100
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user