Files

493 lines
25 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 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;
}
}
}
}