Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,49 @@ public void Parse_FEState_ExpectedFEState(int state, FEState expState)
Assert.Equal(expState, fitnessEquipment.State);
}

[Theory]
[InlineData(FitnessEquipmentType.Treadmill, DataPage.GeneralFEData)]
[InlineData(FitnessEquipmentType.Treadmill, DataPage.GeneralMetabolicData)]
[InlineData(FitnessEquipmentType.Treadmill, DataPage.GeneralSettings)]
[InlineData(FitnessEquipmentType.Treadmill, DataPage.TreadmillData)]
[InlineData(FitnessEquipmentType.Elliptical, DataPage.EllipticalData)]
[InlineData(FitnessEquipmentType.Rower, DataPage.RowerData)]
[InlineData(FitnessEquipmentType.Climber, DataPage.ClimberData)]
[InlineData(FitnessEquipmentType.StationaryBike, DataPage.StationaryBikeData)]
[InlineData(FitnessEquipmentType.NordicSkier, DataPage.NordicSkierData)]
[InlineData(FitnessEquipmentType.TrainerStationaryBike, DataPage.TrainerStationaryBikeData)]
public void Parse_DataPage_LapToggledEventRaised(FitnessEquipmentType equipmentType, DataPage page)
{
// Arrange
var fitnessEquipment = CreateFitnessEquipment(equipmentType);
byte[] dataPage = [(byte)page, 0, 0, 0, 0, 0, 0, 0x80];
bool eventRaised = false;
bool lowHighToggle = false;
bool highLowToggle = false;
fitnessEquipment.LapToggled += (s, e) => eventRaised = true;

// Act
fitnessEquipment.Parse(
dataPage);
lowHighToggle = eventRaised;

eventRaised = false;
dataPage[7] = 0x00; // Toggle back to low
fitnessEquipment.Parse(
dataPage);
highLowToggle = eventRaised;

// Assert
Assert.True(lowHighToggle);
Assert.True(highLowToggle);
}

