Files

596 lines
27 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}
}
}