Skip to content

Commit 78b6931

Browse files
authored
feat(ModbusCrc16): add modbus crc-16 algorithm (#560)
* refactor: 更改回调名称 * refactor: 恢复缓存变量 * test: 提高 TryConvertTo 方法代码覆盖率 * test: 提高 HexConverter 代码覆盖率 * refactor: 优化代码 * refactor: 增加异常保护 * test: 增加 SocketLogging 单元测试 * test: 增加 CreateInstance 扩展方法单元测试 * feat: 增加 Modbus CRC-16 算法 * refactor: 更改参数类型 * feat: 增加重载方法 * chore: bump version 9.0.7 * test: 增加单元测试 * refactor: 更改命名空间 * chore: bump version 9.0.8
1 parent 1596adc commit 78b6931

13 files changed

Lines changed: 270 additions & 21 deletions

File tree

src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<Version>9.0.6</Version>
4+
<Version>9.0.8</Version>
55
</PropertyGroup>
66

77
<PropertyGroup>

src/extensions/BootstrapBlazor.Socket/DataConverter/HexConverter.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ public static string ToString(byte[]? bytes, string? separator = "-", bool upper
3232
return string.Join(separator, bytes.Select(i => upper ? i.ToString("X2") : i.ToString("x2")));
3333
}
3434

35+
/// <summary>
36+
/// 将 byte[] 转为 16 进制字符串
37+
/// <para>Converts a byte array to its hexadecimal string representation.</para>
38+
/// </summary>
39+
/// <param name="span"></param>
40+
/// <param name="separator"></param>
41+
/// <param name="upper"></param>
42+
/// <returns></returns>
43+
public static string ToString(ReadOnlySpan<byte> span, string? separator = "-", bool upper = true) => ToString(span.ToArray(), separator, upper);
44+
3545
/// <summary>
3646
/// 将字符串转换为字节数组
3747
/// </summary>

src/extensions/BootstrapBlazor.Socket/Extensions/ActivatorExtensions.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
// Website: https://www.blazor.zone or https://argozhang.github.io/
44

5+
using BootstrapBlazor.Socket.Logging;
56
using System.Reflection;
67

78
namespace System;
@@ -20,7 +21,17 @@ public static class ActivatorExtensions
2021
public static object? CreateInstance(this Type type, object?[]? args = null)
2122
{
2223
var bindings = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
23-
return Activator.CreateInstance(type, bindings, null, args, null);
24+
25+
object? instance = null;
26+
try
27+
{
28+
instance = Activator.CreateInstance(type, bindings, null, args, null);
29+
}
30+
catch (Exception ex)
31+
{
32+
SocketLogging.LogError(ex, $"Create Instance {type.FullName} failed");
33+
}
34+
return instance;
2435
}
2536

2637
/// <summary>

src/extensions/BootstrapBlazor.Socket/Logging/SocketLogging.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,17 @@ public static void Init(ILogger logger)
3030
}
3131

