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