Skip to content

Commit f54ee37

Browse files
authored
AntDeviceCollection Update (#36)
* Remove Dispose call in critical log scenario The line `Dispose(); // clear the channels` was removed from the method where a critical log message is generated if `ChannelId` is null. This change ensures that the code will no longer call the `Dispose` method to clear the channels in this scenario. * Refactor AntDeviceCollection and add SendMessageChannel - Made `AntDeviceCollection` a partial class. - Added `_sendMessageChannel` field to `AntDeviceCollection`. - Simplified `AntDeviceCollection` constructor by removing async initialization. - Added `StartScanning` method to initialize ANT radio and channels. - Renamed `Channel_ChannelResponse` to `MessageHandler` and updated it. - Refactored `CreateAntDevice` to use `_sendMessageChannel`. - Updated `AntPlus.csproj` to version `5.0.0.0`. - Added `SendMessageChannel` class to manage multiple ANT channels. - Implemented async data sending and channel management in `SendMessageChannel`. * Refactor tests and add new logging verifications Refactored test classes to use mock loggers and updated test methods to be asynchronous. Added new tests for logging critical messages and warnings. Introduced a new `SendMessageChannelTests` class to verify `NotImplementedException` and concurrent method invocations. * Add release notes and documentation for versions 5.0.0.0 and 1.1.1.0 Updated `AntPlus.csproj` to include new package release notes indicating breaking changes. The `AntDeviceCollection` constructor no longer initiates ANT device scanning; `StartScanning` must be called after instantiation. Updated `AntPllusVersionHistory.aml` and `HostingExtensionsVersionHistory.aml` to include new list items with links to the new version history. Updated `ContentLayout.content` to add new topics for versions 5.0.0.0 and 1.1.1.0, marking version 5.0.0.0 as selected. Updated `Documentation.shfbproj` to re-add namespace summary items and include new entries for the version history files for versions 5.0.0.0 and 1.1.1.0. Added new files `v5.0.0.0.aml` and `v1.1.1.0.aml` detailing the releases, including breaking changes, improvements, and a fix for a potential race condition.
1 parent c5a1d31 commit f54ee37

File tree

14 files changed

+789
-439
lines changed

14 files changed

+789
-439
lines changed

AntPlus.Extensions.Hosting/AntCollection.cs

-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ private void MessageHandler(object? sender, AntResponse e)
9191
e.ChannelNumber,
9292
(MessageId)e.ResponseId,
9393
e.Payload != null ? BitConverter.ToString(e.Payload) : "Null");
94-
Dispose(); // clear the channels
9594
}
9695
}
9796

AntPlus.UnitTests/AntDeviceCollectionTests.cs

+97-13
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
using SmallEarthTech.AntPlus;
44
using SmallEarthTech.AntPlus.DeviceProfiles;
55
using SmallEarthTech.AntPlus.DeviceProfiles.AssetTracker;
6+
using SmallEarthTech.AntPlus.DeviceProfiles.BicyclePower;
67
using SmallEarthTech.AntPlus.DeviceProfiles.BikeSpeedAndCadence;
8+
using SmallEarthTech.AntPlus.DeviceProfiles.FitnessEquipment;
79
using SmallEarthTech.AntRadioInterface;
810
using System;
11+
using System.Linq;
912
using System.Threading;
1013
using System.Threading.Tasks;
1114

@@ -18,6 +21,8 @@ public class AntDeviceCollectionTests
1821

1922
private Mock<IAntRadio> mockAntRadio;
2023
private Mock<IAntChannel> mockAntChannel;
24+
private Mock<ILogger> mockLogger;
25+
private Mock<ILoggerFactory> mockLoggerFactory;
2126

2227
[TestInitialize]
2328
public void TestInitialize()
@@ -26,27 +31,28 @@ public void TestInitialize()
2631

2732
mockAntRadio = mockRepository.Create<IAntRadio>();
2833
mockAntChannel = mockRepository.Create<IAntChannel>();
34+
mockLogger = mockRepository.Create<ILogger>();
35+
mockLoggerFactory = mockRepository.Create<ILoggerFactory>();
36+
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
2937
}
3038