3232
/// <summary>
33-
///
34-
/// </summary>
35-
/// <param name="message"></param>
36-
public static void LogError(string message) => _logger?.LogError(message);
37-
38-
/// <summary>
39-
///
33+
/// 记录异常信息方法
4034
/// </summary>
4135
/// <param name="ex"></param>
4236
/// <param name="message"></param>
43-
public static void LogError(Exception ex, string? message) => _logger?.LogError(ex, message);
37+
public static void LogError(Exception ex, string? message = null)
38+
{
39+
if (_logger == null)
40+
{
41+
return;
42+
}
43+
44+
_logger.LogError(ex, "{message}", message);
45+
}
4446
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright (c) BootstrapBlazor & Argo Zhang (argo@live.ca). All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Website: https://www.blazor.zone or https://argozhang.github.io/
4+
5+
namespace BootstrapBlazor.Socket.Algorithm;
6+
7+
/// <summary>
8+
/// Modubs CRC-16 查表算法
9+
/// </summary>
10+
public static class ModbusCrc16
11+
{
12+
// 预计算的CRC表(256个条目)
13+
private static readonly ushort[] CrcTable =
14+
[
15+
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
16+
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
17+
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
18+
0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
19+
0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
20+
0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
21+
0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
22+
0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
23+
0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
24+
0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
25+
0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
26+
0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
27+
0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
28+
0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
29+
0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
30+
0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
31+
0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
32+
0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
33+
0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
34+
0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
35+
0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
36+
0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
37+
0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
38+
0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
39+
0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
40+
0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
41+
0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
42+
0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
43+
0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
44+
0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
45+
0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
46+
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
47+
];
48+
49+
/// <summary>
50+
/// 计算 Modbus CRC-16 校验码
51+
/// </summary>
52+
/// <param name="data">要计算的数据字节数组</param>
53+
/// <returns>CRC校验码(低字节在前)</returns>
54+
public static ushort Compute(ReadOnlySpan<byte> data)
55+
{
56+
ushort crc = 0xFFFF;
57+
foreach (var b in data)
58+
{
59+
crc = (ushort)((crc >> 8) ^ CrcTable[(crc ^ b) & 0xFF]);
60+
}
61+
return crc;
62+
}
63+
64+
/// <summary>
65+
/// 计算 CRC 并将结果添加到消息末尾(Modbus格式)
66+
/// </summary>
67+
/// <param name="data">原始数据</param>
68+
/// <returns>带 CRC 校验码的数据</returns>
69+
public static ReadOnlySpan<byte> Append(ReadOnlySpan<byte> data)
70+
{
71+
ushort crc = Compute(data);
72+
73+
// 使用 stackalloc 避免堆分配(小数据时)
74+
if (data.Length <= 256)
75+
{
76+
Span<byte> result = stackalloc byte[data.Length + 2];
77+
data.CopyTo(result);
78+
result[data.Length] = (byte)(crc & 0xFF);
79+
result[data.Length + 1] = (byte)(crc >> 8);
80+
return result.ToArray();
81+
}
82+
else
83+
{
84+
// 大数据使用常规数组
85+
byte[] result = new byte[data.Length + 2];
86+
data.CopyTo(result);
87+
result[data.Length] = (byte)(crc & 0xFF);
88+
result[data.Length + 1] = (byte)(crc >> 8);
89+
return result;
90+
}
91+
}
92+
93+
/// <summary>
94+
/// 验证带 CRC 的数据是否有效
95+
/// </summary>
96+
/// <param name="dataWithCrc">包含 CRC 校验码的数据</param>
97+
/// <returns>验证结果</returns>
98+
public static bool Validate(ReadOnlySpan<byte> dataWithCrc)
99+
{
100+
if (dataWithCrc.Length < 2)
101+
{
102+
return false;
103+
}
104+
105+
ushort receivedCrc = (ushort)(dataWithCrc[^1] << 8 | dataWithCrc[^2]);
106+
ushort calculatedCrc = Compute(dataWithCrc[..^2]);
107+
return receivedCrc == calculatedCrc;
108+
}
109+
}

src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClient.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class DefaultTcpSocketClient(TcpSocketClientOptions options) : IServiceProvider,
4747
/// <summary>
4848
/// <inheritdoc/>
4949
/// </summary>
50-
public Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack { get; set; }
50+
public Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallback { get; set; }
5151

5252
/// <summary>
5353
/// <inheritdoc/>
@@ -329,10 +329,10 @@ private async ValueTask<int> ReceiveCoreAsync(ITcpSocketClientProvider client, M
329329
buffer = buffer[..len];
330330
}
331331

332-
if (ReceivedCallBack != null)
332+
if (ReceivedCallback != null)
333333
{
334334
// 如果订阅回调则触发回调
335-
await ReceivedCallBack(buffer);
335+
await ReceivedCallback(buffer);
336336
}
337337
}
338338
catch (OperationCanceledException ex)

src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ async ValueTask ReceivedCallback(ReadOnlyMemory<byte> buffer)
7676
Cache.Add(client, [(adapter, ReceivedCallback)]);
7777
}
7878

79-
client.ReceivedCallBack += ReceivedCallback;
79+
client.ReceivedCallback += ReceivedCallback;
8080

