diff --git a/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj b/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj index 57639b71..43da4f59 100644 --- a/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj +++ b/src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj @@ -1,7 +1,7 @@  - 9.0.6 + 9.0.8 diff --git a/src/extensions/BootstrapBlazor.Socket/DataConverter/HexConverter.cs b/src/extensions/BootstrapBlazor.Socket/DataConverter/HexConverter.cs index 7d6b95c7..3b74ef3f 100644 --- a/src/extensions/BootstrapBlazor.Socket/DataConverter/HexConverter.cs +++ b/src/extensions/BootstrapBlazor.Socket/DataConverter/HexConverter.cs @@ -32,6 +32,16 @@ public static string ToString(byte[]? bytes, string? separator = "-", bool upper return string.Join(separator, bytes.Select(i => upper ? i.ToString("X2") : i.ToString("x2"))); } + /// + /// 将 byte[] 转为 16 进制字符串 + /// Converts a byte array to its hexadecimal string representation. + /// + /// + /// + /// + /// + public static string ToString(ReadOnlySpan span, string? separator = "-", bool upper = true) => ToString(span.ToArray(), separator, upper); + /// /// 将字符串转换为字节数组 /// diff --git a/src/extensions/BootstrapBlazor.Socket/Extensions/ActivatorExtensions.cs b/src/extensions/BootstrapBlazor.Socket/Extensions/ActivatorExtensions.cs index 0b4bd8d3..371e03f5 100644 --- a/src/extensions/BootstrapBlazor.Socket/Extensions/ActivatorExtensions.cs +++ b/src/extensions/BootstrapBlazor.Socket/Extensions/ActivatorExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Website: https://www.blazor.zone or https://argozhang.github.io/ +using BootstrapBlazor.Socket.Logging; using System.Reflection; namespace System; @@ -20,7 +21,17 @@ public static class ActivatorExtensions public static object? CreateInstance(this Type type, object?[]? args = null) { var bindings = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; - return Activator.CreateInstance(type, bindings, null, args, null); + + object? instance = null; + try + { + instance = Activator.CreateInstance(type, bindings, null, args, null); + } + catch (Exception ex) + { + SocketLogging.LogError(ex, $"Create Instance {type.FullName} failed"); + } + return instance; } /// diff --git a/src/extensions/BootstrapBlazor.Socket/Logging/SocketLogging.cs b/src/extensions/BootstrapBlazor.Socket/Logging/SocketLogging.cs index 423f0b3d..07b46c03 100644 --- a/src/extensions/BootstrapBlazor.Socket/Logging/SocketLogging.cs +++ b/src/extensions/BootstrapBlazor.Socket/Logging/SocketLogging.cs @@ -30,15 +30,17 @@ public static void Init(ILogger logger) } /// - /// - /// - /// - public static void LogError(string message) => _logger?.LogError(message); - - /// - /// + /// 记录异常信息方法 /// /// /// - public static void LogError(Exception ex, string? message) => _logger?.LogError(ex, message); + public static void LogError(Exception ex, string? message = null) + { + if (_logger == null) + { + return; + } + + _logger.LogError(ex, "{message}", message); + } } diff --git a/src/extensions/BootstrapBlazor.Socket/Utility/ModbusCrc16.cs b/src/extensions/BootstrapBlazor.Socket/Utility/ModbusCrc16.cs new file mode 100644 index 00000000..74190383 --- /dev/null +++ b/src/extensions/BootstrapBlazor.Socket/Utility/ModbusCrc16.cs @@ -0,0 +1,109 @@ +// Copyright (c) BootstrapBlazor & Argo Zhang (argo@live.ca). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.Socket.Algorithm; + +/// +/// Modubs CRC-16 查表算法 +/// +public static class ModbusCrc16 +{ + // 预计算的CRC表(256个条目) + private static readonly ushort[] CrcTable = + [ + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 + ]; + + /// + /// 计算 Modbus CRC-16 校验码 + /// + /// 要计算的数据字节数组 + /// CRC校验码(低字节在前) + public static ushort Compute(ReadOnlySpan data) + { + ushort crc = 0xFFFF; + foreach (var b in data) + { + crc = (ushort)((crc >> 8) ^ CrcTable[(crc ^ b) & 0xFF]); + } + return crc; + } + + /// + /// 计算 CRC 并将结果添加到消息末尾(Modbus格式) + /// + /// 原始数据 + /// 带 CRC 校验码的数据 + public static ReadOnlySpan Append(ReadOnlySpan data) + { + ushort crc = Compute(data); + + // 使用 stackalloc 避免堆分配(小数据时) + if (data.Length <= 256) + { + Span result = stackalloc byte[data.Length + 2]; + data.CopyTo(result); + result[data.Length] = (byte)(crc & 0xFF); + result[data.Length + 1] = (byte)(crc >> 8); + return result.ToArray(); + } + else + { + // 大数据使用常规数组 + byte[] result = new byte[data.Length + 2]; + data.CopyTo(result); + result[data.Length] = (byte)(crc & 0xFF); + result[data.Length + 1] = (byte)(crc >> 8); + return result; + } + } + + /// + /// 验证带 CRC 的数据是否有效 + /// + /// 包含 CRC 校验码的数据 + /// 验证结果 + public static bool Validate(ReadOnlySpan dataWithCrc) + { + if (dataWithCrc.Length < 2) + { + return false; + } + + ushort receivedCrc = (ushort)(dataWithCrc[^1] << 8 | dataWithCrc[^2]); + ushort calculatedCrc = Compute(dataWithCrc[..^2]); + return receivedCrc == calculatedCrc; + } +} diff --git a/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClient.cs b/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClient.cs index 72f7c237..52486cc5 100644 --- a/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClient.cs +++ b/src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClient.cs @@ -47,7 +47,7 @@ class DefaultTcpSocketClient(TcpSocketClientOptions options) : IServiceProvider, /// /// /// - public Func, ValueTask>? ReceivedCallBack { get; set; } + public Func, ValueTask>? ReceivedCallback { get; set; } /// /// @@ -329,10 +329,10 @@ private async ValueTask ReceiveCoreAsync(ITcpSocketClientProvider client, M buffer = buffer[..len]; } - if (ReceivedCallBack != null) + if (ReceivedCallback != null) { // 如果订阅回调则触发回调 - await ReceivedCallBack(buffer); + await ReceivedCallback(buffer); } } catch (OperationCanceledException ex) diff --git a/src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs b/src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs index a2d21b50..f229bd56 100644 --- a/src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs +++ b/src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs @@ -76,7 +76,7 @@ async ValueTask ReceivedCallback(ReadOnlyMemory buffer) Cache.Add(client, [(adapter, ReceivedCallback)]); } - client.ReceivedCallBack += ReceivedCallback; + client.ReceivedCallback += ReceivedCallback; // 设置 DataPackageAdapter 的回调函数 adapter.ReceivedCallBack = callback; @@ -94,7 +94,7 @@ public static void RemoveDataPackageAdapter(this ITcpSocketClient client, Func i.Adapter.ReceivedCallBack == callback).ToList(); foreach (var c in items) { - client.ReceivedCallBack -= c.Callback; + client.ReceivedCallback -= c.Callback; list.Remove(c); } } @@ -142,7 +142,7 @@ async ValueTask ReceivedCallback(ReadOnlyMemory buffer) EntityCache.Add(client, [(ReceivedCallback, callback)]); } - client.ReceivedCallBack += ReceivedCallback; + client.ReceivedCallback += ReceivedCallback; // 设置 DataPackageAdapter 的回调函数 adapter.ReceivedCallBack = async buffer => @@ -168,7 +168,7 @@ public static void RemoveDataPackageAdapter(this ITcpSocketClient clien var items = list.Where(i => i.EntityCallback.Equals(callback)).ToList(); foreach (var c in items) { - client.ReceivedCallBack -= c.ReceivedCallback; + client.ReceivedCallback -= c.ReceivedCallback; list.Remove(c); } } @@ -217,7 +217,7 @@ async ValueTask ReceivedCallback(ReadOnlyMemory buffer) EntityCache.Add(client, [(ReceivedCallback, callback)]); } - client.ReceivedCallBack += ReceivedCallback; + client.ReceivedCallback += ReceivedCallback; IDataConverter? converter = null; diff --git a/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClient.cs b/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClient.cs index 3e491134..5ad2f777 100644 --- a/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClient.cs +++ b/src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClient.cs @@ -35,7 +35,7 @@ public interface ITcpSocketClient : IAsyncDisposable /// The callback function should be designed to handle the received data efficiently and /// asynchronously. Ensure that the implementation does not block or perform long-running operations, as this may /// impact performance. - Func, ValueTask>? ReceivedCallBack { get; set; } + Func, ValueTask>? ReceivedCallback { get; set; } /// /// Gets or sets the callback function that is invoked when a connection attempt is initiated. diff --git a/test/UnitTestTcpSocket/ActivationExtensionsTest.cs b/test/UnitTestTcpSocket/ActivationExtensionsTest.cs new file mode 100644 index 00000000..3de407d1 --- /dev/null +++ b/test/UnitTestTcpSocket/ActivationExtensionsTest.cs @@ -0,0 +1,45 @@ +// Copyright (c) BootstrapBlazor & Argo Zhang (argo@live.ca). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace UnitTestTcpSocket; + +public class ActivationExtensionsTest +{ + [Fact] + public void Activation_Ok() + { + var type = typeof(Foo); + var o = type.CreateInstance(); + Assert.NotNull(o); + + var foo = o as Foo; + Assert.NotNull(foo); + + var foo1 = type.CreateInstance(); + Assert.NotNull(foo1); + } + + [Fact] + public void Activation_Nest() + { + var o = typeof(MockNestEntity).CreateInstance([0.01f]); + Assert.Equal(0.01f, o?.Rate); + } + + [Fact] + public void Activation_Fail() + { + var type = typeof(string); + var o = type.CreateInstance([123]); + Assert.Null(o); + + var foo = type.CreateInstance(); + Assert.Null(foo); + } + + class MockNestEntity(float rate) + { + public float Rate { get; } = rate; + } +} diff --git a/test/UnitTestTcpSocket/HexConverterTest.cs b/test/UnitTestTcpSocket/HexConverterTest.cs index 840b07f8..4c95a1db 100644 --- a/test/UnitTestTcpSocket/HexConverterTest.cs +++ b/test/UnitTestTcpSocket/HexConverterTest.cs @@ -31,7 +31,10 @@ public void ToHexString_Ok() var actual = HexConverter.ToString(data); Assert.Equal("1A-02-13-04-FE", actual); - actual = HexConverter.ToString(data, " "); + actual = HexConverter.ToString(data, " ", false); + Assert.Equal("1a 02 13 04 fe", actual); + + actual = HexConverter.ToString(data, " ", true); Assert.Equal("1A 02 13 04 FE", actual); } diff --git a/test/UnitTestTcpSocket/ModbusCrcTest.cs b/test/UnitTestTcpSocket/ModbusCrcTest.cs new file mode 100644 index 00000000..fc1b5b7f --- /dev/null +++ b/test/UnitTestTcpSocket/ModbusCrcTest.cs @@ -0,0 +1,36 @@ +// Copyright (c) BootstrapBlazor & Argo Zhang (argo@live.ca). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using BootstrapBlazor.Socket.Algorithm; + +namespace UnitTestTcpSocket; + +public class ModbusCrcTest +{ + [Fact] + public void Computer_Ok() + { + // 06 00 00 01 7F C9 BA + var data = new byte[] { 0x01, 0x06, 0x00, 0x00, 0x01, 0x7F }; + + var crc = ModbusCrc16.Compute(data); + Assert.Equal("BAC9", crc.ToString("X4")); + Assert.Equal("01060000017FC9BA", HexConverter.ToString(ModbusCrc16.Append(data), "")); + } + + [Fact] + public void Validate_Ok() + { + var result = ModbusCrc16.Validate([0x01]); + Assert.False(result); + + result = ModbusCrc16.Validate([0x01, 0x06, 0x00, 0x00, 0x01, 0x7F, 0xC9, 0xBA]); + Assert.True(result); + + result = false; + var data = Enumerable.Range(0, 300).Select(i => (byte)Random.Shared.Next(0, 255)); + result = ModbusCrc16.Validate(ModbusCrc16.Append(data.ToArray())); + Assert.True(result); + } +} diff --git a/test/UnitTestTcpSocket/SocketLoggingTest.cs b/test/UnitTestTcpSocket/SocketLoggingTest.cs new file mode 100644 index 00000000..34292790 --- /dev/null +++ b/test/UnitTestTcpSocket/SocketLoggingTest.cs @@ -0,0 +1,16 @@ +// Copyright (c) BootstrapBlazor & Argo Zhang (argo@live.ca). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using BootstrapBlazor.Socket.Logging; + +namespace UnitTestTcpSocket; + +public class SocketLoggingTest +{ + [Fact] + public void Logger_Ok() + { + SocketLogging.LogError(new Exception()); + } +} diff --git a/test/UnitTestTcpSocket/TcpSocketFactoryTest.cs b/test/UnitTestTcpSocket/TcpSocketFactoryTest.cs index 0b524042..25ba6a91 100644 --- a/test/UnitTestTcpSocket/TcpSocketFactoryTest.cs +++ b/test/UnitTestTcpSocket/TcpSocketFactoryTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Website: https://www.blazor.zone or https://argozhang.github.io/ +using BootstrapBlazor.Socket.Logging; using Microsoft.Extensions.Logging; using System.Buffers; using System.Net; @@ -337,7 +338,7 @@ public async Task ReceiveAsync_Error() var tcs = new TaskCompletionSource(); // 增加接收回调方法 - client.ReceivedCallBack = b => + client.ReceivedCallback = b => { buffer = b; tcs.SetResult(); @@ -631,6 +632,14 @@ public async Task DelimiterDataPackageHandler_Ok() Assert.NotNull(ex); } + [Fact] + public void TryConvertTo_Error() + { + var converter = new MockErrorDataConverter(); + var result = converter.TryConvertTo(new byte[] { 0x1, 0x2 }, out var entity); + Assert.False(result); + } + [Fact] public async Task TryConvertTo_Ok() { @@ -1417,6 +1426,14 @@ protected override bool Parse(ReadOnlyMemory data, MockEntity entity) } } + class MockErrorDataConverter : DataConverter + { + protected override bool Parse(ReadOnlyMemory data, MockEntity entity) + { + throw new Exception("Mock parse error"); + } + } + class FooConverter(string name) : IDataPropertyConverter { public object? Convert(ReadOnlyMemory data)