3139
private AntDeviceCollection CreateAntDeviceCollection()
3240
{
33-
IAntChannel[] mockChannels = new IAntChannel[8];
34-
Array.Fill(mockChannels, mockAntChannel.Object);
35-
mockAntRadio.Setup(r => r.InitializeContinuousScanMode().Result).Returns(mockChannels);
41+
var mockChannels = new Mock<IAntChannel>[8];
42+
Array.Fill(mockChannels, mockAntChannel);
43+
mockAntRadio.Setup(r => r.InitializeContinuousScanMode()).ReturnsAsync(mockChannels.Select(m => m.Object).ToArray());
3644
AntDeviceCollection adc = new(
3745
mockAntRadio.Object,
38-
null,
39-
3000);
40-
Thread.Sleep(2000); // allow time for background initialization task to complete
46+
mockLoggerFactory.Object);
4147
return adc;
4248
}
4349

4450
[TestMethod]
45-
public void MultithreadedAdd_Collection_ExpectedCount()
51+
public async Task MultithreadedAdd_Collection_ExpectedCount()
4652
{
4753
// Arrange
48-
Mock<ILogger> mockLogger = new();
4954
var antDeviceCollection = CreateAntDeviceCollection();
55+
await antDeviceCollection.StartScanning();
5056
int numberOfDevices = 16;
5157
using SemaphoreSlim semaphore = new(0, numberOfDevices);
5258
Task[] tasks = new Task[numberOfDevices];
@@ -71,11 +77,11 @@ public void MultithreadedAdd_Collection_ExpectedCount()
7177
}
7278

7379
[TestMethod]
74-
public void MultithreadedRemove_Collection_ExpectedCount()
80+
public async Task MultithreadedRemove_Collection_ExpectedCount()
7581
{
7682
// Arrange
77-
Mock<ILogger<UnknownDevice>> mockLogger = new();
7883
var antDeviceCollection = CreateAntDeviceCollection();
84+
await antDeviceCollection.StartScanning();
7985
int numberOfDevices = 16;
8086
using SemaphoreSlim semaphore = new(0, numberOfDevices);
8187
Task[] tasks = new Task[numberOfDevices];
@@ -102,16 +108,16 @@ public void MultithreadedRemove_Collection_ExpectedCount()
102108

103109
[TestMethod]
104110
[DataRow(HeartRate.DeviceClass, typeof(HeartRate))]
105-
//[DataRow(BicyclePowerTests.DeviceClass, typeof(BicyclePowerTests))] TODO: Fix this
111+
[DataRow(BicyclePower.DeviceClass, typeof(StandardPowerSensor))]
106112
[DataRow(BikeSpeedSensor.DeviceClass, typeof(BikeSpeedSensor))]
107113
[DataRow(BikeCadenceSensor.DeviceClass, typeof(BikeCadenceSensor))]
108114
[DataRow(CombinedSpeedAndCadenceSensor.DeviceClass, typeof(CombinedSpeedAndCadenceSensor))]
109-
//[DataRow(Equipment.DeviceClass, typeof(Treadmill))] TODO: Fix this
115+
[DataRow(FitnessEquipment.DeviceClass, typeof(UnknownDevice))]
110116
[DataRow(MuscleOxygen.DeviceClass, typeof(MuscleOxygen))]
111117
[DataRow(Geocache.DeviceClass, typeof(Geocache))]
112118
[DataRow(Tracker.DeviceClass, typeof(Tracker))]
113119
[DataRow(StrideBasedSpeedAndDistance.DeviceClass, typeof(StrideBasedSpeedAndDistance))]
114-
public void ChannelResponseEvent_Collection_ExpectedDeviceInCollection(byte deviceClass, Type deviceType)
120+
public async Task ChannelResponseEvent_Collection_ExpectedDeviceInCollection(byte deviceClass, Type deviceType)
115121
{
116122
// Arrange
117123
byte[] id = new byte[4] { 1, 0, deviceClass, 0 };
@@ -120,6 +126,7 @@ public void ChannelResponseEvent_Collection_ExpectedDeviceInCollection(byte devi
120126
mockAntChannel.SetupRemove(m => m.ChannelResponse -= It.IsAny<EventHandler<AntResponse>>());
121127
var mockResponse = new MockResponse(cid, new byte[8]);
122128
var antDeviceCollection = CreateAntDeviceCollection();
129+
await antDeviceCollection.StartScanning();
123130

124131
// Act
125132
mockAntChannel.Raise(m => m.ChannelResponse += null, mockAntChannel.Object, mockResponse);
@@ -136,5 +143,82 @@ public MockResponse(ChannelId channelId, byte[] payload)
136143
Payload = payload;
137144
}
138145
}
146+
147+
//[TestMethod]
148+
//public async Task DeviceOffline_RemovesDeviceFromCollection()
149+
//{
150+
// // Arrange
151+
// var antDeviceCollection = CreateAntDeviceCollection();
152+
// await antDeviceCollection.StartScanning();
153+
// Mock<AntDevice> antDevice = new(new ChannelId(1), mockAntChannel.Object, Mock.Of<ILogger>(), 50);
154+
// antDeviceCollection.Add(antDevice.Object);
155+
156+
// // Act
157+
// antDevice.Raise(d => d.DeviceWentOffline += null, EventArgs.Empty);
158+
// //Thread.Sleep(100);
159+
160+
// // Assert
161+
// Assert.AreEqual(0, antDeviceCollection.Count);
162+
//}
163+
164+
[TestMethod]
165+
public async Task MessageHandler_NullChannelId_LogsCritical()
166+
{
167+
// Arrange
168+
var antDeviceCollection = CreateAntDeviceCollection();
169+
await antDeviceCollection.StartScanning();
170+
var response = new MockResponse(null, new byte[8]);
171+
172+
// Act
173+
mockAntChannel.Raise(m => m.ChannelResponse += null, mockAntChannel.Object, response);
174+
175+
// Assert
176+
mockLogger.Verify(
177+
x => x.Log(
178+
LogLevel.Critical,
179+
It.IsAny<EventId>(),
180+
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("ChannelId or Payload is null")),
181+
It.IsAny<Exception>(),
182+
It.Is<Func<It.IsAnyType, Exception, string>>((v, t) => true)),
183+
Times.Once);
184+
}
185+
186+
[TestMethod]
187+
public async Task MessageHandler_NullPayload_LogsCritical()
188+
{
189+
// Arrange
190+
var antDeviceCollection = CreateAntDeviceCollection();
191+
await antDeviceCollection.StartScanning();
192+
var response = new MockResponse(new ChannelId(1), null);
193+
194+
// Act
195+
mockAntChannel.Raise(m => m.ChannelResponse += null, mockAntChannel.Object, response);
196+
197+
// Assert
198+
mockLogger.Verify(
199+
x => x.Log(
200+
LogLevel.Critical,
201+
It.IsAny<EventId>(),
202+
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("ChannelId or Payload is null")),
203+
It.IsAny<Exception>(),
204+
It.Is<Func<It.IsAnyType, Exception, string>>((v, t) => true)),
205+
Times.Once);
206+
}
207+
208+
//[TestMethod]
209+
//public void CreateAntDevice_ValidChannelId_CreatesCorrectDevice()
210+
//{
211+
// // Arrange
212+
// var antDeviceCollection = CreateAntDeviceCollection();
213+
// var channelId = new ChannelId(1) { DeviceType = HeartRate.DeviceClass };
214+
// var dataPage = new byte[8];
215+
216+
// // Act
217+
// var device = antDeviceCollection.CreateAntDevice(channelId, dataPage);
218+
219+
// // Assert
220+
// Assert.IsInstanceOfType(device, typeof(HeartRate));
221+
//}
222+
139223
}
140224
}

