A:优化PLC数据管理
This commit is contained in:
1 parent
2f3eb935dd
commit
bc09280d4e
15 files changed
+562
-93
No files matched your search
@@ -7,6 +7,7 @@ using Plc.Models;
|
||||
using Plc.Siemens.Options;
|
||||
using S7.Net;
|
||||
using System.Windows;
|
||||
using Sharp.Wpf.Abstractions;
|
||||
|
||||
namespace Client.ViewModels;
|
||||
|
||||
@@ -14,6 +15,7 @@ public class PlcViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IPlcService _plcService;
|
||||
private readonly SiemensPlcOptions _siemensOptions;
|
||||
private readonly IDispatcher _dispatcher;
|
||||
|
||||
// 连接配置
|
||||
private PlcBrand _selectedBrand = PlcBrand.Siemens;
|
||||
@@ -30,10 +32,14 @@ public class PlcViewModel : ViewModelBase
|
||||
// 监控
|
||||
private string _monitorAddress = "DB1.DBW0";
|
||||
|
||||
public PlcViewModel(IPlcService plcService, SiemensPlcOptions siemensOptions)
|
||||
public PlcViewModel(
|
||||
IPlcService plcService,
|
||||
SiemensPlcOptions siemensOptions,
|
||||
IDispatcher dispatcher)
|
||||
{
|
||||
_plcService = plcService;
|
||||
_siemensOptions = siemensOptions;
|
||||
_dispatcher = dispatcher;
|
||||
|
||||
// 初始化界面值为当前配置值
|
||||
_ipAddress = _siemensOptions.IpAddress;
|
||||
@@ -44,6 +50,7 @@ public class PlcViewModel : ViewModelBase
|
||||
ReadCommand = new AsyncCommand(ReadAsync);
|
||||
WriteCommand = new AsyncCommand(WriteAsync);
|
||||
AddMonitorCommand = new RelayCommand(AddMonitor);
|
||||
RemoveMonitorCommand = new RelayCommand<PlcTag>(RemoveMonitor, tag => tag != null);
|
||||
}
|
||||
|
||||
#region Properties
|
||||
@@ -124,6 +131,7 @@ public class PlcViewModel : ViewModelBase
|
||||
public IAsyncCommand ReadCommand { get; }
|
||||
public IAsyncCommand WriteCommand { get; }
|
||||
public ICommand AddMonitorCommand { get; }
|
||||
public ICommand RemoveMonitorCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -201,12 +209,34 @@ public class PlcViewModel : ViewModelBase
|
||||
DataType = typeof(short),
|
||||
OnChanged = (t, v) =>
|
||||
{
|
||||
// 注意:UI绑定更新需要在UI线程,这里简化处理,假设WPF做了同步或者是后台线程安全集合
|
||||
t.Value = v;
|
||||
// 使用Dispatcher在UI线程更新值,避免跨线程访问异常
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
t.Value = v;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_plcService.Subscribe(tag);
|
||||
MonitoredTags.Add(tag);
|
||||
|
||||
// 在UI线程添加到集合
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
MonitoredTags.Add(tag);
|
||||
});
|
||||
}
|
||||
|
||||
private void RemoveMonitor(PlcTag? tag)
|
||||
{
|
||||
if (tag == null) return;
|
||||
|
||||
// 从PLC服务取消订阅
|
||||
_plcService.Unsubscribe(tag.Id);
|
||||
|
||||
// 从UI集合移除
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
MonitoredTags.Remove(tag);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Plc.Abstractions;
|
||||
using Plc.Services;
|
||||
using Plc.Siemens;
|
||||
@@ -8,15 +9,42 @@ namespace Plc.Siemens.DependencyInjection;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddSiemensPlc(this IServiceCollection services, Action<SiemensPlcOptions> configureOptions)
|
||||
/// <summary>
|
||||
/// 添加西门子PLC服务到DI容器
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configureOptions">配置选项的委托</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSiemensPlc(
|
||||
this IServiceCollection services,
|
||||
Action<SiemensPlcOptions> configureOptions)
|
||||
{
|
||||
var options = new SiemensPlcOptions();
|
||||
configureOptions(options);
|
||||
// 使用标准的 IOptions 模式
|
||||
services.Configure(configureOptions);
|
||||
|
||||
services.AddSingleton(options);
|
||||
services.AddSingleton<IPlcDriver, SiemensDriver>();
|
||||
services.AddSingleton<IPlcOptimizer, SiemensOptimizer>();
|
||||
// 同时注册 SiemensPlcOptions 单例,供需要直接访问的地方使用(如 PlcViewModel)
|
||||
// 注意:这是一个共享实例,从 IOptions 获取值
|
||||
services.AddSingleton(sp => sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value);
|
||||
|
||||
// 注册驱动程序(使用 IOptions)
|
||||
services.AddSingleton<IPlcDriver>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value;
|
||||
return new SiemensDriver(options);
|
||||
});
|
||||
|
||||
// 注册优化器(使用 IOptions 中的 MaxGapBytes)
|
||||
services.AddSingleton<IPlcOptimizer>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<SiemensPlcOptions>>().Value;
|
||||
var logger = sp.GetService<Microsoft.Extensions.Logging.ILogger<SiemensOptimizer>>();
|
||||
return new SiemensOptimizer(options.MaxGapBytes, logger);
|
||||
});
|
||||
|
||||
// 注册数据转换器
|
||||
services.AddSingleton<IDataConverter, BigEndianConverter>();
|
||||
|
||||
// 注册PLC服务
|
||||
services.AddSingleton<IPlcService, PlcMonitorService>();
|
||||
|
||||
return services;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Plc.Abstractions;
|
||||
|
||||
namespace Plc.Siemens.Models;
|
||||
|
||||
public enum SiemensMemoryArea
|
||||
@@ -10,7 +12,7 @@ public enum SiemensMemoryArea
|
||||
Counter = 0x1C
|
||||
}
|
||||
|
||||
public class SiemensAddress
|
||||
public class SiemensAddress : IPlcAddress
|
||||
{
|
||||
public SiemensMemoryArea Area { get; set; }
|
||||
public int DbNumber { get; set; } // Only for DataBlock
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
using S7.Net;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net;
|
||||
|
||||
namespace Plc.Siemens.Options;
|
||||
|
||||
public class SiemensPlcOptions
|
||||
public class SiemensPlcOptions : IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
public string IpAddress { get; set; } = "127.0.0.1";
|
||||
|
||||
[Range(1, 65535)]
|
||||
public int Port { get; set; } = 102;
|
||||
|
||||
[Range(0, 255)]
|
||||
public short Rack { get; set; } = 0;
|
||||
|
||||
[Range(0, 255)]
|
||||
public short Slot { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// S7-1200/1500 通常支持 960,S7-300 通常支持 240。
|
||||
/// 默认保守值设为 240。
|
||||
/// </summary>
|
||||
[Range(128, 960)]
|
||||
public int PduSize { get; set; } = 240;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,4 +30,39 @@ public class SiemensPlcOptions
|
||||
/// 默认为 S71200。
|
||||
/// </summary>
|
||||
public CpuType CpuType { get; set; } = CpuType.S71200;
|
||||
|
||||
/// <summary>
|
||||
/// 优化器允许的最大间隔字节数。
|
||||
/// 两个标签之间如果间隔小于此值,会合并为一次读取。
|
||||
/// 默认为 20。
|
||||
/// </summary>
|
||||
[Range(0, 1000)]
|
||||
public int MaxGapBytes { get; set; } = 20;
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
// 验证IP地址格式
|
||||
if (!IPAddress.TryParse(IpAddress, out _))
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
$"Invalid IP address format: {IpAddress}",
|
||||
new[] { nameof(IpAddress) });
|
||||
}
|
||||
|
||||
// 验证PDU大小合理性
|
||||
if (PduSize < 128 || PduSize > 960)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
$"PDU size must be between 128 and 960, got: {PduSize}",
|
||||
new[] { nameof(PduSize) });
|
||||
}
|
||||
|
||||
// 验证MaxGapBytes的合理性
|
||||
if (MaxGapBytes < 0 || MaxGapBytes > PduSize)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
$"MaxGapBytes ({MaxGapBytes}) must be between 0 and PduSize ({PduSize})",
|
||||
new[] { nameof(MaxGapBytes) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
||||
<PackageReference Include="S7netplus" Version="0.20.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -11,11 +11,10 @@ public class SiemensDriver : IPlcDriver
|
||||
{
|
||||
private readonly SiemensPlcOptions _options;
|
||||
private S7.Net.Plc? _plc;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public SiemensDriver(SiemensPlcOptions options)
|
||||
{
|
||||
_options = options;
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public bool IsConnected => _plc?.IsConnected ?? false;
|
||||
@@ -34,7 +33,12 @@ public class SiemensDriver : IPlcDriver
|
||||
}
|
||||
|
||||
// Create new instance with latest options
|
||||
_plc = new S7.Net.Plc(_options.CpuType, _options.IpAddress, _options.Port, _options.Rack, _options.Slot);
|
||||
_plc = new S7.Net.Plc(
|
||||
_options.CpuType,
|
||||
_options.IpAddress,
|
||||
_options.Port,
|
||||
_options.Rack,
|
||||
_options.Slot);
|
||||
|
||||
await _plc.OpenAsync(cancellationToken);
|
||||
|
||||
@@ -45,34 +49,39 @@ public class SiemensDriver : IPlcDriver
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure(ex.Message);
|
||||
return Result.Failure($"Connection failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DisconnectAsync()
|
||||
public Task<Result> DisconnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_plc?.Close();
|
||||
return await Task.FromResult(Result.Success());
|
||||
return Task.FromResult(Result.Success());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure(ex.Message);
|
||||
return Task.FromResult(Result.Failure($"Disconnect failed: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<byte[]>> ReadBytesAsync(string address, ushort length, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsConnected || _plc == null)
|
||||
return Result.Failure<byte[]>("PLC 未连接。");
|
||||
return Result.Failure<byte[]>("PLC is not connected");
|
||||
|
||||
try
|
||||
{
|
||||
var siemensAddr = SiemensAddressParser.Parse(address);
|
||||
var dataType = MapToS7DataType(siemensAddr.Area);
|
||||
|
||||
var bytes = await _plc.ReadBytesAsync(dataType, siemensAddr.DbNumber, siemensAddr.ByteOffset, length, cancellationToken);
|
||||
var bytes = await _plc.ReadBytesAsync(
|
||||
dataType,
|
||||
siemensAddr.DbNumber,
|
||||
siemensAddr.ByteOffset,
|
||||
length,
|
||||
cancellationToken);
|
||||
|
||||
return Result.Success(bytes);
|
||||
}
|
||||
@@ -85,14 +94,19 @@ public class SiemensDriver : IPlcDriver
|
||||
public async Task<Result> WriteBytesAsync(string address, byte[] data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsConnected || _plc == null)
|
||||
return Result.Failure("PLC 未连接。");
|
||||
return Result.Failure("PLC is not connected");
|
||||
|
||||
try
|
||||
{
|
||||
var siemensAddr = SiemensAddressParser.Parse(address);
|
||||
var dataType = MapToS7DataType(siemensAddr.Area);
|
||||
|
||||
await _plc.WriteBytesAsync(dataType, siemensAddr.DbNumber, siemensAddr.ByteOffset, data, cancellationToken);
|
||||
await _plc.WriteBytesAsync(
|
||||
dataType,
|
||||
siemensAddr.DbNumber,
|
||||
siemensAddr.ByteOffset,
|
||||
data,
|
||||
cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
@@ -112,12 +126,13 @@ public class SiemensDriver : IPlcDriver
|
||||
SiemensMemoryArea.Outputs => DataType.Output,
|
||||
SiemensMemoryArea.Timer => DataType.Timer,
|
||||
SiemensMemoryArea.Counter => DataType.Counter,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(area), area, null)
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(area), area, $"Unsupported memory area: {area}")
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_plc?.Close();
|
||||
_plc = null;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,23 @@ using Plc.Models;
|
||||
using Plc.Siemens.Models;
|
||||
using Plc.Siemens.Utils;
|
||||
using Plc.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Plc.Siemens;
|
||||
|
||||
public class SiemensOptimizer : IPlcOptimizer
|
||||
{
|
||||
// 允许变量之间合并为一次读取的最大间隔字节数
|
||||
private const int MaxGapBytes = 20;
|
||||
private const int DefaultMaxGapBytes = 20;
|
||||
|
||||
private readonly int _maxGapBytes;
|
||||
private readonly ILogger<SiemensOptimizer>? _logger;
|
||||
|
||||
public SiemensOptimizer(int maxGapBytes = DefaultMaxGapBytes, ILogger<SiemensOptimizer>? logger = null)
|
||||
{
|
||||
_maxGapBytes = maxGapBytes;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<PlcReadBatch> Optimize(IEnumerable<PlcTag> tags, int maxPduSize)
|
||||
{
|
||||
@@ -24,16 +34,21 @@ public class SiemensOptimizer : IPlcOptimizer
|
||||
try
|
||||
{
|
||||
tag.ParsedAddress = SiemensAddressParser.Parse(tag.Address);
|
||||
_logger?.LogTrace("Parsed address {Address} for tag {TagId}", tag.Address, tag.Id);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误?暂时跳过。
|
||||
_logger?.LogWarning(ex, "Failed to parse address {Address} for tag {TagId}, skipping",
|
||||
tag.Address, tag.Id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
validTags.Add(tag);
|
||||
}
|
||||
|
||||
_logger?.LogDebug("Optimizing {ValidCount} valid tags out of {TotalCount} tags",
|
||||
validTags.Count, tags.Count());
|
||||
|
||||
// 2. 按区域和 DB 号分组(物理位置)
|
||||
var groups = validTags.GroupBy(t =>
|
||||
{
|
||||
@@ -46,12 +61,17 @@ public class SiemensOptimizer : IPlcOptimizer
|
||||
// 3. 按字节偏移量排序
|
||||
var sortedTags = group.OrderBy(t => ((SiemensAddress)t.ParsedAddress!).ByteOffset).ToList();
|
||||
|
||||
_logger?.LogTrace("Processing group: Area={Area}, DB={DbNumber}, Tags={Count}",
|
||||
group.Key.Area, group.Key.DbNumber, sortedTags.Count);
|
||||
|
||||
PlcReadBatch? currentBatch = null;
|
||||
|
||||
foreach (var tag in sortedTags)
|
||||
{
|
||||
var addr = (SiemensAddress)tag.ParsedAddress!;
|
||||
int typeSize = TypeHelper.GetSize(tag.DataType);
|
||||
int typeSize = tag.DataType == typeof(string)
|
||||
? TypeHelper.GetSize(tag.DataType, tag.Length)
|
||||
: TypeHelper.GetSize(tag.DataType);
|
||||
int byteLength = typeSize * tag.Length; // 此标签的总字节数
|
||||
|
||||
// 如果是 bool,它可能在一个字节内,但我们在检查边界时假设它占用该字节
|
||||
@@ -60,6 +80,7 @@ public class SiemensOptimizer : IPlcOptimizer
|
||||
{
|
||||
currentBatch = CreateBatch(tag, addr, byteLength);
|
||||
batches.Add(currentBatch);
|
||||
_logger?.LogTrace("Created new batch starting at {Address}", tag.Address);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -77,20 +98,27 @@ public class SiemensOptimizer : IPlcOptimizer
|
||||
// 1. 标签在当前结束之后(或位置相同)开始(因为已排序,所以是肯定的)
|
||||
// 2. 间隔足够小
|
||||
// 3. 总大小适合 PDU
|
||||
if (gap >= 0 && gap <= MaxGapBytes && newTotalLength <= maxPduSize)
|
||||
if (gap >= 0 && gap <= _maxGapBytes && newTotalLength <= maxPduSize)
|
||||
{
|
||||
currentBatch.Tags.Add(tag);
|
||||
currentBatch.TotalLength = newTotalLength;
|
||||
_logger?.LogTrace("Merged tag {TagId} into batch (gap={Gap}, newLength={Length})",
|
||||
tag.Id, gap, newTotalLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 开启新批次
|
||||
_logger?.LogTrace("Cannot merge tag {TagId} (gap={Gap}, newLength={Length}, maxPdu={MaxPdu}), creating new batch",
|
||||
tag.Id, gap, newTotalLength, maxPduSize);
|
||||
currentBatch = CreateBatch(tag, addr, byteLength);
|
||||
batches.Add(currentBatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger?.LogDebug("Optimization complete: {TagCount} tags -> {BatchCount} batches",
|
||||
validTags.Count, batches.Count);
|
||||
|
||||
return batches;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,22 @@ namespace Plc.Abstractions;
|
||||
|
||||
public interface IDataConverter
|
||||
{
|
||||
// 将原始字节(从指定偏移量开始)转换为目标类型
|
||||
object? Parse(byte[] buffer, int offset, Type type, int length);
|
||||
/// <summary>
|
||||
/// 将原始字节(从指定偏移量开始)转换为目标类型
|
||||
/// </summary>
|
||||
/// <param name="buffer">字节缓冲区</param>
|
||||
/// <param name="offset">起始偏移量</param>
|
||||
/// <param name="type">目标类型</param>
|
||||
/// <param name="length">数据长度(对字符串和数组有效)</param>
|
||||
/// <param name="bitOffset">位偏移量(仅对bool类型有效)</param>
|
||||
object? Parse(byte[] buffer, int offset, Type type, int length, int bitOffset = 0);
|
||||
|
||||
// 将值转换为字节
|
||||
byte[] ToBytes(object value, Type type, int length);
|
||||
/// <summary>
|
||||
/// 将值转换为字节
|
||||
/// </summary>
|
||||
/// <param name="value">要转换的值</param>
|
||||
/// <param name="type">值的类型</param>
|
||||
/// <param name="length">数据长度(对字符串和数组有效)</param>
|
||||
/// <param name="bitOffset">位偏移量(仅对bool类型有效)</param>
|
||||
byte[] ToBytes(object value, Type type, int length, int bitOffset = 0);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Plc.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 定义PLC地址的通用接口,用于不同品牌的地址解析结果
|
||||
/// </summary>
|
||||
public interface IPlcAddress
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取字节偏移量
|
||||
/// </summary>
|
||||
int ByteOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取位偏移量(仅对bool类型有效)
|
||||
/// </summary>
|
||||
int BitOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取地址的字符串表示
|
||||
/// </summary>
|
||||
string ToString();
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class PlcReadBatch
|
||||
|
||||
/// <summary>
|
||||
/// 驱动程序特定的起始地址上下文(避免重新解析)。
|
||||
/// 使用 IPlcAddress 接口以支持类型安全访问。
|
||||
/// </summary>
|
||||
public object? ParsedStartAddress { get; set; }
|
||||
public IPlcAddress? ParsedStartAddress { get; set; }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Plc.Abstractions;
|
||||
|
||||
namespace Plc.Models;
|
||||
|
||||
/// <summary>
|
||||
@@ -39,6 +41,7 @@ public class PlcTag
|
||||
/// <summary>
|
||||
/// 内部使用:由驱动程序/优化器用于存储解析后的地址对象。
|
||||
/// 这避免了在每个周期重新解析地址字符串。
|
||||
/// 使用 IPlcAddress 接口以支持类型安全访问。
|
||||
/// </summary>
|
||||
public object? ParsedAddress { get; set; }
|
||||
public IPlcAddress? ParsedAddress { get; set; }
|
||||
}
|
||||
@@ -10,4 +10,8 @@
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -5,54 +5,129 @@ namespace Plc.Services;
|
||||
|
||||
public class BigEndianConverter : IDataConverter
|
||||
{
|
||||
public object? Parse(byte[] buffer, int offset, Type type, int length)
|
||||
public object? Parse(byte[] buffer, int offset, Type type, int length, int bitOffset = 0)
|
||||
{
|
||||
// 为简洁起见省略了边界检查,但在生产环境中至关重要
|
||||
// 参数验证
|
||||
if (buffer == null)
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
if (offset < 0 || offset >= buffer.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset), "Offset is out of buffer range");
|
||||
|
||||
// Bool类型处理(按位)
|
||||
if (type == typeof(bool))
|
||||
return buffer[offset] > 0; // 简单的检查,真实的位逻辑需要位索引
|
||||
|
||||
if (type == typeof(byte))
|
||||
return buffer[offset];
|
||||
|
||||
if (type == typeof(short))
|
||||
return BinaryPrimitives.ReadInt16BigEndian(buffer.AsSpan(offset));
|
||||
|
||||
if (type == typeof(ushort))
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(offset));
|
||||
|
||||
if (type == typeof(int))
|
||||
return BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset));
|
||||
|
||||
if (type == typeof(uint))
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(offset));
|
||||
|
||||
if (type == typeof(float))
|
||||
return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset)));
|
||||
|
||||
if (type == typeof(double))
|
||||
return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(buffer.AsSpan(offset)));
|
||||
|
||||
// 简单处理字符串 (假设 ASCII/S7 String)
|
||||
if (type == typeof(string))
|
||||
{
|
||||
// S7 String 前两个字节是 MaxLen 和 ActualLen,后面是字符
|
||||
// 这里假设是纯字符数组或者需要特定处理
|
||||
// 简单起见,按 ASCII 读取指定长度
|
||||
return System.Text.Encoding.ASCII.GetString(buffer, offset, length);
|
||||
if (offset >= buffer.Length)
|
||||
throw new ArgumentException("Buffer too small for bool");
|
||||
if (bitOffset < 0 || bitOffset > 7)
|
||||
throw new ArgumentOutOfRangeException(nameof(bitOffset), "Bit offset must be 0-7");
|
||||
|
||||
byte b = buffer[offset];
|
||||
return (b & (1 << bitOffset)) != 0;
|
||||
}
|
||||
|
||||
return null;
|
||||
// Byte类型
|
||||
if (type == typeof(byte))
|
||||
{
|
||||
if (offset >= buffer.Length)
|
||||
throw new ArgumentException("Buffer too small for byte");
|
||||
return buffer[offset];
|
||||
}
|
||||
|
||||
// Short类型
|
||||
if (type == typeof(short))
|
||||
{
|
||||
if (offset + 2 > buffer.Length)
|
||||
throw new ArgumentException("Buffer too small for short");
|
||||
return BinaryPrimitives.ReadInt16BigEndian(buffer.AsSpan(offset));
|
||||
}
|
||||
|
||||
// UShort类型
|
||||
if (type == typeof(ushort))
|
||||
{
|
||||
if (offset + 2 > buffer.Length)
|
||||
throw new ArgumentException("Buffer too small for ushort");
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(offset));
|
||||
}
|
||||
|
||||
// Int类型
|
||||
if (type == typeof(int))
|
||||
{
|
||||
if (offset + 4 > buffer.Length)
|
||||
throw new ArgumentException("Buffer too small for int");
|
||||
return BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset));
|
||||
}
|
||||
|
||||
// UInt类型
|
||||
if (type == typeof(uint))
|
||||
{
|
||||
if (offset + 4 > buffer.Length)
|
||||
throw new ArgumentException("Buffer too small for uint");
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(offset));
|
||||
}
|
||||
|
||||
// Float类型
|
||||
if (type == typeof(float))
|
||||
{
|
||||
if (offset + 4 > buffer.Length)
|
||||
throw new ArgumentException("Buffer too small for float");
|
||||
return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(offset)));
|
||||
}
|
||||
|
||||
// Double类型
|
||||
if (type == typeof(double))
|
||||
{
|
||||
if (offset + 8 > buffer.Length)
|
||||
throw new ArgumentException("Buffer too small for double");
|
||||
return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(buffer.AsSpan(offset)));
|
||||
}
|
||||
|
||||
// String类型(S7 String格式)
|
||||
if (type == typeof(string))
|
||||
{
|
||||
// S7 String结构:
|
||||
// 字节0: MaxLength
|
||||
// 字节1: ActualLength
|
||||
// 字节2+: 字符数据
|
||||
if (offset + 2 > buffer.Length)
|
||||
throw new ArgumentException("Buffer too small for S7 string header");
|
||||
|
||||
int maxLen = buffer[offset];
|
||||
int actualLen = buffer[offset + 1];
|
||||
|
||||
if (actualLen > maxLen)
|
||||
actualLen = maxLen; // 安全处理
|
||||
|
||||
if (offset + 2 + actualLen > buffer.Length)
|
||||
throw new ArgumentException($"Buffer too small for S7 string data (needs {actualLen} bytes)");
|
||||
|
||||
return System.Text.Encoding.ASCII.GetString(buffer, offset + 2, actualLen);
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Type {type.Name} is not supported by this converter");
|
||||
}
|
||||
|
||||
public byte[] ToBytes(object value, Type type, int length)
|
||||
public byte[] ToBytes(object value, Type type, int length, int bitOffset = 0)
|
||||
{
|
||||
if (type == typeof(bool))
|
||||
return new[] { (bool)value ? (byte)1 : (byte)0 };
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
// Bool类型(按位)
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
if (bitOffset < 0 || bitOffset > 7)
|
||||
throw new ArgumentOutOfRangeException(nameof(bitOffset), "Bit offset must be 0-7");
|
||||
|
||||
byte b = 0;
|
||||
if ((bool)value)
|
||||
b = (byte)(1 << bitOffset);
|
||||
return new[] { b };
|
||||
}
|
||||
|
||||
// Byte类型
|
||||
if (type == typeof(byte))
|
||||
return new[] { (byte)value };
|
||||
|
||||
// Short类型
|
||||
if (type == typeof(short))
|
||||
{
|
||||
var bytes = new byte[2];
|
||||
@@ -60,6 +135,7 @@ public class BigEndianConverter : IDataConverter
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// UShort类型
|
||||
if (type == typeof(ushort))
|
||||
{
|
||||
var bytes = new byte[2];
|
||||
@@ -67,6 +143,7 @@ public class BigEndianConverter : IDataConverter
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Int类型
|
||||
if (type == typeof(int))
|
||||
{
|
||||
var bytes = new byte[4];
|
||||
@@ -74,6 +151,7 @@ public class BigEndianConverter : IDataConverter
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// UInt类型
|
||||
if (type == typeof(uint))
|
||||
{
|
||||
var bytes = new byte[4];
|
||||
@@ -81,6 +159,7 @@ public class BigEndianConverter : IDataConverter
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Float类型
|
||||
if (type == typeof(float))
|
||||
{
|
||||
var bytes = new byte[4];
|
||||
@@ -89,6 +168,7 @@ public class BigEndianConverter : IDataConverter
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Double类型
|
||||
if (type == typeof(double))
|
||||
{
|
||||
var bytes = new byte[8];
|
||||
@@ -97,6 +177,22 @@ public class BigEndianConverter : IDataConverter
|
||||
return bytes;
|
||||
}
|
||||
|
||||
return Array.Empty<byte>();
|
||||
// String类型(S7 String格式)
|
||||
if (type == typeof(string))
|
||||
{
|
||||
var str = (string)value;
|
||||
var strBytes = System.Text.Encoding.ASCII.GetBytes(str);
|
||||
var actualLen = Math.Min(strBytes.Length, length);
|
||||
|
||||
// S7 String结构
|
||||
var result = new byte[2 + length];
|
||||
result[0] = (byte)length; // MaxLength
|
||||
result[1] = (byte)actualLen; // ActualLength
|
||||
Array.Copy(strBytes, 0, result, 2, actualLen);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Type {type.Name} is not supported by this converter");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Plc.Abstractions;
|
||||
using Plc.Models;
|
||||
using Plc.Utils;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Plc.Services;
|
||||
|
||||
@@ -11,91 +12,197 @@ public class PlcMonitorService : IPlcService, IDisposable
|
||||
private readonly IPlcDriver _driver;
|
||||
private readonly IPlcOptimizer _optimizer;
|
||||
private readonly IDataConverter _converter;
|
||||
private readonly ILogger<PlcMonitorService>? _logger;
|
||||
|
||||
private readonly ConcurrentDictionary<string, PlcTag> _subscriptions = new();
|
||||
private Task? _monitorTask;
|
||||
private CancellationTokenSource? _cts;
|
||||
private readonly TimeSpan _refreshRate = TimeSpan.FromMilliseconds(200);
|
||||
|
||||
public PlcMonitorService(IPlcDriver driver, IPlcOptimizer optimizer, IDataConverter converter)
|
||||
// 重连策略
|
||||
private int _reconnectAttempts = 0;
|
||||
private const int MaxReconnectAttempts = 10;
|
||||
|
||||
public PlcMonitorService(
|
||||
IPlcDriver driver,
|
||||
IPlcOptimizer optimizer,
|
||||
IDataConverter converter,
|
||||
ILogger<PlcMonitorService>? logger = null)
|
||||
{
|
||||
_driver = driver;
|
||||
_optimizer = optimizer;
|
||||
_converter = converter;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_monitorTask != null) return;
|
||||
if (_monitorTask != null)
|
||||
{
|
||||
_logger?.LogWarning("Monitor service is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
await _driver.ConnectAsync(cancellationToken);
|
||||
_logger?.LogInformation("Starting PLC monitor service");
|
||||
|
||||
var connectResult = await _driver.ConnectAsync(cancellationToken);
|
||||
if (connectResult.IsFailure)
|
||||
{
|
||||
_logger?.LogError("Failed to connect to PLC: {Error}", connectResult.ErrorMessage);
|
||||
throw new InvalidOperationException($"Cannot start monitor service: {connectResult.ErrorMessage}");
|
||||
}
|
||||
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_monitorTask = Task.Run(() => MonitorLoop(_cts.Token), _cts.Token);
|
||||
|
||||
_logger?.LogInformation("PLC monitor service started successfully");
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
_logger?.LogInformation("Stopping PLC monitor service");
|
||||
|
||||
_cts?.Cancel();
|
||||
if (_monitorTask != null)
|
||||
{
|
||||
try { await _monitorTask; } catch (OperationCanceledException) { }
|
||||
try
|
||||
{
|
||||
await _monitorTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger?.LogDebug("Monitor task cancelled successfully");
|
||||
}
|
||||
}
|
||||
|
||||
await _driver.DisconnectAsync();
|
||||
_logger?.LogInformation("PLC monitor service stopped");
|
||||
}
|
||||
|
||||
public void Subscribe(PlcTag tag)
|
||||
{
|
||||
_subscriptions.TryAdd(tag.Id, tag);
|
||||
if (_subscriptions.TryAdd(tag.Id, tag))
|
||||
{
|
||||
_logger?.LogDebug("Subscribed to tag {TagId} at address {Address}", tag.Id, tag.Address);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogWarning("Tag {TagId} is already subscribed", tag.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void Unsubscribe(string tagId)
|
||||
{
|
||||
_subscriptions.TryRemove(tagId, out _);
|
||||
if (_subscriptions.TryRemove(tagId, out _))
|
||||
{
|
||||
_logger?.LogDebug("Unsubscribed tag {TagId}", tagId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogWarning("Failed to unsubscribe tag {TagId} - not found", tagId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<T>> ReadAsync<T>(string address)
|
||||
{
|
||||
_logger?.LogDebug("Reading from address {Address}", address);
|
||||
|
||||
// 临时读取(未优化)
|
||||
int size = TypeHelper.GetSize(typeof(T));
|
||||
var result = await _driver.ReadBytesAsync(address, (ushort)size);
|
||||
|
||||
if (result.IsFailure)
|
||||
{
|
||||
_logger?.LogError("Failed to read from {Address}: {Error}", address, result.ErrorMessage);
|
||||
return Result.Failure<T>(result.ErrorMessage ?? result.Exception?.Message ?? "Read failed");
|
||||
}
|
||||
|
||||
if (result.Value is null)
|
||||
{
|
||||
_logger?.LogError("Read from {Address} returned null data", address);
|
||||
return Result.Failure<T>("Read returned null data");
|
||||
}
|
||||
|
||||
var val = _converter.Parse(result.Value, 0, typeof(T), 1);
|
||||
if (val is null)
|
||||
{
|
||||
_logger?.LogError("Failed to convert data from {Address}", address);
|
||||
return Result.Failure<T>("Conversion failed");
|
||||
}
|
||||
|
||||
_logger?.LogDebug("Successfully read value from {Address}", address);
|
||||
return Result.Success((T)val);
|
||||
}
|
||||
|
||||
public async Task<Result> WriteAsync<T>(string address, T value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
_logger?.LogWarning("Attempted to write null value to {Address}", address);
|
||||
return Result.Failure("Cannot write null value");
|
||||
}
|
||||
|
||||
_logger?.LogDebug("Writing to address {Address}", address);
|
||||
|
||||
int size = TypeHelper.GetSize(typeof(T));
|
||||
var bytes = _converter.ToBytes(value, typeof(T), size);
|
||||
|
||||
if (bytes.Length == 0)
|
||||
{
|
||||
_logger?.LogError("Conversion to bytes failed for type {Type}", typeof(T).Name);
|
||||
return Result.Failure("Conversion to bytes returned empty array. Type might not be supported.");
|
||||
}
|
||||
|
||||
return await _driver.WriteBytesAsync(address, bytes);
|
||||
var result = await _driver.WriteBytesAsync(address, bytes);
|
||||
|
||||
if (result.IsFailure)
|
||||
{
|
||||
_logger?.LogError("Failed to write to {Address}: {Error}", address, result.ErrorMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogDebug("Successfully wrote to {Address}", address);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task MonitorLoop(CancellationToken token)
|
||||
{
|
||||
_logger?.LogInformation("Monitor loop started");
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 连接检查和重试逻辑
|
||||
if (!_driver.IsConnected)
|
||||
{
|
||||
await _driver.ConnectAsync(token);
|
||||
_logger?.LogWarning("PLC disconnected, attempting to reconnect (attempt {Attempt}/{Max})",
|
||||
_reconnectAttempts + 1, MaxReconnectAttempts);
|
||||
|
||||
// 指数退避策略
|
||||
var delay = Math.Min(1000 * Math.Pow(2, _reconnectAttempts), 30000);
|
||||
await Task.Delay((int)delay, token);
|
||||
|
||||
var connectResult = await _driver.ConnectAsync(token);
|
||||
if (connectResult.IsSuccess)
|
||||
{
|
||||
_logger?.LogInformation("Successfully reconnected to PLC");
|
||||
_reconnectAttempts = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_reconnectAttempts++;
|
||||
_logger?.LogError("Reconnection failed: {Error}", connectResult.ErrorMessage);
|
||||
|
||||
if (_reconnectAttempts >= MaxReconnectAttempts)
|
||||
{
|
||||
_logger?.LogCritical("Max reconnection attempts reached, monitor loop will continue trying");
|
||||
_reconnectAttempts = 0; // 重置以继续尝试
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var currentTags = _subscriptions.Values.ToList();
|
||||
@@ -107,6 +214,8 @@ public class PlcMonitorService : IPlcService, IDisposable
|
||||
|
||||
// 优化:计算分桶
|
||||
var batches = _optimizer.Optimize(currentTags, _driver.MaxPduSize);
|
||||
_logger?.LogTrace("Optimized {TagCount} tags into {BatchCount} batches",
|
||||
currentTags.Count, batches.Count);
|
||||
|
||||
// 执行读取
|
||||
foreach (var batch in batches)
|
||||
@@ -121,48 +230,87 @@ public class PlcMonitorService : IPlcService, IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
// 记录错误
|
||||
_logger?.LogWarning("Batch read failed for address {Address}: {Error}",
|
||||
batch.StartAddress, readResult.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 记录异常
|
||||
_logger?.LogDebug("Monitor loop cancelled");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error in monitor loop");
|
||||
}
|
||||
|
||||
await Task.Delay(_refreshRate, token);
|
||||
}
|
||||
|
||||
_logger?.LogInformation("Monitor loop stopped");
|
||||
}
|
||||
|
||||
private void DistributeValues(PlcReadBatch batch, byte[] data)
|
||||
{
|
||||
dynamic batchStart = batch.ParsedStartAddress!;
|
||||
if (batch.ParsedStartAddress == null)
|
||||
{
|
||||
_logger?.LogError("Batch missing parsed start address");
|
||||
return;
|
||||
}
|
||||
|
||||
var batchStart = batch.ParsedStartAddress;
|
||||
int batchStartByte = batchStart.ByteOffset;
|
||||
|
||||
foreach (var tag in batch.Tags)
|
||||
{
|
||||
dynamic tagAddr = tag.ParsedAddress!;
|
||||
if (tag.ParsedAddress == null)
|
||||
{
|
||||
_logger?.LogWarning("Tag {TagId} missing parsed address", tag.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
var tagAddr = tag.ParsedAddress;
|
||||
int tagStartByte = tagAddr.ByteOffset;
|
||||
int relativeOffset = tagStartByte - batchStartByte;
|
||||
|
||||
// 检查边界
|
||||
int typeSize = TypeHelper.GetSize(tag.DataType);
|
||||
int typeSize = tag.DataType == typeof(string)
|
||||
? TypeHelper.GetSize(tag.DataType, tag.Length)
|
||||
: TypeHelper.GetSize(tag.DataType);
|
||||
|
||||
if (relativeOffset + typeSize <= data.Length)
|
||||
{
|
||||
var newVal = _converter.Parse(data, relativeOffset, tag.DataType, tag.Length);
|
||||
|
||||
// 变更检测
|
||||
if (!Equals(tag.Value, newVal))
|
||||
try
|
||||
{
|
||||
tag.Value = newVal;
|
||||
tag.OnChanged?.Invoke(tag, newVal);
|
||||
// 传递位偏移量以支持bool类型
|
||||
var newVal = _converter.Parse(data, relativeOffset, tag.DataType, tag.Length, tagAddr.BitOffset);
|
||||
|
||||
// 变更检测
|
||||
if (!Equals(tag.Value, newVal))
|
||||
{
|
||||
tag.Value = newVal;
|
||||
tag.OnChanged?.Invoke(tag, newVal);
|
||||
_logger?.LogTrace("Tag {TagId} value changed to {Value}", tag.Id, newVal);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to parse value for tag {TagId} at address {Address}",
|
||||
tag.Id, tag.Address);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogWarning("Tag {TagId} offset {Offset} + size {Size} exceeds batch data length {Length}",
|
||||
tag.Id, relativeOffset, typeSize, data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_logger?.LogDebug("Disposing PLC monitor service");
|
||||
_cts?.Cancel();
|
||||
_driver.Dispose();
|
||||
}
|
||||
|
||||
@@ -2,14 +2,46 @@ namespace Plc.Utils;
|
||||
|
||||
public static class TypeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取PLC数据类型的字节大小
|
||||
/// </summary>
|
||||
/// <param name="type">数据类型</param>
|
||||
/// <returns>字节大小</returns>
|
||||
/// <exception cref="ArgumentException">当类型不支持或需要明确长度时抛出</exception>
|
||||
public static int GetSize(Type type)
|
||||
{
|
||||
if (type == typeof(bool)) return 1; // S7 usually treats bool as bit, but allocation often aligns to bytes or we need spec handling
|
||||
if (type == typeof(bool)) return 1; // S7中bool按字节对齐
|
||||
if (type == typeof(byte)) return 1;
|
||||
if (type == typeof(short) || type == typeof(ushort)) return 2;
|
||||
if (type == typeof(int) || type == typeof(uint) || type == typeof(float)) return 4;
|
||||
if (type == typeof(long) || type == typeof(ulong) || type == typeof(double)) return 8;
|
||||
|
||||
return 1; // Default or throw
|
||||
// 字符串类型需要明确指定长度
|
||||
if (type == typeof(string))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"String type requires explicit length specification. Use the overload with length parameter or specify length in PlcTag.",
|
||||
nameof(type));
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Type {type.Name} is not supported. Supported types: bool, byte, short, ushort, int, uint, float, long, ulong, double, string.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取PLC数据类型的字节大小(支持字符串长度)
|
||||
/// </summary>
|
||||
/// <param name="type">数据类型</param>
|
||||
/// <param name="length">长度(对字符串类型有效)</param>
|
||||
/// <returns>字节大小</returns>
|
||||
public static int GetSize(Type type, int length)
|
||||
{
|
||||
if (type == typeof(string))
|
||||
{
|
||||
// S7 String = 2字节头部 + 实际字符数据
|
||||
return 2 + length;
|
||||
}
|
||||
|
||||
// 对于非字符串类型,忽略length参数
|
||||
return GetSize(type);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user