How-To: Create and Add a Custom AntDevice to Host

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.

Create the custom ANT device class

  1. Add the NuGet package SmallEartTech.AntPlus.Extensions.Hosting to the application project.

  2. 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.

    C#
    namespace WpfUsbStickApp.CustomAntDevice
    {
        public partial class BikeRadar : AntDevice
        {
        }
    }
  3. 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.

  4. Implement required properties in the BikeRadar class. You will override ChannelCount, DeviceImageStream, and ToString.

      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.

    C#
    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.

    C#
    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()); }
    }
  5. Define the device specific data pages.

    C#
    /// <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,
    }
  6. Add custom properties the device supports. Add CommonDataPages property if the device supports it.

      Note

    The CommunityToolkit.Mvvm supports properties with ObservableProperty attribute. This attribute requires the property declaration to be private and the first letter of the property name set as lowercase. The CommunityToolkit generated partial class uses this private declaration as a backing store and will create a public property with the first letter capitalized.

    C#
    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; }
  7. Override Parse. Set the custom properties and/or internal state according to the data page. Parse common data pages in the default clause.

    C#
    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;
        }
    }
  8. Add custom methods to send commands to the ANT device.

    C#
    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 });
    }
  9. Add your custom device to the host services collection. This must be a keyed transient.

    C#
    // 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.

See Also