AntPlus.UnitTests/DeviceProfiles/BicyclePower/CrankTorqueFrequencySensorTests.cs

+59-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.Extensions.Logging.Abstractions;
1+
using Microsoft.Extensions.Logging;
22
using Moq;
33
using SmallEarthTech.AntPlus.DeviceProfiles.BicyclePower;
44
using SmallEarthTech.AntRadioInterface;
@@ -14,21 +14,24 @@ public class CrankTorqueFrequencySensorTests
1414

1515
private readonly ChannelId mockChannelId = new(0);
1616
private Mock<IAntChannel> mockAntChannel;
17-
private Mock<NullLoggerFactory> mockLogger;
17+
private Mock<ILogger> mockLogger;
18+
private Mock<ILoggerFactory> mockLoggerFactory;
1819

1920
[TestInitialize]
2021
public void TestInitialize()
2122
{
2223
mockRepository = new MockRepository(MockBehavior.Strict);
2324

2425
mockAntChannel = mockRepository.Create<IAntChannel>(MockBehavior.Loose);
25-
mockLogger = mockRepository.Create<NullLoggerFactory>(MockBehavior.Loose);
26+
mockLogger = mockRepository.Create<ILogger>(MockBehavior.Loose);
27+
mockLoggerFactory = mockRepository.Create<ILoggerFactory>();
28+
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
2629
}
2730

2831
private CrankTorqueFrequencySensor CreateCrankTorqueFrequencySensor()
2932
{
3033
byte[] page = new byte[8] { (byte)BicyclePower.DataPage.CrankTorqueFrequency, 0, 0, 0, 0, 0, 0, 0 };
31-
return BicyclePower.GetBicyclePowerSensor(page, mockChannelId, mockAntChannel.Object, mockLogger.Object, 2000) as CrankTorqueFrequencySensor;
34+
return BicyclePower.GetBicyclePowerSensor(page, mockChannelId, mockAntChannel.Object, mockLoggerFactory.Object, 2000) as CrankTorqueFrequencySensor;
3235
}
3336

