将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。

This commit is contained in:
QI Mingxuan
2026-04-16 17:31:13 +08:00
parent 6ec4c3ddaa
commit 2bd6e566c3
581 changed files with 74600 additions and 222 deletions
+595
View File
@@ -0,0 +1,595 @@
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Plc.Abstractions;
using XP.Hardware.Plc.Core;
using XP.Hardware.Plc.Exceptions;
using XP.Hardware.PLC.Configs;
using XP.Hardware.PLC.Helpers;
using XP.Hardware.PLC.Models;
using XP.Hardware.PLC.Services;
namespace XP.Hardware.Plc.Services
{
/// <summary>
/// PLC 服务层,适配 Prism MVVM 架构 | PLC service layer, adapted for Prism MVVM architecture
/// </summary>
public class PlcService : BindableBase, IPlcService, IDisposable
{
private readonly IPlcClient _plcClient;
private readonly ILoggerService _logger;
private PlcConfig _config;
private Timer _connectionMonitorTimer;
private int _reconnectAttempts = 0;
private bool _isConnected;
private string _statusText;
private bool _disposed = false;
// 信号解析器和统一信号字典 | Signal parser and unified signal dictionary
private readonly XmlSignalParser _signalParser;
private Dictionary<string, SignalEntry> _signalDict = new Dictionary<string, SignalEntry>();
// 批量读取缓存和定时任务 | Bulk read cache and timer
private byte[] _bulkReadCache;
private readonly object _cacheLock = new object();
private Timer _bulkReadTimer;
private int _bulkReadFailCount = 0;
private volatile bool _disconnectNotified = false;
// 写入队列和直接写入通道引用,由 PlcService 统一管理生命周期
// Write queue and direct write channel references, lifecycle managed by PlcService
private PlcWriteQueue _writeQueue;
private IPlcClient _directWriteChannel;
/// <summary>
/// PLC 配置,供同程序集内部访问 | PLC configuration, accessible within the same assembly
/// </summary>
internal PlcConfig Config => _config;
/// <summary>
/// PLC 连接状态 | PLC connection status
/// </summary>
public bool IsConnected
{
get => _isConnected;
private set => SetProperty(ref _isConnected, value);
}
/// <summary>
/// 状态文本,用于 UI 显示 | Status text for UI display
/// </summary>
public string StatusText
{
get => _statusText;
private set => SetProperty(ref _statusText, value);
}
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="plcClient">PLC 客户端接口 | PLC client interface</param>
/// <param name="logger">日志服务 | Logger service</param>
/// <param name="signalParser">XML 信号解析器 | XML signal parser</param>
public PlcService(IPlcClient plcClient, ILoggerService logger, XmlSignalParser signalParser)
{
_plcClient = plcClient ?? throw new ArgumentNullException(nameof(plcClient));
// 使用泛型方法自动获取完整类型名:XP.Hardware.Plc.Services.PlcService
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
_signalParser = signalParser ?? throw new ArgumentNullException(nameof(signalParser));
StatusText = "未初始化 | Not initialized";
}
/// <summary>
/// 注册写入队列和直接写入通道,由 PLCModule 在 DI 注册后调用
/// Register write queue and direct write channel, called by PLCModule after DI registration
/// </summary>
/// <param name="writeQueue">写入队列实例 | Write queue instance</param>
/// <param name="directWriteChannel">直接写入通道实例 | Direct write channel instance</param>
public void RegisterWriteComponents(PlcWriteQueue writeQueue, IPlcClient directWriteChannel)
{
_writeQueue = writeQueue;
_directWriteChannel = directWriteChannel;
}
/// <summary>
/// 初始化 PLC 连接 | Initialize PLC connection
/// </summary>
/// <param name="config">PLC 配置 | PLC configuration</param>
/// <returns>连接是否成功 | Whether connection succeeded</returns>
public async Task<bool> InitializeAsync(PlcConfig config)
{
try
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_logger.Info("正在初始化 PLC 连接... | Initializing PLC connection...");
var result = await _plcClient.ConnectAsync(config);
if (result)
{
StatusText = "连接成功 | Connection successful";
_logger.Info("PLC 连接成功 | PLC connection successful");
// 启动连接监控 | Start connection monitoring
StartConnectionMonitoring();
// 启动批量读取定时任务 | Start bulk read timer
_bulkReadTimer = new Timer(BulkReadCallback, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(_config.BulkReadIntervalMs));
_logger.Info("批量读取定时任务已启动,周期 {Interval}ms | Bulk read timer started, interval {Interval}ms", _config.BulkReadIntervalMs);
// 启动写入队列 | Start write queue
if (_writeQueue != null)
{
// 连接写入队列的 PLC 客户端(独立 Socket| Connect write queue's PLC client (independent socket)
try
{
var wqResult = await _writeQueue.PlcClient.ConnectAsync(config);
if (wqResult)
{
_writeQueue.Start();
_logger.Info("写入队列 PLC 通道连接成功并已启动 | Write queue PLC channel connected and started");
}
else
{
_logger.Warn("写入队列 PLC 通道连接失败,队列写入将不可用 | Write queue PLC channel connection failed, queue write will be unavailable");
}
}
catch (Exception wqEx)
{
_logger.Error(wqEx, "写入队列 PLC 通道连接异常: {Message} | Write queue PLC channel connection exception: {Message}", wqEx.Message);
}
}
// 连接直接写入通道(使用相同配置,独立 Socket| Connect direct write channel (same config, independent socket)
if (_directWriteChannel != null)
{
try
{
var dwcResult = await _directWriteChannel.ConnectAsync(config);
if (dwcResult)
{
_logger.Info("直接写入通道连接成功 | Direct write channel connected");
}
else
{
_logger.Warn("直接写入通道连接失败,WriteDirectWithVerify 将不可用 | Direct write channel connection failed, WriteDirectWithVerify will be unavailable");
}
}
catch (Exception dwcEx)
{
_logger.Error(dwcEx, "直接写入通道连接异常: {Message} | Direct write channel connection exception: {Message}", dwcEx.Message);
}
}
}
else
{
StatusText = "连接失败 | Connection failed";
_logger.Warn("PLC 连接失败 | PLC connection failed");
}
return result;
}
catch (Exceptions.PlcException ex)
{
StatusText = $"连接异常: {ex.Message} | Connection exception: {ex.Message}";
_logger.Error(ex, $"PLC 初始化失败: {ex.Message} | PLC initialization failed: {ex.Message}");
MessageBox.Show($"PLC初始化失败 | PLC initialization failed: {ex.Message}",
"错误 | Error", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
catch (Exception ex)
{
StatusText = $"连接异常: {ex.Message} | Connection exception: {ex.Message}";
_logger.Error(ex, $"PLC 初始化异常: {ex.Message} | PLC initialization exception: {ex.Message}");
MessageBox.Show($"PLC初始化失败 | PLC initialization failed: {ex.Message}",
"错误 | Error", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
}
/// <summary>
/// 启动连接状态监控 | Start connection status monitoring
/// </summary>
private void StartConnectionMonitoring()
{
if (_connectionMonitorTimer != null)
{
return;
}
_logger.Info("启动连接状态监控 | Starting connection status monitoring");
// 创建 Timer,首次延迟 1 秒,之后每 3 秒检查一次
// Create Timer, first delay 1 second, then check every 3 seconds
_connectionMonitorTimer = new Timer(
CheckConnectionStatus,
null,
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(3));
}
/// <summary>
/// 停止连接状态监控 | Stop connection status monitoring
/// </summary>
private void StopConnectionMonitoring()
{
if (_connectionMonitorTimer != null)
{
_logger.Info("停止连接状态监控 | Stopping connection status monitoring");
_connectionMonitorTimer.Dispose();
_connectionMonitorTimer = null;
}
}
/// <summary>
/// 检查连接状态 | Check connection status
/// </summary>
/// <param name="state">状态对象 | State object</param>
private void CheckConnectionStatus(object state)
{
try
{
// 先检查 _plcClient.IsConnected 标志 | First check _plcClient.IsConnected flag
var clientReportsConnected = _plcClient.IsConnected;
// 如果客户端报告已连接,但批量读取连续失败,则认为实际已断开
// If client reports connected but bulk read has consecutive failures, treat as disconnected
var effectiveStatus = clientReportsConnected && _bulkReadFailCount < 3;
// 检测到连接状态变化 | Connection status change detected
if (effectiveStatus != IsConnected)
{
IsConnected = effectiveStatus;
if (effectiveStatus)
{
StatusText = "PLC 连接正常 | PLC connection normal";
_logger.Info("PLC 连接状态恢复 | PLC connection status restored");
}
else
{
StatusText = "PLC 连接断开 | PLC connection disconnected";
_logger.Warn("检测到 PLC 连接断开 | PLC connection disconnected detected");
}
}
// 已断开且启用自动重连时,持续尝试重连 | When disconnected and auto-reconnect enabled, keep retrying
if (!IsConnected && _config?.bReConnect == true)
{
_ = TryReconnectAsync();
}
}
catch (Exception ex)
{
_logger.Error(ex, $"检查连接状态时发生异常: {ex.Message} | Exception occurred while checking connection status: {ex.Message}");
}
}
/// <summary>
/// 尝试自动重连 | Try auto-reconnection
/// </summary>
private async Task TryReconnectAsync()
{
_reconnectAttempts++;
_logger.Warn($"检测到连接断开,尝试第 {_reconnectAttempts} 次重连... | Connection disconnected detected, attempting reconnection #{_reconnectAttempts}...");
StatusText = $"正在重连 (第 {_reconnectAttempts} 次)... | Reconnecting (attempt #{_reconnectAttempts})...";
try
{
var result = await _plcClient.ConnectAsync(_config);
if (result)
{
_reconnectAttempts = 0;
_bulkReadFailCount = 0;
_disconnectNotified = false;
IsConnected = true;
StatusText = "重连成功 | Reconnection successful";
_logger.Info("PLC 重连成功 | PLC reconnection successful");
}
else
{
StatusText = $"重连失败 (第 {_reconnectAttempts} 次) | Reconnection failed (attempt #{_reconnectAttempts})";
_logger.Warn($"PLC 重连失败 (第 {_reconnectAttempts} 次) | PLC reconnection failed (attempt #{_reconnectAttempts})");
}
}
catch (Exceptions.PlcException ex)
{
StatusText = $"重连异常: {ex.Message} | Reconnection exception: {ex.Message}";
_logger.Error(ex, $"PLC 重连失败 (第 {_reconnectAttempts} 次): {ex.Message} | PLC reconnection failed (attempt #{_reconnectAttempts}): {ex.Message}");
}
catch (Exception ex)
{
StatusText = $"重连异常: {ex.Message} | Reconnection exception: {ex.Message}";
_logger.Error(ex, $"PLC 重连异常 (第 {_reconnectAttempts} 次): {ex.Message} | PLC reconnection exception (attempt #{_reconnectAttempts}): {ex.Message}");
}
}
/// <summary>
/// 批量读取定时回调,周期性从 PLC 读取数据块并更新缓存 | Bulk read timer callback, periodically reads data block from PLC and updates cache
/// </summary>
/// <param name="state">状态对象 | State object</param>
private async void BulkReadCallback(object state)
{
// 未连接时跳过读取,等待重连成功 | Skip read when disconnected, wait for reconnection
if (!IsConnected) return;
try
{
var data = await _plcClient.ReadBytesAsync(_config.ReadDbBlock, _config.ReadStartAddress, _config.ReadLength);
// 批量读取成功,线程安全更新缓存 | Bulk read succeeded, update cache with thread safety
lock (_cacheLock)
{
_bulkReadCache = data;
}
// 读取成功,重置连续失败计数 | Read succeeded, reset consecutive failure count
_bulkReadFailCount = 0;
}
catch (Exception ex)
{
_bulkReadFailCount++;
// 仅首次和每10次记录日志,避免刷屏 | Log only first and every 10th to avoid spam
if (_bulkReadFailCount == 1 || _bulkReadFailCount % 10 == 0)
{
_logger.Error(ex, "批量读取 PLC 数据失败(第{Count}次): {Message} | Bulk read PLC data failed (#{Count}): {Message}",
_bulkReadFailCount, ex.Message);
}
// 连续失败达到阈值,判定连接已断开 | Consecutive failures reached threshold, mark as disconnected
if (_bulkReadFailCount >= 2 && IsConnected && !_disconnectNotified)
{
_disconnectNotified = true;
_logger.Warn("批量读取连续失败 {Count} 次,判定 PLC 连接已断开 | Bulk read failed {Count} consecutive times, marking PLC as disconnected",
_bulkReadFailCount);
IsConnected = false;
StatusText = "PLC 连接断开(批量读取失败)| PLC disconnected (bulk read failed)";
// 清空缓存,避免返回过期数据 | Clear cache to avoid returning stale data
lock (_cacheLock)
{
_bulkReadCache = null;
}
// 首次断开时弹窗提示(非阻塞,投递到 UI 线程)| Show non-blocking notification on first disconnect
var autoReconnect = _config?.bReConnect == true;
var message = autoReconnect
? "PLC 连接已断开,系统将自动尝试重连。\nPLC connection lost, auto-reconnecting..."
: "PLC 连接已断开,自动重连未启用,请手动检查连接。\nPLC connection lost, auto-reconnect disabled. Please check manually.";
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
{
MessageBox.Show(
Application.Current?.MainWindow,
message,
"PLC",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}));
}
}
}
/// <summary>
/// 通用读取方法 | Generic read method
/// </summary>
/// <typeparam name="T">数据类型 | Data type</typeparam>
/// <param name="address">PLC 地址 | PLC address</param>
/// <returns>读取的值 | Read value</returns>
public async Task<T> ReadValueAsync<T>(string address)
{
try
{
return await _plcClient.ReadAsync<T>(address);
}
catch (Exceptions.PlcException ex)
{
StatusText = $"读取失败: {ex.Message} | Read failed: {ex.Message}";
_logger.Error(ex, $"读取 PLC 数据失败: 地址={address}, 错误={ex.Message} | Failed to read PLC data: address={address}, error={ex.Message}");
throw;
}
}
/// <summary>
/// 按指定长度读取字符串 | Read string with specified length
/// </summary>
/// <param name="address">PLC 地址 | PLC address</param>
/// <param name="length">字符串长度(字节数)| String length in bytes</param>
/// <returns>读取的字符串 | Read string</returns>
public async Task<string> ReadStringAsync(string address, ushort length)
{
try
{
return await _plcClient.ReadStringAsync(address, length);
}
catch (Exceptions.PlcException ex)
{
StatusText = $"读取字符串失败: {ex.Message} | Read string failed: {ex.Message}";
_logger.Error(ex, $"读取字符串失败: 地址={address}, 长度={length}, 错误={ex.Message} | Read string failed");
throw;
}
}
/// <summary>
/// 按指定长度写入字符串 | Write string with specified length
/// </summary>
/// <param name="address">PLC 地址 | PLC address</param>
/// <param name="value">字符串值 | String value</param>
/// <param name="length">字符串长度(字节数)| String length in bytes</param>
/// <returns>是否成功 | Whether succeeded</returns>
public async Task<bool> WriteStringAsync(string address, string value, ushort length)
{
try
{
var result = await _plcClient.WriteStringAsync(address, value, length);
if (!result)
{
StatusText = $"写入字符串失败: 地址={address} | Write string failed";
_logger.Warn($"写入字符串失败: 地址={address}, 值={value} | Write string failed");
}
return result;
}
catch (Exceptions.PlcException ex)
{
StatusText = $"写入字符串异常: {ex.Message} | Write string exception";
_logger.Error(ex, $"写入字符串异常: 地址={address}, 错误={ex.Message} | Write string exception");
return false;
}
}
/// <summary>
/// 通用写入方法 | Generic write method
/// </summary>
/// <typeparam name="T">数据类型 | Data type</typeparam>
/// <param name="address">PLC 地址 | PLC address</param>
/// <param name="value">要写入的值 | Value to write</param>
/// <returns>写入是否成功 | Whether write succeeded</returns>
public async Task<bool> WriteValueAsync<T>(string address, T value)
{
try
{
var result = await _plcClient.WriteAsync(address, value);
if (!result)
{
StatusText = $"写入失败: 地址={address} | Write failed: address={address}";
_logger.Warn($"写入 PLC 数据失败: 地址={address}, 值={value} | Failed to write PLC data: address={address}, value={value}");
}
return result;
}
catch (Exceptions.PlcException ex)
{
StatusText = $"写入异常: {ex.Message} | Write exception: {ex.Message}";
_logger.Error(ex, $"写入 PLC 数据异常: 地址={address}, 值={value}, 错误={ex.Message} | Exception writing PLC data: address={address}, value={value}, error={ex.Message}");
return false;
}
}
/// <summary>
/// 加载信号定义文件,建立读取和写入信号字典索引 | Load signal definitions and build dictionary indexes
/// </summary>
/// <param name="xmlFilePath">XML 信号定义文件路径 | XML signal definition file path</param>
public void LoadSignalDefinitions(string xmlFilePath)
{
_logger.Info($"正在加载信号定义文件: {xmlFilePath} | Loading signal definitions: {xmlFilePath}");
// 解析所有 Group | Parse all groups
var groups = _signalParser.LoadFromFile(xmlFilePath);
// 建立统一信号字典索引,检查信号名称全局唯一性 | Build unified signal dictionary, check global name uniqueness
var newDict = new Dictionary<string, SignalEntry>();
foreach (var group in groups)
{
foreach (var signal in group.Signals)
{
if (newDict.TryGetValue(signal.Name, out var existing))
{
// 信号名称重复,抛出异常 | Duplicate signal name, throw exception
throw new PlcException(
$"信号名称重复: '{signal.Name}',分别存在于 Group '{existing.GroupId}' 和 Group '{group.GroupId}' | " +
$"Duplicate signal name: '{signal.Name}', found in Group '{existing.GroupId}' and Group '{group.GroupId}'");
}
newDict[signal.Name] = signal;
}
}
_signalDict = newDict;
_logger.Info($"信号定义加载完成: 共 {_signalDict.Count} 个信号,来自 {groups.Count} 个 Group | " +
$"Signal definitions loaded: {_signalDict.Count} signals from {groups.Count} groups");
}
/// <summary>
/// 按名称查找信号定义,O(1) 时间复杂度 | Find signal definition by name, O(1) lookup
/// </summary>
/// <param name="signalName">信号逻辑名称 | Signal logical name</param>
/// <returns>信号条目 | Signal entry</returns>
/// <exception cref="PlcException">信号名称未找到时抛出 | Thrown when signal name not found</exception>
public SignalEntry FindSignal(string signalName)
{
if (_signalDict.TryGetValue(signalName, out var entry))
{
return entry;
}
throw new PlcException($"信号未找到: {signalName} | Signal not found: {signalName}");
}
/// <summary>
/// 获取当前批量读取缓存的 PlcDataBlock 快照 | Get PlcDataBlock snapshot of current bulk read cache
/// 在 lock 内复制 byte[] 并创建新实例,确保快照不可变 | Copy byte[] inside lock and create new instance to ensure snapshot immutability
/// </summary>
/// <returns>缓存快照,缓存为空时返回 null | Cache snapshot, returns null when cache is empty</returns>
public PlcDataBlock GetCacheSnapshot()
{
lock (_cacheLock)
{
if (_bulkReadCache == null)
{
return null;
}
// 复制 byte[] 确保快照独立于缓存 | Copy byte[] to ensure snapshot is independent from cache
var copy = new byte[_bulkReadCache.Length];
Array.Copy(_bulkReadCache, copy, _bulkReadCache.Length);
return new PlcDataBlock(copy, _config.ReadStartAddress);
}
}
/// <summary>
/// 释放资源 | Dispose resources
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
_logger.Info("正在释放 PlcService 资源... | Disposing PlcService resources...");
// 停止批量读取定时任务并清空缓存 | Stop bulk read timer and clear cache
if (_bulkReadTimer != null)
{
_bulkReadTimer.Dispose();
_bulkReadTimer = null;
}
lock (_cacheLock)
{
_bulkReadCache = null;
}
// 停止写入队列 | Stop write queue
if (_writeQueue != null)
{
_writeQueue.Dispose();
}
// 断开并释放直接写入通道 | Disconnect and dispose direct write channel
if (_directWriteChannel is IDisposable dwcDisposable)
{
_directWriteChannel.Disconnect();
dwcDisposable.Dispose();
}
StopConnectionMonitoring();
if (_plcClient is IDisposable disposable)
{
disposable.Dispose();
}
_disposed = true;
_logger.Info("PlcService 资源已释放 | PlcService resources disposed");
}
}
}