[Theory]
[InlineData(FitnessEquipmentType.Treadmill)]
[InlineData(FitnessEquipmentType.Elliptical)]
[InlineData(FitnessEquipmentType.Rower)]
[InlineData(FitnessEquipmentType.Climber)]
[InlineData(FitnessEquipmentType.StationaryBike)]
[InlineData(FitnessEquipmentType.NordicSkier)]
[InlineData(FitnessEquipmentType.TrainerStationaryBike)]
public void GetEquipment_GeneralDataPage_ExpectedEquipment(FitnessEquipmentType equipmentType)
Expand All @@ -73,6 +111,9 @@ public void GetEquipment_GeneralDataPage_ExpectedEquipment(FitnessEquipmentType
case FitnessEquipmentType.Climber:
Assert.IsType<Climber>(fitnessEquipment);
break;
case FitnessEquipmentType.StationaryBike:
Assert.IsType<StationaryBike>(fitnessEquipment);
break;
case FitnessEquipmentType.NordicSkier:
Assert.IsType<NordicSkier>(fitnessEquipment);
break;
Expand All @@ -90,6 +131,7 @@ public void GetEquipment_GeneralDataPage_ExpectedEquipment(FitnessEquipmentType
[InlineData(DataPage.EllipticalData, FitnessEquipmentType.Elliptical)]
[InlineData(DataPage.RowerData, FitnessEquipmentType.Rower)]
[InlineData(DataPage.ClimberData, FitnessEquipmentType.Climber)]
[InlineData(DataPage.StationaryBikeData, FitnessEquipmentType.StationaryBike)]
[InlineData(DataPage.NordicSkierData, FitnessEquipmentType.NordicSkier)]
[InlineData(DataPage.TrainerStationaryBikeData, FitnessEquipmentType.TrainerStationaryBike)]
[InlineData(DataPage.TrainerTorqueData, FitnessEquipmentType.TrainerStationaryBike)]
Expand All @@ -116,6 +158,9 @@ public void GetEquipment_SpecificPage_ExpectedEquipment(DataPage pageNumber, Fit
case FitnessEquipmentType.Climber:
Assert.IsType<Climber>(fitnessEquipment);
break;
case FitnessEquipmentType.StationaryBike:
Assert.IsType<StationaryBike>(fitnessEquipment);
break;
case FitnessEquipmentType.NordicSkier:
Assert.IsType<NordicSkier>(fitnessEquipment);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Microsoft.Extensions.Logging;
using Moq;
using SmallEarthTech.AntPlus.DeviceProfiles.FitnessEquipment;
using SmallEarthTech.AntRadioInterface;
using Xunit;

namespace AntPlus.UnitTests.DeviceProfiles.FitnessEquipment
{
public class StationaryBikeTests
{
private readonly StationaryBike _stationaryBike;

public StationaryBikeTests()
{
_stationaryBike = new(new ChannelId(0), Mock.Of<IAntChannel>(), Mock.Of<ILogger<StationaryBike>>(), It.IsAny<int>());
Comment thread
StephenHidem marked this conversation as resolved.
}

[Fact]
public void Parse_InstantaneousCadenceAndPower_Matches()
{
// Arrange
byte[] dataPage = [0x15, 0xFF, 0xFF, 0xFF, 60, 150, 0, 0];

// Act
_stationaryBike.Parse(dataPage);

// Assert
Assert.Equal(60, _stationaryBike.Cadence);
Assert.Equal(150, _stationaryBike.InstantaneousPower);
}

[Fact]
public void Parse_PowerHighValue_Correct()
{
// Arrange - 274W = 0x0112
byte[] dataPage = [0x15, 0xFF, 0xFF, 0xFF, 87, 0x12, 0x01, 0];
Comment thread
StephenHidem marked this conversation as resolved.

// Act
_stationaryBike.Parse(dataPage);

// Assert
Assert.Equal(87, _stationaryBike.Cadence);
Assert.Equal(274, _stationaryBike.InstantaneousPower);
}

[Fact]
public void Parse_CommonDataPage_Handled()
{
// Arrange - common page 0x50 (Manufacturer Info)
byte[] dataPage = [0x50, 0xFF, 0xFF, 16, 32, 0, 100, 0];

// Act & Assert - should not throw
_stationaryBike.Parse(dataPage);
}

[Fact]
public void ToString_ReturnsExpected()
{
Assert.Equal("Stationary Bike", _stationaryBike.ToString());
}
}
}
15 changes: 9 additions & 6 deletions AntPlus/AntPlus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<RootNamespace>SmallEarthTech.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<PackageId>SmallEarthTech.$(AssemblyName)</PackageId>
<VersionPrefix>6.0.4</VersionPrefix>
<VersionPrefix>6.1.0</VersionPrefix>
<Title>ANT+ Class Library</Title>
<PackageProjectUrl>https://stephenhidem.github.io/AntPlus</PackageProjectUrl>
<Authors>Stephen Hidem</Authors>
Expand All @@ -23,16 +23,19 @@
<PackageIcon>PackageLogo.png</PackageIcon>
<PackageReadmeFile>readme.md</PackageReadmeFile>
<PackageReleaseNotes>
1. Updated NuGet dependencies.
1. New device profile: Legacy Stationary Bike added to Fitness Equipment category.
2. Refactor: HandleFEState handles lap toggle.
3. Bug fix: Treadmill now correctly handles fitness equipment state.
4. Updated NuGet dependencies.
</PackageReleaseNotes>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>

<PropertyGroup>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<PackageLicenseFile>OSMFEULA.txt</PackageLicenseFile>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<PackageLicenseFile>OSMFEULA.txt</PackageLicenseFile>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
</PropertyGroup>

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
Expand Down
26 changes: 17 additions & 9 deletions AntPlus/DeviceProfiles/FitnessEquipment/FitnessEquipment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public enum DataPage
TreadmillData = 0x13,
/// <summary>Elliptical data page</summary>
EllipticalData = 0x14,
/// <summary> Legacy stationary bike dAta page</summary>
StationaryBikeData = 0x15,
/// <summary>Rower data page</summary>
RowerData = 0x16,
/// <summary>Climber data page</summary>
Expand Down Expand Up @@ -82,6 +84,8 @@ public enum FitnessEquipmentType
Treadmill = 0x13,
/// <summary>Elliptical</summary>
Elliptical = 0x14,
/// <summary>Legacy stationary bike</summary>
StationaryBike = 0x15,
/// <summary>Rower</summary>
Rower = 0x16,
/// <summary>Climber</summary>
Expand Down Expand Up @@ -328,13 +332,6 @@ public override void Parse(byte[] dataPage)
case DataPage.GeneralFEData:
HandleFEState(dataPage);
GeneralData.Parse(dataPage);

// check for lap toggle
if (lapToggleState != ((dataPage[7] & 0x80) == 0x80))
{
lapToggleState = (dataPage[7] & 0x80) == 0x80;
LapToggled?.Invoke(this, EventArgs.Empty);
}
break;
case DataPage.GeneralSettings:
HandleFEState(dataPage);
Expand All @@ -344,7 +341,7 @@ public override void Parse(byte[] dataPage)
HandleFEState(dataPage);
GeneralMetabolic.Parse(dataPage);
break;
// handle specific FE pages
// handle on demand pages
case DataPage.FECapabilities:
MaxTrainerResistance = BitConverter.ToUInt16(dataPage, 5);
TrainingModes = (SupportedTrainingModes)dataPage[7];
Expand All @@ -355,10 +352,17 @@ public override void Parse(byte[] dataPage)
}
}

/// <summary>Handles the state of the fitness equipment.</summary>
/// <summary>Handles the state of the fitness equipment including the lap toggle.</summary>
/// <param name="dataPage">The data page being parsed.</param>
protected void HandleFEState(byte[] dataPage)
{
// check for lap toggle
if (lapToggleState != ((dataPage[7] & 0x80) == 0x80))
{
lapToggleState = (dataPage[7] & 0x80) == 0x80;
LapToggled?.Invoke(this, EventArgs.Empty);
}

var st = (dataPage[7] & 0x70) >> 4;
// check for valid state
if (Enum.IsDefined(typeof(FEState), st))
Expand Down Expand Up @@ -478,6 +482,8 @@ public async Task<MessagingReturnCode> RequestFECapabilities()
return new Treadmill(channelId, antChannel, loggerFactory.CreateLogger<Treadmill>(), timeout);
case FitnessEquipmentType.Elliptical:
return new Elliptical(channelId, antChannel, loggerFactory.CreateLogger<Elliptical>(), timeout);
case FitnessEquipmentType.StationaryBike:
return new StationaryBike(channelId, antChannel, loggerFactory.CreateLogger<StationaryBike>(), timeout);
case FitnessEquipmentType.Rower:
return new Rower(channelId, antChannel, loggerFactory.CreateLogger<Rower>(), timeout);
case FitnessEquipmentType.Climber:
Expand All @@ -495,6 +501,8 @@ public async Task<MessagingReturnCode> RequestFECapabilities()
return new Treadmill(channelId, antChannel, loggerFactory.CreateLogger<Treadmill>(), timeout);
case DataPage.EllipticalData:
return new Elliptical(channelId, antChannel, loggerFactory.CreateLogger<Elliptical>(), timeout);
case DataPage.StationaryBikeData:
return new StationaryBike(channelId, antChannel, loggerFactory.CreateLogger<StationaryBike>(), timeout);
case DataPage.RowerData:
return new Rower(channelId, antChannel, loggerFactory.CreateLogger<Rower>(), timeout);
case DataPage.ClimberData:
Expand Down
61 changes: 61 additions & 0 deletions AntPlus/DeviceProfiles/FitnessEquipment/StationaryBike.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.Logging;
using SmallEarthTech.AntRadioInterface;
using System;

namespace SmallEarthTech.AntPlus.DeviceProfiles.FitnessEquipment
{
/// <summary>
/// This class supports the ANT+ Legacy Stationary Bike device profile. It extends the FitnessEquipment base class to provide specific properties and parsing logic for stationary bike data pages, including cadence and instantaneous power.
/// </summary>
public partial class StationaryBike : FitnessEquipment
{
/// <summary>Gets the instantaneous pedaling cadence in revolutions per minute.</summary>
/// <value>The instantaneous cadence. 255 (0xFF) indicates invalid.</value>
[ObservableProperty]
private byte cadence;
/// <summary>Gets the instantaneous power in watts.</summary>
/// <value>The instantaneous power. 65535 (0xFFFF) indicates invalid.</value>
Comment thread
StephenHidem marked this conversation as resolved.
[ObservableProperty]
private int instantaneousPower;

/// <summary>Initializes a new instance of the <see cref="StationaryBike" /> class.</summary>
/// <inheritdoc cref="AntDevice(ChannelId, IAntChannel, ILogger, int)"/>
public StationaryBike(ChannelId channelId, IAntChannel antChannel, ILogger logger, int timeout) : base(channelId, antChannel, logger, timeout)
{
}

/// <summary>Initializes a new instance of the <see cref="StationaryBike" /> class.</summary>
/// <inheritdoc cref="AntDevice(ChannelId, IAntChannel, ILogger, TimeoutOptions?)"/>
public StationaryBike(ChannelId channelId, IAntChannel antChannel, ILogger logger, TimeoutOptions? timeoutOptions) : base(channelId, antChannel, logger, timeoutOptions)
{
}

/// <summary>
/// Parses the specified data page and updates the relevant properties.
/// </summary>
/// <remarks>If the data page represents stationary bike data, the method extracts cadence and
/// instantaneous power values. For other data page types, common data page parsing is delegated to the
/// CommonDataPages class. The method does not process the page if it has already been handled.</remarks>
/// <param name="dataPage">A byte array containing the data page to parse.</param>
override public void Parse(byte[] dataPage)
{
base.Parse(dataPage);
if (handledPage) return;

if ((DataPage)dataPage[0] == DataPage.StationaryBikeData)
{
HandleFEState(dataPage);
Cadence = dataPage[4];
InstantaneousPower = BitConverter.ToInt16(dataPage, 5);
Comment thread
StephenHidem marked this conversation as resolved.
}
else
{
CommonDataPages.ParseCommonDataPage(dataPage);
Comment thread
StephenHidem marked this conversation as resolved.
}
}

/// <inheritdoc/>
public override string ToString() => "Stationary Bike";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace SmallEarthTech.AntPlus.DeviceProfiles.FitnessEquipment
{
/// <summary>
/// This class supports the stationary bike fitness equipment type.
/// This class supports the trainer/stationary bike fitness equipment type.
/// </summary>
public partial class TrainerStationaryBike : FitnessEquipment
{
Expand Down
1 change: 1 addition & 0 deletions AntPlus/DeviceProfiles/FitnessEquipment/Treadmill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public override void Parse(byte[] dataPage)

if ((DataPage)dataPage[0] == DataPage.TreadmillData)
{
HandleFEState(dataPage);
if (isFirstDataMessage)
{
prevNeg = dataPage[5];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@
<link xlink:href="96dd3305-46d4-42b2-bfa0-0ccf90028989" />
</para>
</listItem>
<listItem>
<para>
<link xlink:href="2b295d47-8d19-4efd-8ed5-775da9613fab" />
</para>
</listItem>
</list>
</content>
<!-- If a section contains a sections element, its content creates
Expand Down
Loading
Loading