8181
// 设置 DataPackageAdapter 的回调函数
8282
adapter.ReceivedCallBack = callback;
@@ -94,7 +94,7 @@ public static void RemoveDataPackageAdapter(this ITcpSocketClient client, Func<R
9494
var items = list.Where(i => i.Adapter.ReceivedCallBack == callback).ToList();
9595
foreach (var c in items)
9696
{
97-
client.ReceivedCallBack -= c.Callback;
97+
client.ReceivedCallback -= c.Callback;
9898
list.Remove(c);
9999
}
100100
}
@@ -142,7 +142,7 @@ async ValueTask ReceivedCallback(ReadOnlyMemory<byte> buffer)
142142
EntityCache.Add(client, [(ReceivedCallback, callback)]);
143143
}
144144

145-
client.ReceivedCallBack += ReceivedCallback;
145+
client.ReceivedCallback += ReceivedCallback;
146146

147147
// 设置 DataPackageAdapter 的回调函数
148148
adapter.ReceivedCallBack = async buffer =>
@@ -168,7 +168,7 @@ public static void RemoveDataPackageAdapter<TEntity>(this ITcpSocketClient clien
168168
var items = list.Where(i => i.EntityCallback.Equals(callback)).ToList();
169169
foreach (var c in items)
170170
{
171-
client.ReceivedCallBack -= c.ReceivedCallback;
171+
client.ReceivedCallback -= c.ReceivedCallback;
172172
list.Remove(c);
173173
}
174174
}
@@ -217,7 +217,7 @@ async ValueTask ReceivedCallback(ReadOnlyMemory<byte> buffer)
217217
EntityCache.Add(client, [(ReceivedCallback, callback)]);
218218
}
219219

220-
client.ReceivedCallBack += ReceivedCallback;
220+
client.ReceivedCallback += ReceivedCallback;
221221

222222
IDataConverter<TEntity>? converter = null;
223223

src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public interface ITcpSocketClient : IAsyncDisposable
3535
/// <remarks>The callback function should be designed to handle the received data efficiently and
3636
/// asynchronously. Ensure that the implementation does not block or perform long-running operations, as this may
3737
/// impact performance.</remarks>
38-
Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack { get; set; }
38+
Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallback { get; set; }
3939

4040
/// <summary>
4141
/// Gets or sets the callback function that is invoked when a connection attempt is initiated.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) BootstrapBlazor & Argo Zhang (argo@live.ca). All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Website: https://www.blazor.zone or https://argozhang.github.io/
4+
5+
namespace UnitTestTcpSocket;
6+
7+
public class ActivationExtensionsTest
8+
{
9+
[Fact]
10+
public void Activation_Ok()
11+
{
12+
var type = typeof(Foo);
13+
var o = type.CreateInstance();
14+
Assert.NotNull(o);
15+
16+
var foo = o as Foo;
17+
Assert.NotNull(foo);
18+
19+
var foo1 = type.CreateInstance<Foo>();
20+
Assert.NotNull(foo1);
21+
}
22+
23+
[Fact]
24+
public void Activation_Nest()
25+
{
26+
var o = typeof(MockNestEntity).CreateInstance<MockNestEntity>([0.01f]);
27+
Assert.Equal(0.01f, o?.Rate);
28+
}
29+
30+
[Fact]
31+
public void Activation_Fail()
32+
{
33+
var type = typeof(string);
34+
var o = type.CreateInstance([123]);
35+
Assert.Null(o);
36+
37+
var foo = type.CreateInstance<Foo>();
38+
Assert.Null(foo);
39+
}
40+
41+
class MockNestEntity(float rate)
42+
{
43+
public float Rate { get; } = rate;
44+
}
45+
}

test/UnitTestTcpSocket/HexConverterTest.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ public void ToHexString_Ok()
3131
var actual = HexConverter.ToString(data);
3232
Assert.Equal("1A-02-13-04-FE", actual);
3333

34-
actual = HexConverter.ToString(data, " ");
34+
actual = HexConverter.ToString(data, " ", false);
35+
Assert.Equal("1a 02 13 04 fe", actual);
36+
37+
actual = HexConverter.ToString(data, " ", true);
3538
Assert.Equal("1A 02 13 04 FE", actual);
3639
}
3740

0 commit comments

Comments
 (0)