将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.Plc.Abstractions;
|
||||
|
||||
namespace XP.Hardware.PLC.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 写入任务完成事件参数 | Write task completed event args
|
||||
/// </summary>
|
||||
public class PlcWriteCompletedEventArgs : EventArgs
|
||||
{
|
||||
public string Address { get; }
|
||||
public object Value { get; }
|
||||
public bool Success { get; }
|
||||
public Exception Error { get; }
|
||||
public DateTime EnqueueTime { get; }
|
||||
public DateTime CompleteTime { get; }
|
||||
|
||||
public PlcWriteCompletedEventArgs(string address, object value, bool success,
|
||||
Exception error, DateTime enqueueTime, DateTime completeTime)
|
||||
{
|
||||
Address = address;
|
||||
Value = value;
|
||||
Success = success;
|
||||
Error = error;
|
||||
EnqueueTime = enqueueTime;
|
||||
CompleteTime = completeTime;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入任务项 | Write task item
|
||||
/// </summary>
|
||||
internal class WriteTaskItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 写入执行委托 | Write execution delegate
|
||||
/// </summary>
|
||||
public Func<IPlcClient, Task<bool>> ExecuteFunc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// PLC 地址(用于日志和事件)| PLC address (for logging and events)
|
||||
/// </summary>
|
||||
public string Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 写入值(用于日志和事件)| Write value (for logging and events)
|
||||
/// </summary>
|
||||
public object Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入队时间 | Enqueue time
|
||||
/// </summary>
|
||||
public DateTime EnqueueTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结果回调(用于 EnqueueAsync 等待模式)| Result callback (for EnqueueAsync await mode)
|
||||
/// </summary>
|
||||
public TaskCompletionSource<bool> Completion { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PLC 队列写入服务实现 | PLC queue write service implementation
|
||||
/// 后台单线程消费队列,顺序写入 PLC,避免并发拥堵
|
||||
/// </summary>
|
||||
public class PlcWriteQueue : IDisposable
|
||||
{
|
||||
private readonly IPlcClient _plcClient;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly BlockingCollection<WriteTaskItem> _queue;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private Thread _workerThread;
|
||||
private bool _disposed;
|
||||
private volatile bool _isRunning;
|
||||
|
||||
/// <summary>
|
||||
/// 写入完成事件 | Write completed event
|
||||
/// </summary>
|
||||
public event EventHandler<PlcWriteCompletedEventArgs> WriteCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 当前队列中待处理的任务数 | Pending task count in queue
|
||||
/// </summary>
|
||||
public int PendingCount => _queue.Count;
|
||||
|
||||
/// <summary>
|
||||
/// 队列是否正在运行 | Whether queue is running
|
||||
/// </summary>
|
||||
public bool IsRunning => _isRunning;
|
||||
|
||||
/// <summary>
|
||||
/// 获取内部 PLC 客户端引用,供 PlcService 管理连接生命周期
|
||||
/// Get internal PLC client reference for PlcService to manage connection lifecycle
|
||||
/// </summary>
|
||||
internal IPlcClient PlcClient => _plcClient;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="plcClient">PLC 客户端接口 | PLC client interface</param>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
/// <param name="maxQueueSize">最大队列容量,默认 1000 | Max queue size, default 1000</param>
|
||||
public PlcWriteQueue(IPlcClient plcClient, ILoggerService logger, int maxQueueSize = 1000)
|
||||
{
|
||||
_plcClient = plcClient ?? throw new ArgumentNullException(nameof(plcClient));
|
||||
_logger = logger?.ForModule<PlcWriteQueue>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_queue = new BlockingCollection<WriteTaskItem>(maxQueueSize);
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动队列处理 | Start queue processing
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
if (_isRunning)
|
||||
{
|
||||
_logger.Warn("写入队列已在运行中 | Write queue is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
_workerThread = new Thread(ProcessQueue)
|
||||
{
|
||||
Name = "PlcWriteQueue-Worker",
|
||||
IsBackground = true
|
||||
};
|
||||
_workerThread.Start();
|
||||
_logger.Info("PLC 写入队列已启动 | PLC write queue started");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止队列处理(等待当前任务完成)| Stop queue processing (wait for current task)
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (!_isRunning) return;
|
||||
|
||||
_logger.Info("正在停止 PLC 写入队列... | Stopping PLC write queue...");
|
||||
_isRunning = false;
|
||||
_queue.CompleteAdding();
|
||||
|
||||
// 等待工作线程结束,最多 5 秒 | Wait for worker thread to finish, max 5 seconds
|
||||
_workerThread?.Join(TimeSpan.FromSeconds(5));
|
||||
_logger.Info("PLC 写入队列已停止 | PLC write queue stopped");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空队列中所有待处理任务 | Clear all pending tasks in queue
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
int cleared = 0;
|
||||
while (_queue.TryTake(out var item))
|
||||
{
|
||||
// 取消等待中的异步任务 | Cancel waiting async tasks
|
||||
item.Completion?.TrySetCanceled();
|
||||
cleared++;
|
||||
}
|
||||
if (cleared > 0)
|
||||
_logger.Info("已清空 {Count} 个待处理写入任务 | Cleared {Count} pending write tasks", cleared);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入队写入任务(即发即忘)| Enqueue write task (fire-and-forget)
|
||||
/// </summary>
|
||||
public void Enqueue<T>(string address, T value)
|
||||
{
|
||||
var item = new WriteTaskItem
|
||||
{
|
||||
Address = address,
|
||||
Value = value,
|
||||
EnqueueTime = DateTime.Now,
|
||||
ExecuteFunc = async (client) => await client.WriteAsync(address, value)
|
||||
};
|
||||
EnqueueItem(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入队写入任务并等待结果 | Enqueue write task and await result
|
||||
/// </summary>
|
||||
public Task<bool> EnqueueAsync<T>(string address, T value)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var item = new WriteTaskItem
|
||||
{
|
||||
Address = address,
|
||||
Value = value,
|
||||
EnqueueTime = DateTime.Now,
|
||||
Completion = tcs,
|
||||
ExecuteFunc = async (client) => await client.WriteAsync(address, value)
|
||||
};
|
||||
EnqueueItem(item);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入队字符串写入任务(即发即忘)| Enqueue string write task (fire-and-forget)
|
||||
/// </summary>
|
||||
public void EnqueueString(string address, string value, ushort length)
|
||||
{
|
||||
var item = new WriteTaskItem
|
||||
{
|
||||
Address = address,
|
||||
Value = value,
|
||||
EnqueueTime = DateTime.Now,
|
||||
ExecuteFunc = async (client) => await client.WriteStringAsync(address, value, length)
|
||||
};
|
||||
EnqueueItem(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入队字符串写入任务并等待结果 | Enqueue string write task and await result
|
||||
/// </summary>
|
||||
public Task<bool> EnqueueStringAsync(string address, string value, ushort length)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var item = new WriteTaskItem
|
||||
{
|
||||
Address = address,
|
||||
Value = value,
|
||||
EnqueueTime = DateTime.Now,
|
||||
Completion = tcs,
|
||||
ExecuteFunc = async (client) => await client.WriteStringAsync(address, value, length)
|
||||
};
|
||||
EnqueueItem(item);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将任务项加入队列 | Add task item to queue
|
||||
/// </summary>
|
||||
private void EnqueueItem(WriteTaskItem item)
|
||||
{
|
||||
if (_disposed || _queue.IsAddingCompleted)
|
||||
{
|
||||
_logger.Warn("写入队列已停止,无法入队: 地址={Address} | Write queue stopped, cannot enqueue: address={Address}", item.Address);
|
||||
item.Completion?.TrySetResult(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_queue.TryAdd(item))
|
||||
{
|
||||
_logger.Warn("写入队列已满,丢弃任务: 地址={Address} | Write queue full, task dropped: address={Address}", item.Address);
|
||||
item.Completion?.TrySetResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台队列处理循环 | Background queue processing loop
|
||||
/// </summary>
|
||||
private void ProcessQueue()
|
||||
{
|
||||
_logger.Debug("写入队列工作线程已启动 | Write queue worker thread started");
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var item in _queue.GetConsumingEnumerable(_cts.Token))
|
||||
{
|
||||
ProcessItem(item);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Debug("写入队列工作线程被取消 | Write queue worker thread cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "写入队列工作线程异常退出: {Message} | Write queue worker thread exception: {Message}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isRunning = false;
|
||||
_logger.Debug("写入队列工作线程已退出 | Write queue worker thread exited");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理单个写入任务 | Process single write task
|
||||
/// </summary>
|
||||
private void ProcessItem(WriteTaskItem item)
|
||||
{
|
||||
bool success = false;
|
||||
Exception error = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 同步等待异步写入完成 | Synchronously wait for async write to complete
|
||||
success = item.ExecuteFunc(_plcClient).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex;
|
||||
_logger.Error(ex, "队列写入失败: 地址={Address}, 错误={Message} | Queue write failed: address={Address}, error={Message}",
|
||||
item.Address, ex.Message);
|
||||
}
|
||||
|
||||
var completeTime = DateTime.Now;
|
||||
|
||||
// 通知等待的异步调用者 | Notify waiting async callers
|
||||
if (item.Completion != null)
|
||||
{
|
||||
if (error != null)
|
||||
item.Completion.TrySetException(error);
|
||||
else
|
||||
item.Completion.TrySetResult(success);
|
||||
}
|
||||
|
||||
// 触发完成事件 | Fire completed event
|
||||
try
|
||||
{
|
||||
WriteCompleted?.Invoke(this, new PlcWriteCompletedEventArgs(
|
||||
item.Address, item.Value, success, error, item.EnqueueTime, completeTime));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "写入完成事件处理异常: {Message} | Write completed event handler exception: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源 | Dispose resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
_logger.Info("正在释放 PlcWriteQueue 资源... | Disposing PlcWriteQueue resources...");
|
||||
|
||||
try { _cts.Cancel(); } catch { /* 静默处理 | Silent handling */ }
|
||||
try { Stop(); } catch { /* 静默处理 | Silent handling */ }
|
||||
try { Clear(); } catch { /* 静默处理 | Silent handling */ }
|
||||
try { _queue.Dispose(); } catch { /* 静默处理 | Silent handling */ }
|
||||
try { _cts.Dispose(); } catch { /* 静默处理 | Silent handling */ }
|
||||
|
||||
_logger.Info("PlcWriteQueue 资源已释放 | PlcWriteQueue resources disposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.Plc.Abstractions;
|
||||
using XP.Hardware.Plc.Exceptions;
|
||||
using XP.Hardware.Plc.Services;
|
||||
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 信号数据交互服务实现 | PLC signal data interaction service implementation
|
||||
/// 提供基于信号逻辑名称的缓存读取、队列写入和直接写入+回读校验操作
|
||||
/// Provides cache read, queue write, and direct write with read-back verification based on signal logical names
|
||||
/// </summary>
|
||||
public class SignalDataService : ISignalDataService
|
||||
{
|
||||
private readonly PlcService _plcService;
|
||||
private readonly PlcWriteQueue _writeQueue;
|
||||
private readonly IPlcClient _directWriteChannel;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 直接写入通道引用,供 PlcService 管理连接生命周期
|
||||
/// Direct write channel reference, for PlcService to manage connection lifecycle
|
||||
/// </summary>
|
||||
public IPlcClient DirectWriteChannel => _directWriteChannel;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="plcService">PLC 服务实例,提供缓存快照和信号查找 | PLC service instance for cache snapshot and signal lookup</param>
|
||||
/// <param name="writeQueue">PLC 写入队列,用于队列写入操作 | PLC write queue for enqueue write operations</param>
|
||||
/// <param name="directWriteChannel">直接写入通道,独立的 IPlcClient 实例用于高优先级写入 | Direct write channel, independent IPlcClient instance for high-priority writes</param>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
public SignalDataService(
|
||||
PlcService plcService,
|
||||
PlcWriteQueue writeQueue,
|
||||
IPlcClient directWriteChannel,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
|
||||
_writeQueue = writeQueue ?? throw new ArgumentNullException(nameof(writeQueue));
|
||||
_directWriteChannel = directWriteChannel ?? throw new ArgumentNullException(nameof(directWriteChannel));
|
||||
_logger = logger?.ForModule<SignalDataService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 ReadDbBlock 配置字符串中提取 DB 块号 | Extract DB block number from ReadDbBlock config string
|
||||
/// 例如 "DB1" → 1,"DB31" → 31 | e.g. "DB1" → 1, "DB31" → 31
|
||||
/// </summary>
|
||||
/// <param name="readDbBlock">ReadDbBlock 配置值 | ReadDbBlock config value</param>
|
||||
/// <returns>DB 块号 | DB block number</returns>
|
||||
private int ExtractDbNumber(string readDbBlock)
|
||||
{
|
||||
if (string.IsNullOrEmpty(readDbBlock))
|
||||
return -1;
|
||||
|
||||
// 移除 "DB" 前缀,解析数字部分 | Remove "DB" prefix, parse numeric part
|
||||
var numStr = readDbBlock.StartsWith("DB", StringComparison.OrdinalIgnoreCase)
|
||||
? readDbBlock.Substring(2)
|
||||
: readDbBlock;
|
||||
|
||||
return int.TryParse(numStr, out var num) ? num : -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化单点读取地址 | Format single-point read address
|
||||
/// bool 类型追加位索引 | Bool type appends bit index
|
||||
/// </summary>
|
||||
/// <param name="signal">信号定义条目 | Signal definition entry</param>
|
||||
/// <returns>格式化后的 PLC 地址字符串 | Formatted PLC address string</returns>
|
||||
private string FormatReadAddress(SignalEntry signal)
|
||||
{
|
||||
return signal.Type.ToLowerInvariant() switch
|
||||
{
|
||||
"bool" => $"DB{signal.DBNumber}.{signal.StartAddr}.{signal.IndexOrLength}",
|
||||
_ => $"DB{signal.DBNumber}.{signal.StartAddr}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过单点读取获取信号值 | Get signal value via single-point read
|
||||
/// 根据信号类型调用对应的 ReadAsync 方法 | Call corresponding ReadAsync method based on signal type
|
||||
/// </summary>
|
||||
/// <param name="signal">信号定义条目 | Signal definition entry</param>
|
||||
/// <param name="address">PLC 地址 | PLC address</param>
|
||||
/// <returns>读取的信号值 | Read signal value</returns>
|
||||
private object ReadSinglePoint(SignalEntry signal, string address)
|
||||
{
|
||||
// 使用 Task.Run 在线程池执行异步读取,避免 UI 线程 SynchronizationContext 死锁
|
||||
// Use Task.Run to execute async read on thread pool, avoiding UI thread SynchronizationContext deadlock
|
||||
return signal.Type.ToLowerInvariant() switch
|
||||
{
|
||||
"bool" => Task.Run(() => _plcService.ReadValueAsync<bool>(address)).GetAwaiter().GetResult(),
|
||||
"byte" => Task.Run(() => _plcService.ReadValueAsync<byte>(address)).GetAwaiter().GetResult(),
|
||||
"short" => Task.Run(() => _plcService.ReadValueAsync<short>(address)).GetAwaiter().GetResult(),
|
||||
"int" => Task.Run(() => _plcService.ReadValueAsync<int>(address)).GetAwaiter().GetResult(),
|
||||
"single" => Task.Run(() => _plcService.ReadValueAsync<float>(address)).GetAwaiter().GetResult(),
|
||||
"double" => Task.Run(() => _plcService.ReadValueAsync<double>(address)).GetAwaiter().GetResult(),
|
||||
"string" => Task.Run(() => _plcService.ReadStringAsync(address,
|
||||
int.TryParse(signal.IndexOrLength, out var len) ? (ushort)len : (ushort)1)).GetAwaiter().GetResult(),
|
||||
_ => throw new PlcException(
|
||||
$"不支持的信号类型: {signal.Type}, 信号: {signal.Name} | " +
|
||||
$"Unsupported signal type: {signal.Type}, signal: {signal.Name}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object GetValueByName(string signalName)
|
||||
{
|
||||
// 查找信号定义 | Look up signal definition
|
||||
var signal = _plcService.FindSignal(signalName);
|
||||
|
||||
// 提取批量读取参数 | Extract bulk read parameters
|
||||
var bulkDbNumber = ExtractDbNumber(_plcService.Config.ReadDbBlock);
|
||||
var bulkStart = _plcService.Config.ReadStartAddress;
|
||||
var bulkEnd = bulkStart + _plcService.Config.ReadLength - 1;
|
||||
|
||||
// 判断是否走批量缓存:DB 号匹配 且 信号地址在缓存范围内
|
||||
// Route to bulk cache: DB number matches AND signal address within cache range
|
||||
var useBulkCache = signal.DBNumber == bulkDbNumber
|
||||
&& signal.StartAddr >= bulkStart
|
||||
&& signal.StartAddr <= bulkEnd;
|
||||
|
||||
object result;
|
||||
|
||||
if (useBulkCache)
|
||||
{
|
||||
// 匹配批量读取范围,走缓存解析逻辑 | Within bulk read range, use cache parsing logic
|
||||
var dataBlock = _plcService.GetCacheSnapshot();
|
||||
if (dataBlock == null)
|
||||
{
|
||||
throw new PlcException("数据缓存未就绪 | Data cache not ready");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = ParseSignalValue(signal, dataBlock);
|
||||
}
|
||||
catch (PlcException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PlcException(
|
||||
$"信号解析失败: {signal.Name}, 原因: {ex.Message} | Signal parsing failed: {signal.Name}, reason: {ex.Message}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 不在批量读取范围内,走单点读取逻辑 | Outside bulk read range, use single-point read
|
||||
var address = FormatReadAddress(signal);
|
||||
try
|
||||
{
|
||||
result = ReadSinglePoint(signal, address);
|
||||
}
|
||||
catch (PlcException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PlcException(
|
||||
$"单点读取失败: 信号={signal.Name}, DB块={signal.DBNumber}, 原因={ex.Message} | " +
|
||||
$"Single-point read failed: signal={signal.Name}, DB={signal.DBNumber}, reason={ex.Message}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 记录调试日志 | Log debug info
|
||||
_logger.Debug("读取信号: {SignalName}, DB块={DBNumber}, 结果: {Result} | Read signal: {SignalName}, DB={DBNumber}, result: {Result}",
|
||||
signalName, signal.DBNumber, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T GetValueByName<T>(string signalName)
|
||||
{
|
||||
// 调用非泛型方法获取解析结果 | Call non-generic method to get parsed result
|
||||
var result = GetValueByName(signalName);
|
||||
|
||||
// 将结果转换为目标类型 T | Convert result to target type T
|
||||
try
|
||||
{
|
||||
return (T)Convert.ChangeType(result, typeof(T));
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidCastException || ex is FormatException || ex is OverflowException)
|
||||
{
|
||||
throw new PlcException(
|
||||
$"类型转换失败: 信号 {signalName}, 期望类型: {typeof(T).Name}, 实际类型: {result?.GetType().Name ?? "null"} | " +
|
||||
$"Type conversion failed: signal {signalName}, expected: {typeof(T).Name}, actual: {result?.GetType().Name ?? "null"}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据信号定义从数据块中解析信号值 | Parse signal value from data block based on signal definition
|
||||
/// </summary>
|
||||
/// <param name="signal">信号定义条目 | Signal definition entry</param>
|
||||
/// <param name="dataBlock">PLC 数据块快照 | PLC data block snapshot</param>
|
||||
/// <returns>解析后的信号值 | Parsed signal value</returns>
|
||||
private object ParseSignalValue(SignalEntry signal, PlcDataBlock dataBlock)
|
||||
{
|
||||
return signal.Type.ToLowerInvariant() switch
|
||||
{
|
||||
"bool" => dataBlock.GetBool(signal.StartAddr, int.TryParse(signal.IndexOrLength, out var bitIdx) ? bitIdx : 0),
|
||||
"byte" => dataBlock.GetByte(signal.StartAddr),
|
||||
"short" => dataBlock.GetInt16(signal.StartAddr),
|
||||
"int" => dataBlock.GetInt32(signal.StartAddr),
|
||||
"single" => dataBlock.GetFloat(signal.StartAddr),
|
||||
"double" => dataBlock.GetDouble(signal.StartAddr),
|
||||
"string" => dataBlock.GetString(signal.StartAddr, int.TryParse(signal.IndexOrLength, out var len) ? len : 1),
|
||||
_ => throw new PlcException(
|
||||
$"不支持的信号类型: {signal.Type}, 信号: {signal.Name} | " +
|
||||
$"Unsupported signal type: {signal.Type}, signal: {signal.Name}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据信号类型获取对应的 CLR 类型 | Get CLR type based on signal type
|
||||
/// </summary>
|
||||
/// <param name="signalType">信号类型字符串 | Signal type string</param>
|
||||
/// <returns>对应的 CLR 类型 | Corresponding CLR type</returns>
|
||||
private Type GetClrType(string signalType)
|
||||
{
|
||||
return signalType.ToLowerInvariant() switch
|
||||
{
|
||||
"bool" => typeof(bool),
|
||||
"byte" => typeof(byte),
|
||||
"short" => typeof(short),
|
||||
"int" => typeof(int),
|
||||
"single" => typeof(float),
|
||||
"double" => typeof(double),
|
||||
"string" => typeof(string),
|
||||
_ => throw new PlcException($"不支持的信号类型: {signalType} | Unsupported signal type: {signalType}")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据信号类型转换写入值 | Convert write value based on signal type
|
||||
/// 使用 System.Convert 执行安全类型转换 | Uses System.Convert for safe type conversion
|
||||
/// </summary>
|
||||
/// <param name="signalName">信号名称,用于异常消息 | Signal name for exception messages</param>
|
||||
/// <param name="signal">信号定义条目 | Signal definition entry</param>
|
||||
/// <param name="value">待转换的值 | Value to convert</param>
|
||||
/// <returns>转换后的值 | Converted value</returns>
|
||||
private object ConvertValue(string signalName, SignalEntry signal, object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var targetType = GetClrType(signal.Type);
|
||||
return Convert.ChangeType(value, targetType);
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidCastException || ex is OverflowException || ex is FormatException)
|
||||
{
|
||||
throw new PlcException(
|
||||
$"类型转换失败: 信号 {signalName}, 期望类型: {signal.Type}, 实际类型: {value?.GetType().Name ?? "null"} | " +
|
||||
$"Type conversion failed: signal {signalName}, expected: {signal.Type}, actual: {value?.GetType().Name ?? "null"}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据信号定义格式化 PLC 写入地址 | Format PLC write address based on signal definition
|
||||
/// 使用信号所属 DBNumber 拼接地址 | Uses signal's DBNumber to construct address
|
||||
/// bool 类型包含位索引,其他类型仅包含起始地址 | Bool type includes bit index, others only include start address
|
||||
/// </summary>
|
||||
/// <param name="signal">信号定义条目 | Signal definition entry</param>
|
||||
/// <returns>格式化后的 PLC 地址字符串 | Formatted PLC address string</returns>
|
||||
private string FormatWriteAddress(SignalEntry signal)
|
||||
{
|
||||
return signal.Type.ToLowerInvariant() switch
|
||||
{
|
||||
"bool" => $"DB{signal.DBNumber}.{signal.StartAddr}.{signal.IndexOrLength}",
|
||||
_ => $"DB{signal.DBNumber}.{signal.StartAddr}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool EnqueueWrite(string signalName, object value)
|
||||
{
|
||||
// 查找信号定义 | Look up signal definition
|
||||
var signal = _plcService.FindSignal(signalName);
|
||||
|
||||
// 类型转换校验 | Type conversion validation
|
||||
var convertedValue = ConvertValue(signalName, signal, value);
|
||||
|
||||
// 格式化写入地址 | Format write address
|
||||
var address = FormatWriteAddress(signal);
|
||||
|
||||
// 检查队列状态 | Check queue status
|
||||
if (!_writeQueue.IsRunning)
|
||||
{
|
||||
_logger.Warn("写入队列未运行,无法入队: 信号={SignalName} | Write queue not running, cannot enqueue: signal={SignalName}", signalName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据信号类型调用对应的入队方法 | Call corresponding enqueue method based on signal type
|
||||
var signalType = signal.Type.ToLowerInvariant();
|
||||
if (signalType == "string")
|
||||
{
|
||||
var strValue = convertedValue as string ?? convertedValue.ToString();
|
||||
var length = int.TryParse(signal.IndexOrLength, out var len) ? (ushort)len : (ushort)1;
|
||||
_writeQueue.EnqueueString(address, strValue, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 根据具体类型调用泛型入队方法 | Call generic enqueue method based on specific type
|
||||
switch (signalType)
|
||||
{
|
||||
case "bool":
|
||||
_writeQueue.Enqueue(address, (bool)convertedValue);
|
||||
break;
|
||||
case "byte":
|
||||
_writeQueue.Enqueue(address, (byte)convertedValue);
|
||||
break;
|
||||
case "short":
|
||||
_writeQueue.Enqueue(address, (short)convertedValue);
|
||||
break;
|
||||
case "int":
|
||||
_writeQueue.Enqueue(address, (int)convertedValue);
|
||||
break;
|
||||
case "single":
|
||||
_writeQueue.Enqueue(address, (float)convertedValue);
|
||||
break;
|
||||
case "double":
|
||||
_writeQueue.Enqueue(address, (double)convertedValue);
|
||||
break;
|
||||
default:
|
||||
throw new PlcException($"不支持的信号类型: {signal.Type} | Unsupported signal type: {signal.Type}");
|
||||
}
|
||||
}
|
||||
|
||||
// 记录调试日志 | Log debug info
|
||||
_logger.Debug("队列写入: 信号={SignalName}, 值={Value}, 地址={Address} | Queue write: signal={SignalName}, value={Value}, address={Address}",
|
||||
signalName, convertedValue, address);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> WriteDirectWithVerify(string signalName, object value)
|
||||
{
|
||||
// 查找信号定义 | Look up signal definition
|
||||
var signal = _plcService.FindSignal(signalName);
|
||||
|
||||
// 类型转换校验 | Type conversion validation
|
||||
var convertedValue = ConvertValue(signalName, signal, value);
|
||||
|
||||
// 格式化写入地址 | Format write address
|
||||
var address = FormatWriteAddress(signal);
|
||||
|
||||
// 检查 Direct_Write_Channel 连接状态 | Check Direct_Write_Channel connection status
|
||||
if (!_directWriteChannel.IsConnected)
|
||||
{
|
||||
_logger.Error(null, "直接写入通道未连接: 信号={SignalName} | Direct write channel not connected: signal={SignalName}", signalName);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var signalType = signal.Type.ToLowerInvariant();
|
||||
bool writeSuccess;
|
||||
object readBackValue;
|
||||
|
||||
// 根据信号类型执行写入和回读操作 | Execute write and read-back based on signal type
|
||||
if (signalType == "string")
|
||||
{
|
||||
var strValue = convertedValue as string ?? convertedValue.ToString();
|
||||
var length = int.TryParse(signal.IndexOrLength, out var len) ? (ushort)len : (ushort)1;
|
||||
|
||||
// 写入字符串 | Write string
|
||||
writeSuccess = await _directWriteChannel.WriteStringAsync(address, strValue, length);
|
||||
if (!writeSuccess)
|
||||
{
|
||||
_logger.Error(null, "直接写入失败: 信号={SignalName}, 地址={Address} | Direct write failed: signal={SignalName}, address={Address}",
|
||||
signalName, address);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 回读字符串 | Read back string
|
||||
readBackValue = await _directWriteChannel.ReadStringAsync(address, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 根据具体类型执行泛型写入和回读 | Execute generic write and read-back based on specific type
|
||||
switch (signalType)
|
||||
{
|
||||
case "bool":
|
||||
writeSuccess = await _directWriteChannel.WriteAsync(address, (bool)convertedValue);
|
||||
if (!writeSuccess)
|
||||
{
|
||||
_logger.Error(null, "直接写入失败: 信号={SignalName}, 地址={Address} | Direct write failed: signal={SignalName}, address={Address}",
|
||||
signalName, address);
|
||||
return false;
|
||||
}
|
||||
readBackValue = await _directWriteChannel.ReadAsync<bool>(address);
|
||||
break;
|
||||
case "byte":
|
||||
writeSuccess = await _directWriteChannel.WriteAsync(address, (byte)convertedValue);
|
||||
if (!writeSuccess)
|
||||
{
|
||||
_logger.Error(null, "直接写入失败: 信号={SignalName}, 地址={Address} | Direct write failed: signal={SignalName}, address={Address}",
|
||||
signalName, address);
|
||||
return false;
|
||||
}
|
||||
readBackValue = await _directWriteChannel.ReadAsync<byte>(address);
|
||||
break;
|
||||
case "short":
|
||||
writeSuccess = await _directWriteChannel.WriteAsync(address, (short)convertedValue);
|
||||
if (!writeSuccess)
|
||||
{
|
||||
_logger.Error(null, "直接写入失败: 信号={SignalName}, 地址={Address} | Direct write failed: signal={SignalName}, address={Address}",
|
||||
signalName, address);
|
||||
return false;
|
||||
}
|
||||
readBackValue = await _directWriteChannel.ReadAsync<short>(address);
|
||||
break;
|
||||
case "int":
|
||||
writeSuccess = await _directWriteChannel.WriteAsync(address, (int)convertedValue);
|
||||
if (!writeSuccess)
|
||||
{
|
||||
_logger.Error(null, "直接写入失败: 信号={SignalName}, 地址={Address} | Direct write failed: signal={SignalName}, address={Address}",
|
||||
signalName, address);
|
||||
return false;
|
||||
}
|
||||
readBackValue = await _directWriteChannel.ReadAsync<int>(address);
|
||||
break;
|
||||
case "single":
|
||||
writeSuccess = await _directWriteChannel.WriteAsync(address, (float)convertedValue);
|
||||
if (!writeSuccess)
|
||||
{
|
||||
_logger.Error(null, "直接写入失败: 信号={SignalName}, 地址={Address} | Direct write failed: signal={SignalName}, address={Address}",
|
||||
signalName, address);
|
||||
return false;
|
||||
}
|
||||
readBackValue = await _directWriteChannel.ReadAsync<float>(address);
|
||||
break;
|
||||
case "double":
|
||||
writeSuccess = await _directWriteChannel.WriteAsync(address, (double)convertedValue);
|
||||
if (!writeSuccess)
|
||||
{
|
||||
_logger.Error(null, "直接写入失败: 信号={SignalName}, 地址={Address} | Direct write failed: signal={SignalName}, address={Address}",
|
||||
signalName, address);
|
||||
return false;
|
||||
}
|
||||
readBackValue = await _directWriteChannel.ReadAsync<double>(address);
|
||||
break;
|
||||
default:
|
||||
throw new PlcException(
|
||||
$"不支持的信号类型: {signal.Type}, 信号: {signal.Name} | " +
|
||||
$"Unsupported signal type: {signal.Type}, signal: {signal.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
// 比较回读值与写入值 | Compare read-back value with written value
|
||||
var isMatch = convertedValue.Equals(readBackValue);
|
||||
|
||||
// Info 级别记录信号名称、写入值、回读值和校验结果 | Log at Info level
|
||||
_logger.Info("直接写入校验: 信号={SignalName}, 写入值={WriteValue}, 回读值={ReadValue}, 结果={Result} | " +
|
||||
"Direct write verify: signal={SignalName}, write={WriteValue}, readback={ReadValue}, result={Result}",
|
||||
signalName, convertedValue, readBackValue, isMatch ? "通过|Pass" : "失败|Fail");
|
||||
|
||||
if (!isMatch)
|
||||
{
|
||||
_logger.Error(null, "回读校验失败: 信号={SignalName}, 写入值={WriteValue}, 回读值={ReadValue} | " +
|
||||
"Read-back verification failed: signal={SignalName}, write={WriteValue}, readback={ReadValue}",
|
||||
signalName, convertedValue, readBackValue);
|
||||
}
|
||||
|
||||
return isMatch;
|
||||
}
|
||||
catch (PlcException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "直接写入异常: 信号={SignalName}, 错误={Message} | Direct write exception: signal={SignalName}, error={Message}",
|
||||
signalName, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user