3437
[TestMethod]
@@ -130,5 +133,57 @@ public async Task SaveSerialNumberToFlash_Message_MessageFormatCorrect()
130133
Assert.AreEqual(MessagingReturnCode.Pass, result);
131134
mockRepository.VerifyAll();
132135
}
136+
137+
[TestMethod]
138+
public void Parse_UnknownDataPage_LogsWarning()
139+
{
140+
// Arrange
141+
var crankTorqueFrequencySensor = CreateCrankTorqueFrequencySensor();
142+
byte[] dataPage = new byte[8] { 0xFF, 0, 0, 0, 0, 0, 0, 0 };
143+
144+
// Act
145+
crankTorqueFrequencySensor.Parse(dataPage);
146+
147+
// Assert
148+
mockLogger.Verify(
149+
m => m.Log(
150+
LogLevel.Warning,
151+
It.IsAny<EventId>(),
152+
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Unknown data page")),
153+
null,
154+
It.IsAny<Func<It.IsAnyType, Exception, string>>()),
155+
Times.Once);
156+
}
157+
158+
[TestMethod]
159+
public void ParseCTFMessage_SameUpdateEventCountAndTorqueTicks_NoCalculations()
160+
{
161+
// Arrange
162+
var crankTorqueFrequencySensor = CreateCrankTorqueFrequencySensor();
163+
byte[] dataPage = new byte[8] { 0x20, 0x01, 0x00, 0x64, 0x07, 0xD0, 0, 0x01 };
164+
165+
// Act
166+
crankTorqueFrequencySensor.Parse(dataPage);
167+
crankTorqueFrequencySensor.Parse(dataPage);
168+
169+
// Assert
170+
Assert.AreEqual(60, crankTorqueFrequencySensor.Cadence);
171+
Assert.AreEqual(0.1, crankTorqueFrequencySensor.Torque);
172+
Assert.AreEqual(0.628, crankTorqueFrequencySensor.Power, 0.001);
173+
}
174+
175+
[TestMethod]
176+
public void ParseCalibrationMessage_UnknownCTFDefinedId_NoAction()
177+
{
178+
// Arrange
179+
var crankTorqueFrequencySensor = CreateCrankTorqueFrequencySensor();
180+
byte[] dataPage = new byte[8] { 0x01, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0x11, 0x22 };
181+
182+
// Act
183+
crankTorqueFrequencySensor.Parse(dataPage);
184+
185+
// Assert
186+
// No exception should be thrown
187+
}
133188
}
134189
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using Microsoft.Extensions.Logging;
2+
using Moq;
3+
using SmallEarthTech.AntPlus;
4+
using SmallEarthTech.AntRadioInterface;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Reflection;
8+
using System.Threading.Tasks;
9+
10+
namespace AntPlus.UnitTests
11+
{
12+
[TestClass]
13+
public class SendMessageChannelTests
14+
{
15+
private readonly Mock<IAntChannel> _mockChannel;
16+
private readonly object _sendMessageChannel;
17+
18+
public SendMessageChannelTests()
19+
{
20+
_mockChannel = new Mock<IAntChannel>();
21+
Type sendMessageChannelType = typeof(AntDeviceCollection).GetNestedType("SendMessageChannel", BindingFlags.NonPublic)!;
22+
_sendMessageChannel = Activator.CreateInstance(sendMessageChannelType, new[] { _mockChannel.Object, _mockChannel.Object, _mockChannel.Object }, Mock.Of<ILogger<AntDeviceCollection>>())!;
23+
}
24+
25+
[TestMethod]
26+
public void AllMethods_ThrowNotImplementedException()
27+
{
28+
var methods = new (string MethodName, object[] Parameters)[]
29+
{
30+
("AssignChannel", new object[] { ChannelType.BaseSlaveReceive, (byte)0, (uint)500 }),
31+
("AssignChannelExt", new object[] { ChannelType.BaseSlaveReceive, (byte)0, ChannelTypeExtended.AdvFastStart, (uint)500 }),
32+
("CloseChannel", new object[] { (uint)500 }),
33+
("ConfigFrequencyAgility", new object[] { (byte)0, (byte)0, (byte)0, (uint)500 }),
34+
("Dispose", new object[] { }),
35+
("IncludeExcludeListAddChannel", new object[] { new ChannelId(0), (byte)0, (uint)500 }),
36+
("IncludeExcludeListConfigure", new object[] { (byte)0, true, (uint)500 }),
37+
("OpenChannel", new object[] { (uint)500 }),
38+
("RequestChannelID", new object[] { (uint)500 }),
39+
("RequestStatus", new object[] { (uint)500 }),
40+
("SendAcknowledgedData", new object[] { new byte[0], (uint)500 }),
41+
("SendAcknowledgedDataAsync", new object[] { new byte[0], (uint)500 }),
42+
("SendBroadcastData", new object[] { new byte[0] }),
43+
("SendBurstTransfer", new object[] { new byte[0], (uint)500 }),
44+
("SendBurstTransferAsync", new object[] { new byte[0], (uint)500 }),
45+
("SendExtAcknowledgedData", new object[] { new ChannelId(0), new byte[0], (uint)500 }),
46+
("SendExtBroadcastData", new object[] { new ChannelId(0), new byte[0] }),
47+
("SendExtBurstTransfer", new object[] { new ChannelId(0), new byte[0], (uint)500 }),
48+
("SendExtBurstTransferAsync", new object[] { new ChannelId(0), new byte[0], (uint)500 }),
49+
("SetChannelFreq", new object[] { (byte)0, (uint)500 }),
50+
("SetChannelID", new object[] { new ChannelId(0), (uint)500 }),
51+
("SetChannelID_UsingSerial", new object[] { new ChannelId(0), (uint)500 }),
52+
("SetChannelPeriod", new object[] { (ushort)0, (uint)500 }),
53+
("SetChannelSearchTimeout", new object[] { (byte)0, (uint)500 }),
54+
("SetChannelTransmitPower", new object[] { TransmitPower.Tx0DB, (uint)500 }),
55+
("SetLowPrioritySearchTimeout", new object[] { (byte)0, (uint)500 }),
56+
("SetProximitySearch", new object[] { (byte)0, (uint)500 }),
57+
("SetSearchThresholdRSSI", new object[] { (byte)0, (uint)500 }),
58+
("UnassignChannel", new object[] { (uint)500 })
59+
};
60+
61+
foreach (var method in methods)
62+
{
63+
var methodInfo = _sendMessageChannel.GetType().GetMethod(method.MethodName);
64+
Assert.IsNotNull(methodInfo);
65+
66+
var exception = Assert.ThrowsException<TargetInvocationException>(() => methodInfo.Invoke(_sendMessageChannel, method.Parameters));
67+
Assert.IsInstanceOfType(exception.InnerException, typeof(NotImplementedException));
68+
}
69+
70+
var propertyInfo = _sendMessageChannel.GetType().GetProperty("ChannelNumber");
71+
Assert.IsNotNull(propertyInfo);
72+
73+
var propertyException = Assert.ThrowsException<TargetInvocationException>(() => propertyInfo.GetGetMethod()!.Invoke(_sendMessageChannel, null));
74+
Assert.IsInstanceOfType(propertyException.InnerException, typeof(NotImplementedException));
75+
}
76+
77+
[TestMethod]
78+
public async Task SendExtAcknowledgedDataAsync_InvokesMethodMultipleTimesAndReturnsPass()
79+
{
80+
var channelId = new ChannelId(0);
81+
var data = new byte[0];
82+
var ackWaitTime = 500U;
83+
var messagingReturnCode = MessagingReturnCode.Pass;
84+
85+
_mockChannel.Setup(c => c.SendExtAcknowledgedDataAsync(channelId, data, ackWaitTime))
86+
.ReturnsAsync(messagingReturnCode);
87+
88+
var methodInfo = _sendMessageChannel.GetType().GetMethod("SendExtAcknowledgedDataAsync");
89+
Assert.IsNotNull(methodInfo);
90+
91+
// Act
92+
var tasks = new List<Task<MessagingReturnCode>>();
93+
for (int i = 0; i < 16; i++)
94+
{
95+
tasks.Add((Task<MessagingReturnCode>)methodInfo.Invoke(_sendMessageChannel, new object[] { channelId, data, ackWaitTime })!);
96+
}
97+
98+
var results = await Task.WhenAll(tasks);
99+
100+
// Assert
101+
foreach (var result in results)
102+
{
103+
Assert.AreEqual(messagingReturnCode, result);
104+
}
105+
_mockChannel.Verify(c => c.SendExtAcknowledgedDataAsync(channelId, data, ackWaitTime), Times.Exactly(tasks.Count));
106+
}
107+
108+
}
109+
}

0 commit comments

Comments
 (0)