Important
You need to manually declare your ANT specific device class! This const is used as a key when added to the DI container and the AntCollection will instantiate a device with this key.
This how-to walks through the steps taken to create a custom AntDevice and add it to the host container. We will use the ANT+ device profile Bike Radar to illustrate the procedure. The example WpfUsbStickApp illustrates the full example.
Classes derived from AntDevice inherit from the CommunityToolkit.Mvvm NuGet package. This is a very useful package providing robust support for notifications, commanding, and more.
Add the NuGet package SmallEartTech.AntPlus.Extensions.Hosting to the application project.
Add a new class named BikeRadar and derive from AntDevice.
AntDevice is declared as an ObservableObject. Therefore, your class declaration will include the partial modifier.
namespace WpfUsbStickApp.CustomAntDevice
{
public partial class BikeRadar : AntDevice
{
}
}
Add a PNG file to use as an image/icon to the project and set the build action to "Embedded Resource". The Bike Radar image was copied from the Bike Radar specification and saved as a PNG in the CustomAntDevice solution folder.
Implement required properties in the BikeRadar class. You will override ChannelCount, DeviceImageStream, and ToString.
public const byte DeviceClass = 40; // this value comes from the Bike Radar device type
public override int ChannelCount => 4084; // from master channel period in Bike Radar spec
public override Stream? DeviceImageStream => GetType().Assembly.GetManifestResourceStream("WpfUsbStickApp.CustomAntDevice.BikeRadar.png");
public override string ToString() => "Bike Radar";
Generate the constructor from quick refactorings. This should add some more required namespaces. Change the constructor signature to declare the device specific logger. Bike Radar also supports CommonDataPages. The list of radar targets is initialized.
public BikeRadar(ChannelId channelId, IAntChannel antChannel, ILogger<BikeRadar> logger, TimeoutOptions? options)
: base(channelId, antChannel, logger, options)
{
CommonDataPages = new CommonDataPages(logger);
for (int i = 0; i < 8; i++) { RadarTargets.Add(new RadarTarget()); }
}
Define the device specific data pages.
/// <summary>Bike radar data pages.</summary>
public enum DataPage
{
Unknown = 0,
/// <summary>Status</summary>
DeviceStatus = 1,
/// <summary>Commend</summary>
DeviceCommand = 2,
/// <summary>Page A: targets 1 - 4</summary>
RadarTargetsA = 48,
/// <summary>Page B: targets 5 - 8</summary>
RadarTargetsB = 49,
}
Add custom properties the device supports. Add CommonDataPages property if the device supports it.
public enum DeviceState
{
Broadcasting,
ShutdownRequested,
ShutdownAborted,
ShutdownForced
}
public enum ThreatLevel
{
None,
Approaching,
FastApproach,
Reserved
}
public enum ThreatSide
{
Behind,
Right,
Left,
Reserved
}
public partial class RadarTarget : ObservableObject
{
[ObservableProperty]
private ThreatLevel threatLevel;
[ObservableProperty]
private ThreatSide threatSide;
[ObservableProperty]
private double range;
[ObservableProperty]
private double closingSpeed;
}
[ObservableProperty]
private DeviceState state;
[ObservableProperty]
private bool clearTargets;
public List<RadarTarget> RadarTargets { get; } = new();
public CommonDataPages CommonDataPages { get; }
Override Parse. Set the custom properties and/or internal state according to the data page. Parse common data pages in the default clause.
public override void Parse(byte[] dataPage)
{
uint ranges;
ushort speeds;
base.Parse(dataPage);
switch ((DataPage)dataPage[0])
{
case DataPage.Unknown:
break;
case DataPage.DeviceStatus:
State = (DeviceState)(dataPage[1] & 0x03);
ClearTargets = (dataPage[7] & 0x01) == 0;
break;
case DataPage.DeviceCommand:
break;
case DataPage.RadarTargetsA:
ranges = BitConverter.ToUInt32(dataPage, 3) & 0x00FFFFFF;
speeds = BitConverter.ToUInt16(dataPage, 6);
for (int i = 0; i < 4; i++)
{
var threatLevel = RadarTargets[i].ThreatLevel = (ThreatLevel)((dataPage[1] >> (i * 2)) & 0x03);
RadarTargets[i].ThreatSide = threatLevel == ThreatLevel.None ? ThreatSide.Behind : (ThreatSide)((dataPage[2] >> (i * 2)) & 0x03);
RadarTargets[i].Range = threatLevel == ThreatLevel.None ? 0 : 3.125 * (ranges >> (i * 6) & 0x3F);
RadarTargets[i].ClosingSpeed = threatLevel == ThreatLevel.None ? 0 : 3.04 * (speeds >> (i * 4) & 0x0F);
}
break;
case DataPage.RadarTargetsB:
ranges = BitConverter.ToUInt32(dataPage, 3) & 0x00FFFFFF;
speeds = BitConverter.ToUInt16(dataPage, 6);
for (int i = 0; i < 4; i++)
{
var threatLevel = RadarTargets[i + 4].ThreatLevel = (ThreatLevel)((dataPage[1] >> (i * 2)) & 0x03);
RadarTargets[i + 4].ThreatSide = threatLevel == ThreatLevel.None ? ThreatSide.Behind : (ThreatSide)((dataPage[2] >> (i * 2)) & 0x03);
RadarTargets[i + 4].Range = threatLevel == ThreatLevel.None ? 0 : 3.125 * (ranges >> (i * 6) & 0x3F);
RadarTargets[i + 4].ClosingSpeed = threatLevel == ThreatLevel.None ? 0 : 3.04 * (speeds >> (i * 4) & 0x0F);
}
break;
default:
CommonDataPages.ParseCommonDataPage(dataPage);
break;
}
}
Add custom methods to send commands to the ANT device.
public enum Command
{
AbortShutdown,
Shutdown
}
public async Task<MessagingReturnCode> Shutdown(Command command)
{
return await SendExtAcknowledgedMessage(new byte[8] { (byte)DataPage.DeviceCommand, (byte)command, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF });
}
Add your custom device to the host services collection. This must be a keyed transient.
// dependency services
_host = Host.CreateDefaultBuilder(Environment.GetCommandLineArgs()).
UseSerilog().
UseAntPlus(). // this adds all the required dependencies to use the ANT+ class library
ConfigureServices(services =>
{
// add the implementation of IAntRadio to the host
services.AddSingleton<IAntRadio, AntRadio>();
// add custom device to the host
services.AddKeyedTransient<AntDevice, BikeRadar>(BikeRadar.DeviceClass);
}).
Build();
You have now added a custom device to the services that can be instantiated by AntCollection. The next steps would be to implement the view and view model for your custom ANT device to use in your application.