diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ec14f53 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# AGENTS.md + +FTMS.NET is a .NET client library for the Bluetooth LE **Fitness Machine Service (FTMS)**. The Bluetooth spec PDFs live in `docs/` (`FTMS_v1.0-1.pdf` is the authoritative FTMS spec; `Assigned_Numbers.pdf` and `GATT_Specification_Supplement.pdf` are context). + +## Layout + +- `FTMS.NET/` — the library. Subfolders mirror the FTMS feature areas: `Control/`, `Data/`, `Features/`, `State/`, `Exceptions/`, `Utils/`. +- `FTMS.NET.Tests/` — xUnit v3 + Moq + Microsoft.Reactive.Testing + coverlet. One test class file per source area. +- `docs/` — the Bluetooth FTMS spec PDFs; check these before changing parsing/format code. + +## Build & test + +Both projects multi-target `net8.0;net9.0;net10.0` (set in `Directory.Build.props`), so a bare `dotnet test` runs the suite three times. + +- Fast, focused verification: `dotnet test -f net10.0` (or `dotnet build -f net10.0`). +- No lint/format script exists; style is enforced by `.editorconfig` via `dotnet format`. +- `dotnet pack` produces the NuGet package; the version is derived from git by GitVersion (GitHubFlow, `main` label `alpha`) — never hand-edit version numbers. + +## Architecture (read this before touching the public surface) + +- The library does **not** do BLE. Callers supply an `IFitnessMachineServiceConnection` wrapping their BLE library; `FitnessMachineServiceFactory` extension methods on that interface assemble `IFitnessMachineService` (`FitnessMachineServiceFactory.cs`). +- `FitnessMachineService` itself is `internal`; the public API is the `IFitnessMachineService*` interfaces. +- Optional characteristics (Control Point, Machine/Training State) are swapped for `ThrowingCharacteristic` when absent; required ones throw `NeededCharacteristicNotAvailableException` via `EnsureAvailableCharacteristic`. +- `FtmsUuids.cs` builds a UUID→name dictionary with source-generated reflection (`[SourceReflection]`, `SourceGeneration.Reflection`, kept AOT-friendly). To register a new UUID, add it as a `public static readonly Guid` field — it is picked up automatically. +- Live data flows out as a DynamicData `IChangeSet` (`IFitnessMachineService.Connect()`). + +## Domain gotchas (all were real bugs) + +- Bit indexing is **standard 8-bit, LSB-first** (`IsBitSet(pos)` with pos 0 = LSB). A previous bug used 7-bit indexing — do not reintroduce it. +- FTMS feature-flag and frame byte offsets have been wrong before (e.g. Target Setting Features offset, UInt24 field encoding). Verify offsets against `docs/FTMS_v1.0-1.pdf` before changing. +- Little-endian values; some fields are `UInt24` (`FTMS.NET/Utils/UInt24.cs`). +- **No range validation is intentional**: control requests do not validate against the machine's advertised feature ranges (see README "Remarks"). Do not add it. + +## Testing conventions + +- Tests run through a real `IFitnessMachineServiceConnection` fake (`FakeConnection`/`FakeCharacteristic` in `FitnessMachineServiceFactory.Tests.cs`) or Moq for internals — `InternalsVisibleTo("FTMS.NET.Tests")` grants access. +- Reactive streams are tested with `Microsoft.Reactive.Testing` (ReactiveUI's TestScheduler). +- Run the full `dotnet test` (all TFMs) before finishing; the single-TFM run is only for fast iteration. + +## Release / CI + +`.github/workflows/cicd.yml`: build+test (with coverage) → pack → upload artifacts; publishes to NuGet (secret `NUGET_API_KEY`) from `release/*` branches, `main`, and on GitHub releases. Local debug builds are fine for iteration. diff --git a/FTMS.NET.Tests/Control/ControlExtensions.Tests.cs b/FTMS.NET.Tests/Control/ControlExtensions.Tests.cs new file mode 100644 index 0000000..8388d5e --- /dev/null +++ b/FTMS.NET.Tests/Control/ControlExtensions.Tests.cs @@ -0,0 +1,43 @@ +namespace FTMS.NET.Tests.Control; + +using FTMS.NET.Control; +using FTMS.NET.Utils; + +public sealed class ControlExtensionsTests +{ + [Fact] + public async Task SetTargetedDistance_SendsOpCodeWithLittleEndianThreeByteParameter() + { + var control = new FakeControl(); + + await control.SetTargetedDistance(new UInt24(1000)); // 0x0003E8 + + Assert.NotNull(control.Request); + Assert.Equal(EControlOpCode.SetTargetedDistance, control.Request!.OpCode); + Assert.Equal(new byte[] { 0xE8, 0x03, 0x00 }, control.Request!.Parameter); + } + + [Fact] + public async Task SetTargetedDistance_MaxValue_SendsThreeByteLittleEndian() + { + var control = new FakeControl(); + + await control.SetTargetedDistance(UInt24.MaxValue); // 0xFFFFFF + + Assert.NotNull(control.Request); + Assert.Equal(new byte[] { 0xFF, 0xFF, 0xFF }, control.Request!.Parameter); + } + + private sealed class FakeControl : IFitnessMachineControl + { + public ControlRequest? Request { get; private set; } + + public Task Execute(ControlRequest request) + { + this.Request = request; + return Task.FromResult(new ControlResponse(request.OpCode, EControlResultCode.Success, [])); + } + + public void Dispose() { } + } +} \ No newline at end of file diff --git a/FTMS.NET.Tests/Control/FitnessMachineControl.Tests.cs b/FTMS.NET.Tests/Control/FitnessMachineControl.Tests.cs new file mode 100644 index 0000000..701f797 --- /dev/null +++ b/FTMS.NET.Tests/Control/FitnessMachineControl.Tests.cs @@ -0,0 +1,76 @@ +namespace FTMS.NET.Tests.Control; + +using FTMS.NET.Control; +using FTMS.NET.Exceptions; +using Microsoft.Reactive.Testing; +using System; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; + +public sealed class FitnessMachineControlTests +{ + [Fact(Timeout = 10_000)] + public async Task Execute_NoResponseWithinTimeout_ThrowsControlRequestException() + { + var scheduler = new TestScheduler(); + var controlPoint = new Subject(); + var control = new FitnessMachineControl( + controlPoint, + _ => Task.CompletedTask, + responseTimeout: TimeSpan.FromSeconds(5), + scheduler); + + var executeTask = control.Execute(new ControlRequest(EControlOpCode.RequestControl, [])); + + scheduler.AdvanceBy(TimeSpan.FromSeconds(5).Ticks); + + await Assert.ThrowsAsync(() => executeTask); + } + + [Fact] + public async Task Execute_ValidResponseIndicated_ReturnsResponse() + { + var controlPoint = new Subject(); + var control = new FitnessMachineControl( + controlPoint, + _ => Task.CompletedTask, + responseTimeout: TimeSpan.FromSeconds(5)); + + var executeTask = control.Execute(new ControlRequest(EControlOpCode.RequestControl, [])); + + controlPoint.OnNext([0x80, 0x00, 0x01]); + + var response = await executeTask; + Assert.Equal(EControlOpCode.RequestControl, response.RequestedOpCode); + Assert.Equal(EControlResultCode.Success, response.ResultCode); + } + + [Fact] + public async Task Execute_NonSuccessResultCode_ThrowsControlRequestException() + { + var controlPoint = new Subject(); + var control = new FitnessMachineControl( + controlPoint, + _ => Task.CompletedTask, + responseTimeout: TimeSpan.FromSeconds(5)); + + var executeTask = control.Execute(new ControlRequest(EControlOpCode.RequestControl, [])); + + controlPoint.OnNext([0x80, 0x00, 0x03]); + + await Assert.ThrowsAsync(() => executeTask); + } + + [Fact] + public async Task Execute_WriteThrows_WrapsInControlRequestException() + { + var control = new FitnessMachineControl( + Observable.Never(), + _ => throw new Exception("simulated ATT error"), + responseTimeout: TimeSpan.FromSeconds(5)); + + await Assert.ThrowsAsync(() => + control.Execute(new ControlRequest(EControlOpCode.RequestControl, []))); + } +} \ No newline at end of file diff --git a/FTMS.NET.Tests/Data/FitnessMachineDataReaderTests.cs b/FTMS.NET.Tests/Data/FitnessMachineDataReaderTests.cs new file mode 100644 index 0000000..5c38114 --- /dev/null +++ b/FTMS.NET.Tests/Data/FitnessMachineDataReaderTests.cs @@ -0,0 +1,32 @@ +namespace FTMS.NET.Tests.Data; + +using FTMS.NET.Data; +using System.Collections.Generic; +using System.Linq; + +public sealed class FitnessMachineDataReaderTests +{ + /// + /// Tests that a treadmill frame with Heart Rate present (bit 8 of the 2-octet flag field) + /// is parsed correctly using standard bit numbering. + /// Expected: Both Instantaneous Speed and Heart Rate are parsed from the frame. + /// + [Fact] + public void Read_TreadmillFrameWithHeartRate_ParsesHeartRate() + { + // Flags: bit 0 = 0 (Instantaneous Speed present), bit 8 = 1 (Heart Rate present) + // LE: byte0 = 0x00, byte1 = 0x01 + byte[] frame = [0x00, 0x01, 0xD2, 0x04, 0x48]; // speed raw 1234, HR 72 bpm + + FitnessMachineDataReader reader = new( + SingleFrameStrategies.GetFor(EFitnessMachineType.Threadmill)); + + List values = reader.Read(frame).ToList(); + + IFitnessMachineValue speed = values.Single(v => v.Uuid == FtmsUuids.InstantaneousSpeed); + Assert.Equal(12.34, speed.Value, precision: 2); + + IFitnessMachineValue heartRate = values.Single(v => v.Uuid == FtmsUuids.HeartRate); + Assert.Equal(72, heartRate.Value); + } +} \ No newline at end of file diff --git a/FTMS.NET.Tests/FTMS.NET.Tests.csproj b/FTMS.NET.Tests/FTMS.NET.Tests.csproj index e46964d..df59ce7 100644 --- a/FTMS.NET.Tests/FTMS.NET.Tests.csproj +++ b/FTMS.NET.Tests/FTMS.NET.Tests.csproj @@ -10,6 +10,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/FTMS.NET.Tests/FitnessMachineServiceFactory.Tests.cs b/FTMS.NET.Tests/FitnessMachineServiceFactory.Tests.cs new file mode 100644 index 0000000..81a6816 --- /dev/null +++ b/FTMS.NET.Tests/FitnessMachineServiceFactory.Tests.cs @@ -0,0 +1,47 @@ +namespace FTMS.NET.Tests; + +using System.Reactive; +using System.Reactive.Linq; + +public sealed class FitnessMachineServiceFactoryTests +{ + [Fact] + public async Task ReadFitnessMachineFeaturesAsync_SpeedTargetSettingInByte4_ReportsSupported() + { + byte[] value = new byte[8]; + value[4] = 0x01; // Target Setting Features bit 0 -> Speed Target Setting Supported + + var features = await new FakeConnection(value).ReadFitnessMachineFeaturesAsync(); + + Assert.True(features.SpeedTargetSettingSupported); + Assert.False(features.AverageSpeedSupported); + } + + [Fact] + public async Task ReadFitnessMachineFeaturesAsync_DistanceTargetSettingInByte5_ReportsSupported() + { + byte[] value = new byte[8]; + value[5] = 0x01; // Target Setting Features bit 8 -> Targeted Distance Configuration Supported + + var features = await new FakeConnection(value).ReadFitnessMachineFeaturesAsync(); + + Assert.True(features.TargetedDistanceConfigurationSupported); + Assert.False(features.SpeedTargetSettingSupported); + } + + private sealed class FakeCharacteristic(byte[] value) : IFitnessMachineCharacteristic + { + public Guid Id { get; } = Guid.NewGuid(); + public Task ReadValueAsync() => Task.FromResult(value); + public Task WriteValueAsync(byte[] value) => Task.CompletedTask; + public IObservable ObserveValue() => Observable.Empty(); + } + + private sealed class FakeConnection(byte[] featureValue) : IFitnessMachineServiceConnection + { + public byte[] ServiceData { get; } = []; + public Task GetCharacteristicAsync(Guid id) + => Task.FromResult( + id == FtmsUuids.Feature ? new FakeCharacteristic(featureValue) : null); + } +} \ No newline at end of file diff --git a/FTMS.NET.Tests/FtmsUuids.Tests.cs b/FTMS.NET.Tests/FtmsUuids.Tests.cs index 3e941c9..8507662 100644 --- a/FTMS.NET.Tests/FtmsUuids.Tests.cs +++ b/FTMS.NET.Tests/FtmsUuids.Tests.cs @@ -1,62 +1,62 @@ -namespace FTMS.NET.Tests; - -using System; - -public sealed class FtmsUuids_Tests -{ - public static readonly TheoryData UuidTestData = new() - { - { FtmsUuids.Service, nameof(FtmsUuids.Service) }, - { FtmsUuids.Feature, nameof(FtmsUuids.Feature) }, - { FtmsUuids.MachineState, nameof(FtmsUuids.MachineState) }, - { FtmsUuids.TrainingState, nameof(FtmsUuids.TrainingState) }, - { FtmsUuids.ControlPoint, nameof(FtmsUuids.ControlPoint) }, - { FtmsUuids.SupportedSpeedRange, nameof(FtmsUuids.SupportedSpeedRange) }, - { FtmsUuids.SupportedInclinationRange, nameof(FtmsUuids.SupportedInclinationRange) }, - { FtmsUuids.SupportedResistanceLevelRange, nameof(FtmsUuids.SupportedResistanceLevelRange) }, - { FtmsUuids.SupportedPowerRange, nameof(FtmsUuids.SupportedPowerRange) }, - { FtmsUuids.SupportedHeartRateRange, nameof(FtmsUuids.SupportedHeartRateRange) }, - { FtmsUuids.TreadmillData, nameof(FtmsUuids.TreadmillData) }, - { FtmsUuids.CrossTrainerData, nameof(FtmsUuids.CrossTrainerData) }, - { FtmsUuids.StepClimberData, nameof(FtmsUuids.StepClimberData) }, - { FtmsUuids.StairClimberData, nameof(FtmsUuids.StairClimberData) }, - { FtmsUuids.RowerData, nameof(FtmsUuids.RowerData) }, - { FtmsUuids.IndoorBikeData, nameof(FtmsUuids.IndoorBikeData) }, - - // Indoor Bike specific - { FtmsUuids.InstantaneousSpeed, nameof(FtmsUuids.InstantaneousSpeed) }, - { FtmsUuids.AverageSpeed, nameof(FtmsUuids.AverageSpeed) }, - { FtmsUuids.InstantaneousCadence, nameof(FtmsUuids.InstantaneousCadence) }, - { FtmsUuids.AverageCadence, nameof(FtmsUuids.AverageCadence) }, - { FtmsUuids.TotalDistance, nameof(FtmsUuids.TotalDistance) }, - { FtmsUuids.ResistantLevel, nameof(FtmsUuids.ResistantLevel) }, - { FtmsUuids.InstantaneousPower, nameof(FtmsUuids.InstantaneousPower) }, - { FtmsUuids.AveragePower, nameof(FtmsUuids.AveragePower) }, - { FtmsUuids.TotalEnergy, nameof(FtmsUuids.TotalEnergy) }, - { FtmsUuids.EnergyPerHour, nameof(FtmsUuids.EnergyPerHour) }, - { FtmsUuids.EnergyPerMinute, nameof(FtmsUuids.EnergyPerMinute) }, - }; - - [Theory] - [MemberData(nameof(UuidTestData))] - public void FtmsUuids_GetName_ReturnsCorrectName(Guid uuid, string nameOfUuid) - { - var name = FtmsUuids.GetName(uuid); - Assert.Equal(nameOfUuid, name); - } - - [Fact] - public void FtmsUuids_GetName_WithUnknownUuid_ReturnsEmptyString() - { - var unknownUuid = Guid.NewGuid(); - var name = FtmsUuids.GetName(unknownUuid); - Assert.Equal(string.Empty, name); - } - - [Fact] - public void FtmsUuids_GetName_WithEmptyGuid_ReturnsEmptyString() - { - var name = FtmsUuids.GetName(Guid.Empty); - Assert.Equal(string.Empty, name); - } -} +namespace FTMS.NET.Tests; + +using System; + +public sealed class FtmsUuids_Tests +{ + public static readonly TheoryData UuidTestData = new() + { + { FtmsUuids.Service, nameof(FtmsUuids.Service) }, + { FtmsUuids.Feature, nameof(FtmsUuids.Feature) }, + { FtmsUuids.MachineState, nameof(FtmsUuids.MachineState) }, + { FtmsUuids.TrainingState, nameof(FtmsUuids.TrainingState) }, + { FtmsUuids.ControlPoint, nameof(FtmsUuids.ControlPoint) }, + { FtmsUuids.SupportedSpeedRange, nameof(FtmsUuids.SupportedSpeedRange) }, + { FtmsUuids.SupportedInclinationRange, nameof(FtmsUuids.SupportedInclinationRange) }, + { FtmsUuids.SupportedResistanceLevelRange, nameof(FtmsUuids.SupportedResistanceLevelRange) }, + { FtmsUuids.SupportedPowerRange, nameof(FtmsUuids.SupportedPowerRange) }, + { FtmsUuids.SupportedHeartRateRange, nameof(FtmsUuids.SupportedHeartRateRange) }, + { FtmsUuids.TreadmillData, nameof(FtmsUuids.TreadmillData) }, + { FtmsUuids.CrossTrainerData, nameof(FtmsUuids.CrossTrainerData) }, + { FtmsUuids.StepClimberData, nameof(FtmsUuids.StepClimberData) }, + { FtmsUuids.StairClimberData, nameof(FtmsUuids.StairClimberData) }, + { FtmsUuids.RowerData, nameof(FtmsUuids.RowerData) }, + { FtmsUuids.IndoorBikeData, nameof(FtmsUuids.IndoorBikeData) }, + + // Indoor Bike specific + { FtmsUuids.InstantaneousSpeed, nameof(FtmsUuids.InstantaneousSpeed) }, + { FtmsUuids.AverageSpeed, nameof(FtmsUuids.AverageSpeed) }, + { FtmsUuids.InstantaneousCadence, nameof(FtmsUuids.InstantaneousCadence) }, + { FtmsUuids.AverageCadence, nameof(FtmsUuids.AverageCadence) }, + { FtmsUuids.TotalDistance, nameof(FtmsUuids.TotalDistance) }, + { FtmsUuids.ResistantLevel, nameof(FtmsUuids.ResistantLevel) }, + { FtmsUuids.InstantaneousPower, nameof(FtmsUuids.InstantaneousPower) }, + { FtmsUuids.AveragePower, nameof(FtmsUuids.AveragePower) }, + { FtmsUuids.TotalEnergy, nameof(FtmsUuids.TotalEnergy) }, + { FtmsUuids.EnergyPerHour, nameof(FtmsUuids.EnergyPerHour) }, + { FtmsUuids.EnergyPerMinute, nameof(FtmsUuids.EnergyPerMinute) }, + }; + + [Theory] + [MemberData(nameof(UuidTestData))] + public void FtmsUuids_GetName_ReturnsCorrectName(Guid uuid, string nameOfUuid) + { + var name = FtmsUuids.GetName(uuid); + Assert.Equal(nameOfUuid, name); + } + + [Fact] + public void FtmsUuids_GetName_WithUnknownUuid_ReturnsEmptyString() + { + var unknownUuid = Guid.NewGuid(); + var name = FtmsUuids.GetName(unknownUuid); + Assert.Equal(string.Empty, name); + } + + [Fact] + public void FtmsUuids_GetName_WithEmptyGuid_ReturnsEmptyString() + { + var name = FtmsUuids.GetName(Guid.Empty); + Assert.Equal(string.Empty, name); + } +} \ No newline at end of file diff --git a/FTMS.NET.Tests/State/FitnessMachineStateParameterFactory.Tests.cs b/FTMS.NET.Tests/State/FitnessMachineStateParameterFactory.Tests.cs new file mode 100644 index 0000000..1be1915 --- /dev/null +++ b/FTMS.NET.Tests/State/FitnessMachineStateParameterFactory.Tests.cs @@ -0,0 +1,75 @@ +namespace FTMS.NET.Tests.State; + +using FTMS.NET.State; +using System.Collections.Generic; +using System.Linq; + +public sealed class FitnessMachineStateParameterFactoryTests +{ + /// + /// Tests that a negative target incline (SINT16) is decoded as a negative percent value. + /// + [Fact] + public void ReadParameters_TargetInclineChanged_NegativeIncline_ReturnsNegativePercent() + { + // -20 as SINT16, little-endian + byte[] rawData = [0xEC, 0xFF]; + + var parameters = FitnessMachineStateParameterFactory + .ReadParameters(EStateOpCode.TargetInclineChanged, rawData) + .Cast(); + + var parameter = Assert.Single(parameters); + Assert.Equal(FitnessMachineUnit.Percent, parameter.Unit); + Assert.Equal(-2.0, parameter.Value, precision: 1); // -20 * 0.1 + } + + /// + /// Tests that a targeted distance larger than 65,535 m (UINT24) is decoded fully. + /// + [Fact] + public void ReadParameters_TargetedDistanceChanged_ThreeByteDistance_ReturnsFullValue() + { + // UINT24 little-endian: 0x0F4240 = 1,000,000 meters + byte[] rawData = [0x40, 0x42, 0x0F]; + + var parameters = FitnessMachineStateParameterFactory + .ReadParameters(EStateOpCode.TargetedDistanceChanged, rawData) + .Cast(); + + var parameter = Assert.Single(parameters); + Assert.Equal(FitnessMachineUnit.Meters, parameter.Unit); + Assert.Equal(1_000_000.0, parameter.Value); + } + + /// + /// Tests that target incline decodes positive, zero and negative SINT16 values correctly. + /// + [Theory] + [InlineData(new byte[] { 0x14, 0x00 }, 2.0)] // +20 -> 2.0 % + [InlineData(new byte[] { 0x00, 0x00 }, 0.0)] + [InlineData(new byte[] { 0xFF, 0xFF }, -0.1)] // -1 -> -0.1 % + public void ReadParameters_TargetInclineChanged_PositiveAndNegative_ReturnsCorrectPercent(byte[] rawData, double expected) + { + var parameters = FitnessMachineStateParameterFactory + .ReadParameters(EStateOpCode.TargetInclineChanged, rawData) + .Cast(); + + Assert.Equal(expected, Assert.Single(parameters).Value, precision: 1); + } + + /// + /// Tests that targeted distance decodes three-byte UINT24 values, including the maximum. + /// + [Theory] + [InlineData(new byte[] { 0x64, 0x00, 0x00 }, 100.0)] + [InlineData(new byte[] { 0xFF, 0xFF, 0xFF }, 16_777_215.0)] + public void ReadParameters_TargetedDistanceChanged_EncodesThreeByteValue(byte[] rawData, double expected) + { + var parameters = FitnessMachineStateParameterFactory + .ReadParameters(EStateOpCode.TargetedDistanceChanged, rawData) + .Cast(); + + Assert.Equal(expected, Assert.Single(parameters).Value); + } +} \ No newline at end of file diff --git a/FTMS.NET.Tests/Utils/ByteExtensionsTests.cs b/FTMS.NET.Tests/Utils/ByteExtensionsTests.cs index 387d791..04870f9 100644 --- a/FTMS.NET.Tests/Utils/ByteExtensionsTests.cs +++ b/FTMS.NET.Tests/Utils/ByteExtensionsTests.cs @@ -1,680 +1,720 @@ -namespace FTMS.NET.Tests.Utils; - -using FTMS.NET.Utils; - -public sealed class ByteExtensionsTests -{ - /// - /// Tests that IsBitSet correctly identifies set and unset bits for all positions 0-7 - /// in a byte with a known bit pattern (0b10100101). - /// Expected: Returns true for positions where bit is 1, false where bit is 0. - /// - [Fact] - public void IsBitSet_ByteWithMixedBitPattern_ReturnsCorrectStateForEachPosition() - { - // Arrange - // 0b10100101 == 0xA5 == 165 - byte b = 0b1010_0101; - - // Act & Assert - Assert.True(b.IsBitSet(0)); // LSB = 1 - Assert.False(b.IsBitSet(1)); // bit 1 = 0 - Assert.True(b.IsBitSet(2)); // bit 2 = 1 - Assert.False(b.IsBitSet(3)); // bit 3 = 0 - Assert.False(b.IsBitSet(4)); // bit 4 = 0 - Assert.True(b.IsBitSet(5)); // bit 5 = 1 - Assert.False(b.IsBitSet(6)); // bit 6 = 0 - Assert.True(b.IsBitSet(7)); // MSB = 1 - } - - /// - /// Tests that IsBitSet returns false for all bit positions when the byte value is 0x00 (all bits unset). - /// Expected: Returns false for all positions 0-7. - /// - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - [InlineData(6)] - [InlineData(7)] - public void IsBitSet_ByteWithAllBitsUnset_ReturnsFalseForAllPositions(int pos) - { - // Arrange - byte b = 0x00; - - // Act - bool result = b.IsBitSet(pos); - - // Assert - Assert.False(result); - } - - /// - /// Tests that IsBitSet returns true for all bit positions when the byte value is 0xFF (all bits set). - /// Expected: Returns true for all positions 0-7. - /// - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - [InlineData(6)] - [InlineData(7)] - public void IsBitSet_ByteWithAllBitsSet_ReturnsTrueForAllPositions(int pos) - { - // Arrange - byte b = 0xFF; - - // Act - bool result = b.IsBitSet(pos); - - // Assert - Assert.True(result); - } - - /// - /// Tests that IsBitSet correctly identifies a single set bit at various positions. - /// Expected: Returns true only for the position where the bit is set, false for all others. - /// - [Theory] - [InlineData(0, 0b0000_0001)] - [InlineData(1, 0b0000_0010)] - [InlineData(2, 0b0000_0100)] - [InlineData(3, 0b0000_1000)] - [InlineData(4, 0b0001_0000)] - [InlineData(5, 0b0010_0000)] - [InlineData(6, 0b0100_0000)] - [InlineData(7, 0b1000_0000)] - public void IsBitSet_ByteWithSingleBitSet_ReturnsTrueOnlyForThatPosition(int pos, byte byteValue) - { - // Arrange - byte b = byteValue; - - // Act - bool result = b.IsBitSet(pos); - - // Assert - Assert.True(result); - } - - /// - /// Tests that IsBitSet returns false when checking a position that is not set in a byte with a single bit set. - /// Expected: Returns false for all positions except the one that is set. - /// - [Theory] - [InlineData(0, 0b0000_0010)] // bit 0 unset, bit 1 set - [InlineData(1, 0b0000_0001)] // bit 1 unset, bit 0 set - [InlineData(3, 0b1000_0000)] // bit 3 unset, bit 7 set - [InlineData(7, 0b0000_0001)] // bit 7 unset, bit 0 set - public void IsBitSet_ByteWithSingleBitSet_ReturnsFalseForOtherPositions(int pos, byte byteValue) - { - // Arrange - byte b = byteValue; - - // Act - bool result = b.IsBitSet(pos); - - // Assert - Assert.False(result); - } - - /// - /// Tests behavior of IsBitSet when position is negative. - /// Expected: Due to bit shift behavior, negative positions may produce unexpected results or exceptions. - /// This test documents the actual behavior. - /// - [Theory] - [InlineData(-1)] - [InlineData(-8)] - public void IsBitSet_NegativePosition_ProducesResult(int pos) - { - // Arrange - byte b = 0xFF; - - // Act & Assert - Assert.Throws(() => b.IsBitSet(pos)); - } - - /// - /// Tests behavior of IsBitSet when position is greater than 7 (beyond byte boundary). - /// Expected: Positions beyond 7 should produce results based on bit shift wrapping behavior. - /// This test documents the actual behavior for out-of-range positions. - /// - [Theory] - [InlineData(8)] - [InlineData(9)] - [InlineData(16)] - [InlineData(31)] - public void IsBitSet_PositionBeyondByteBoundary_ProducesResult(int pos) - { - // Arrange - byte b = 0b1010_0101; - - // Act & Assert - Assert.Throws(() => b.IsBitSet(pos)); - } - - /// - /// Tests behavior of IsBitSet with extreme position values. - /// Expected: Documents behavior with int.MaxValue as position. - /// - [Fact] - public void IsBitSet_PositionMaxValue_ProducesResult() - { - // Arrange - byte b = 0xFF; - int pos = int.MaxValue; - - // Act - Assert.Throws(() => b.IsBitSet(pos)); - } - - /// - /// Tests behavior of IsBitSet with extreme negative position value. - /// Expected: Documents behavior with int.MinValue as position. - /// - [Fact] - public void IsBitSet_PositionMinValue_ProducesResult() - { - // Arrange - byte b = 0xFF; - int pos = int.MinValue; - - // Act - Assert.Throws(() => b.IsBitSet(pos)); - } - - /// - /// Tests that IsBitSet correctly handles boundary byte values (byte.MinValue and byte.MaxValue). - /// Expected: byte.MinValue (0) should return false for all positions, byte.MaxValue (255) should return true. - /// - [Theory] - [InlineData(byte.MinValue, 0, false)] - [InlineData(byte.MinValue, 7, false)] - [InlineData(byte.MaxValue, 0, true)] - [InlineData(byte.MaxValue, 7, true)] - public void IsBitSet_BoundaryByteValues_ReturnsExpectedResult(byte b, int pos, bool expected) - { - // Act - Assert.Equal(expected, b.IsBitSet(pos)); - } - - /// - /// Tests that IsBitSet returns false when the ReadOnlySpan is empty, regardless of position. - /// Input: Empty span, position 0. - /// Expected: Returns false. - /// - [Fact] - public void IsBitSet_EmptySpan_ReturnsFalse() - { - // Arrange - ReadOnlySpan data = []; - int pos = 0; - - // Act - AssertThrowsIndexOutOfRange(data, pos); - } - - /// - /// Tests that IsBitSet returns false when position is far beyond the span length. - /// Input: Single byte span, position 100. - /// Expected: Returns false. - /// - [Fact] - public void IsBitSet_PositionBeyondLength_ReturnsFalse() - { - // Arrange - byte[] backing = [0b1111_1111]; - ReadOnlySpan data = new(backing); - int pos = 100; - - // Act - AssertThrowsIndexOutOfRange(data, pos); - } - - /// - /// Tests that IsBitSet returns false when position is at exact boundary where byteIndex equals data.Length. - /// Input: Single byte span, position 7 (byteIndex = 7/7 = 1, which equals length). - /// Expected: Returns false. - /// - [Fact] - public void IsBitSet_PositionAtExactBoundary_ReturnsFalse() - { - // Arrange - byte[] backing = [0b1111_1111]; - ReadOnlySpan data = new(backing); - int pos = 7; // byteIndex = 1, equals data.Length - - // Act - AssertThrowsIndexOutOfRange(data, pos); - } - - /// - /// Tests that IsBitSet returns false when position is int.MaxValue. - /// Input: Single byte span, position int.MaxValue. - /// Expected: Returns false. - /// - [Fact] - public void IsBitSet_PositionIntMaxValue_ReturnsFalse() - { - // Arrange - byte[] backing = [0xFF]; - ReadOnlySpan data = new(backing); - int pos = int.MaxValue; - - // Act - AssertThrowsIndexOutOfRange(data, pos); - } - - /// - /// Tests that IsBitSet handles negative positions correctly. - /// Input: Single byte span with all bits set, position -1. - /// Expected: Behavior depends on implementation (negative byteIndex/bitIndex). - /// Note: -1 / 7 = 0, -1 % 7 = -1, which will call byte.IsBitSet(-1). - /// - [Fact] - public void IsBitSet_NegativePosition_ReturnsExpectedResult() - { - // Arrange - byte[] backing = [0b1111_1111]; - ReadOnlySpan data = new(backing); - int pos = -1; - - // Act - AssertThrowsIndexOutOfRange(data, pos); - } - - /// - /// Tests that IsBitSet handles int.MinValue position correctly. - /// Input: Single byte span, position int.MinValue. - /// Expected: Returns false (extreme negative value). - /// - [Fact] - public void IsBitSet_PositionIntMinValue_ReturnsFalse() - { - // Arrange - byte[] backing = [0xFF]; - ReadOnlySpan data = new(backing); - int pos = int.MinValue; - - // Act - AssertThrowsIndexOutOfRange(data, pos); - } - - /// - /// Tests that IsBitSet correctly checks bits in the first byte using 7-bit packing. - /// Input: Various positions 0-6 mapping to first byte. - /// Expected: Returns true/false based on bit pattern. - /// - [Theory] - [InlineData(0, false)] // Bit 0 of 0b0100_0000 is not set - [InlineData(1, false)] // Bit 1 is not set - [InlineData(2, false)] // Bit 2 is not set - [InlineData(3, false)] // Bit 3 is not set - [InlineData(4, false)] // Bit 4 is not set - [InlineData(5, false)] // Bit 5 is not set - [InlineData(6, true)] // Bit 6 is set - public void IsBitSet_PositionsInFirstByte_ReturnsCorrectResult(int pos, bool expected) - { - // Arrange - byte[] backing = [0b0100_0000]; // Only bit 6 is set - ReadOnlySpan data = new(backing); - Assert.Equal(expected, data.IsBitSet(pos)); - } - - /// - /// Tests that IsBitSet correctly uses 7-bit packing to map positions across multiple bytes. - /// Input: Positions 6 and 7 to verify transition from first to second byte. - /// Expected: Position 6 maps to byte 0 bit 6, position 7 maps to byte 1 bit 0. - /// - [Fact] - public void IsBitSet_PositionsCrossingByteBoundary_MapsCorrectly() - { - // Arrange - // First byte: bit 6 set (0b0100_0000) - // Second byte: bit 0 set (0b0000_0001) - byte[] backing = [0b0100_0000, 0b0000_0001]; - ReadOnlySpan data = new(backing); - - // Act & Assert - // pos = 6 -> byteIndex = 0, bitIndex = 6 -> first byte, bit 6 - Assert.True(data.IsBitSet(6)); - - // pos = 7 -> byteIndex = 1, bitIndex = 0 -> second byte, bit 0 - Assert.True(data.IsBitSet(7)); - - // pos = 0 -> byteIndex = 0, bitIndex = 0 -> first byte, bit 0 (not set) - Assert.False(data.IsBitSet(0)); - - // pos = 8 -> byteIndex = 1, bitIndex = 1 -> second byte, bit 1 (not set) - Assert.False(data.IsBitSet(8)); - } - - /// - /// Tests that IsBitSet correctly handles positions mapping to the second byte. - /// Input: Positions 7-13 mapping to second byte using 7-bit packing. - /// Expected: Returns correct bit values from second byte. - /// - [Theory] - [InlineData(7, true)] // pos 7 -> byte 1, bit 0 (set) - [InlineData(8, false)] // pos 8 -> byte 1, bit 1 (not set) - [InlineData(9, true)] // pos 9 -> byte 1, bit 2 (set) - [InlineData(10, false)] // pos 10 -> byte 1, bit 3 (not set) - [InlineData(11, true)] // pos 11 -> byte 1, bit 4 (set) - [InlineData(12, false)] // pos 12 -> byte 1, bit 5 (not set) - [InlineData(13, true)] // pos 13 -> byte 1, bit 6 (set) - public void IsBitSet_PositionsInSecondByte_ReturnsCorrectResult(int pos, bool expected) - { - // Arrange - // First byte: all zeros - // Second byte: 0b0101_0101 (bits 0, 2, 4, 6 set) - byte[] backing = [0b0000_0000, 0b0101_0101]; - ReadOnlySpan data = new(backing); - Assert.Equal(expected, data.IsBitSet(pos)); - } - - /// - /// Tests that IsBitSet works correctly with all bits set in multiple bytes. - /// Input: Two bytes with all bits set, various positions. - /// Expected: All positions within valid range return true. - /// - [Theory] - [InlineData(0, true)] - [InlineData(3, true)] - [InlineData(6, true)] - [InlineData(7, true)] - [InlineData(10, true)] - [InlineData(13, true)] - public void IsBitSet_AllBitsSet_ReturnsTrue(int pos, bool expected) - { - // Arrange - byte[] backing = [0xFF, 0xFF]; - ReadOnlySpan data = new(backing); - Assert.Equal(expected, data.IsBitSet(pos)); - } - - /// - /// Tests that IsBitSet works correctly with no bits set. - /// Input: Multiple bytes with all bits cleared, various positions. - /// Expected: All positions return false. - /// - [Theory] - [InlineData(0)] - [InlineData(3)] - [InlineData(6)] - [InlineData(7)] - [InlineData(10)] - [InlineData(13)] - public void IsBitSet_NoBitsSet_ReturnsFalse(int pos) - { - // Arrange - byte[] backing = [0x00, 0x00, 0x00]; - ReadOnlySpan data = new(backing); - Assert.False(data.IsBitSet(pos)); - } - - /// - /// Tests that IsBitSet returns false when position maps to third byte but only two bytes exist. - /// Input: Two byte span, position 14 (maps to third byte). - /// Expected: Returns false. - /// - [Fact] - public void IsBitSet_PositionMapsToNonExistentThirdByte_ReturnsFalse() - { - // Arrange - byte[] backing = [0xFF, 0xFF]; - ReadOnlySpan data = new(backing); - int pos = 14; // byteIndex = 14/7 = 2, but only indices 0-1 exist - - // Act - AssertThrowsIndexOutOfRange(data, pos); - } - - /// - /// Tests that IsBitSet handles position zero correctly with single byte. - /// Input: Single byte with specific pattern, position 0. - /// Expected: Returns correct bit value at position 0. - /// - [Theory] - [InlineData(0b0000_0001, true)] // Bit 0 set - [InlineData(0b0000_0000, false)] // Bit 0 not set - [InlineData(0b1111_1110, false)] // Bit 0 not set - public void IsBitSet_PositionZero_ReturnsCorrectResult(byte byteValue, bool expected) - { - // Arrange - byte[] backing = [byteValue]; - ReadOnlySpan data = new(backing); - int pos = 0; - - // Act - bool result = data.IsBitSet(pos); - Assert.Equal(expected, result); - } - - /// - /// Tests that IsBitSet returns false when the position is out of bounds of the array. - /// Uses 7-bit packing, so position is mapped to byteIndex = pos / 7. - /// - [Theory] - [InlineData(7, 1)] // pos=7 maps to byteIndex=1, array has only 1 byte - [InlineData(8, 1)] // pos=8 maps to byteIndex=1, array has only 1 byte - [InlineData(14, 2)] // pos=14 maps to byteIndex=2, array has only 2 bytes - [InlineData(100, 3)] // pos=100 maps to byteIndex=14, array has only 3 bytes - [InlineData(int.MaxValue, 10)] // Very large position, array has 10 bytes - public void IsBitSet_PositionOutOfBounds_ReturnsFalse(int pos, int arrayLength) - { - // Arrange - byte[] data = new byte[arrayLength]; - - // Act - Assert.Throws(() => data.IsBitSet(pos)); - } - - /// - /// Tests that IsBitSet throws NullReferenceException when the array is null. - /// - [Fact] - public void IsBitSet_NullArray_ThrowsNullReferenceException() - { - // Arrange - byte[]? data = null; - - // Act & Assert - Assert.Throws(() => data!.IsBitSet(0)); - } - - /// - /// Tests that IsBitSet returns false for any valid position when the array is empty. - /// - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(10)] - [InlineData(int.MaxValue)] - public void IsBitSet_EmptyArray_ReturnsFalse(int pos) - { - // Arrange - byte[] data = []; - - // Act - Assert.Throws(() => data.IsBitSet(pos)); - } - - /// - /// Tests IsBitSet with various positions and bit patterns using 7-bit packing. - /// Verifies correct mapping: pos/7 gives byte index, pos%7 gives bit index within that byte. - /// - [Theory] - [InlineData(0, new byte[] { 0b0000_0001 }, true)] // pos=0: byte[0] bit 0 is set - [InlineData(0, new byte[] { 0b0000_0000 }, false)] // pos=0: byte[0] bit 0 is not set - [InlineData(1, new byte[] { 0b0000_0010 }, true)] // pos=1: byte[0] bit 1 is set - [InlineData(6, new byte[] { 0b0100_0000 }, true)] // pos=6: byte[0] bit 6 is set - [InlineData(6, new byte[] { 0b0000_0000 }, false)] // pos=6: byte[0] bit 6 is not set - [InlineData(7, new byte[] { 0b0000_0000, 0b0000_0001 }, true)] // pos=7: byte[1] bit 0 is set - [InlineData(7, new byte[] { 0b1111_1111, 0b0000_0000 }, false)] // pos=7: byte[1] bit 0 is not set - [InlineData(8, new byte[] { 0b0000_0000, 0b0000_0010 }, true)] // pos=8: byte[1] bit 1 is set - [InlineData(13, new byte[] { 0b0000_0000, 0b0100_0000 }, true)] // pos=13: byte[1] bit 6 is set - [InlineData(14, new byte[] { 0b0000_0000, 0b0000_0000, 0b0000_0001 }, true)] // pos=14: byte[2] bit 0 is set - public void IsBitSet_ValidPositionWithBitPattern_ReturnsExpectedResult(int pos, byte[] data, bool expectedResult) - { - // Act - Assert.Equal(expectedResult, data.IsBitSet(pos)); - } - - /// - /// Tests IsBitSet at byte boundary positions (where position transitions from one byte to the next). - /// With 7-bit packing: positions 6→7, 13→14, etc. are boundaries. - /// - [Fact] - public void IsBitSet_BoundaryPositions_MapsCorrectlyAcrossBytes() - { - // Arrange - // byte[0] has bit 6 set, byte[1] has bit 0 set, byte[2] has bit 6 set - byte[] data = [0b0100_0000, 0b0000_0001, 0b0100_0000]; - - // Act & Assert - Assert.True(data.IsBitSet(6)); - Assert.True(data.IsBitSet(7)); - Assert.False(data.IsBitSet(13)); - Assert.False(data.IsBitSet(14)); - Assert.True(data.IsBitSet(20)); - } - - /// - /// Tests IsBitSet with negative positions where byteIndex becomes negative, - /// which should throw IndexOutOfRangeException when accessing the array. - /// - [Theory] - [InlineData(-7)] - [InlineData(-8)] - [InlineData(-14)] - [InlineData(int.MinValue)] - public void IsBitSet_NegativePositionWithNegativeByteIndex_ThrowsIndexOutOfRangeException(int pos) - { - // Arrange - byte[] data = [0xFF]; - - // Act & Assert - Assert.Throws(() => data.IsBitSet(pos)); - } - - /// - /// Tests IsBitSet with negative positions in range [-6, -1] where byteIndex = 0 but bitIndex is negative. - /// The behavior depends on the byte.IsBitSet implementation with negative bit indices. - /// This tests the actual behavior of the bit shift operation with negative indices. - /// - [Theory] - [InlineData(-1)] - [InlineData(-2)] - [InlineData(-6)] - public void IsBitSet_NegativePositionWithZeroByteIndex_CallsByteIsBitSetWithNegativeBitIndex(int pos) - { - // Arrange - byte[] data = [0xFF]; - - // Act - Assert.Throws(() => data.IsBitSet(pos)); - } - - /// - /// Tests IsBitSet with position 0 on various byte patterns. - /// Position 0 maps to byte[0] bit 0 (LSB). - /// - [Theory] - [InlineData(0b0000_0001, true)] // LSB set - [InlineData(0b0000_0000, false)] // LSB not set - [InlineData(0b1111_1110, false)] // All bits except LSB set - [InlineData(0b1111_1111, true)] // All bits set - public void IsBitSet_Position0_ChecksFirstBitOfFirstByte(byte firstByte, bool expectedResult) - { - // Arrange - byte[] data = [firstByte]; - - // Act - bool result = data.IsBitSet(0); - - // Assert - Assert.Equal(expectedResult, result); - } - - /// - /// Tests IsBitSet with a comprehensive bit pattern across multiple bytes - /// to verify correct 7-bit packing logic throughout the array. - /// - [Fact] - public void IsBitSet_MultipleBytes_Uses7BitPackingCorrectly() - { - // Arrange - // Create a pattern where specific positions are set - byte[] data = - [ - 0b0101_0101, // byte[0]: bits 0,2,4,6 set - 0b1010_1010, // byte[1]: bits 1,3,5,7 set - 0b0000_1111 // byte[2]: bits 0,1,2,3 set - ]; - - // Act & Assert - // Positions 0-6 map to byte[0] - Assert.True(data.IsBitSet(0)); // bit 0 set - Assert.False(data.IsBitSet(1)); // bit 1 not set - Assert.True(data.IsBitSet(2)); // bit 2 set - Assert.False(data.IsBitSet(3)); // bit 3 not set - Assert.True(data.IsBitSet(4)); // bit 4 set - Assert.False(data.IsBitSet(5)); // bit 5 not set - Assert.True(data.IsBitSet(6)); // bit 6 set - - // Positions 7-13 map to byte[1] - Assert.False(data.IsBitSet(7)); // bit 0 not set - Assert.True(data.IsBitSet(8)); // bit 1 set - Assert.False(data.IsBitSet(9)); // bit 2 not set - Assert.True(data.IsBitSet(10)); // bit 3 set - Assert.False(data.IsBitSet(11)); // bit 4 not set - Assert.True(data.IsBitSet(12)); // bit 5 set - Assert.False(data.IsBitSet(13)); // bit 6 not set - - // Positions 14-20 map to byte[2] - Assert.True(data.IsBitSet(14)); // bit 0 set - Assert.True(data.IsBitSet(15)); // bit 1 set - Assert.True(data.IsBitSet(16)); // bit 2 set - Assert.True(data.IsBitSet(17)); // bit 3 set - Assert.False(data.IsBitSet(18)); // bit 4 not set - Assert.False(data.IsBitSet(19)); // bit 5 not set - Assert.False(data.IsBitSet(20)); // bit 6 not set - } - - /// - /// Tests IsBitSet with position exactly at the upper boundary that maps to the last valid byte. - /// Verifies behavior when position maps exactly to the last byte's last usable bit (bit 6). - /// - [Fact] - public void IsBitSet_PositionAtExactArrayBoundary_ReturnsCorrectResult() - { - // Arrange - // Array with 3 bytes, so valid positions are 0-20 - // Position 20 = byteIndex 2, bitIndex 6 (last valid position) - byte[] data = [0x00, 0x00, 0b0100_0000]; - - // Act & Assert - Assert.True(data.IsBitSet(20)); // Exactly at boundary, bit is set - AssertThrowsIndexOutOfRange(data, 21); // Beyond boundary, throws - } - - // Helper for ReadOnlySpan exception assertion - private static void AssertThrowsIndexOutOfRange(ReadOnlySpan span, int pos) - { - try - { - span.IsBitSet(pos); - Assert.Fail("Expected IndexOutOfRangeException was not thrown."); - } - catch (IndexOutOfRangeException) - { - // Expected - } - } +namespace FTMS.NET.Tests.Utils; + +using FTMS.NET.Utils; + +public sealed class ByteExtensionsTests +{ + /// + /// Tests that IsBitSet correctly identifies set and unset bits for all positions 0-7 + /// in a byte with a known bit pattern (0b10100101). + /// Expected: Returns true for positions where bit is 1, false where bit is 0. + /// + [Fact] + public void IsBitSet_ByteWithMixedBitPattern_ReturnsCorrectStateForEachPosition() + { + // Arrange + // 0b10100101 == 0xA5 == 165 + byte b = 0b1010_0101; + + // Act & Assert + Assert.True(b.IsBitSet(0)); // LSB = 1 + Assert.False(b.IsBitSet(1)); // bit 1 = 0 + Assert.True(b.IsBitSet(2)); // bit 2 = 1 + Assert.False(b.IsBitSet(3)); // bit 3 = 0 + Assert.False(b.IsBitSet(4)); // bit 4 = 0 + Assert.True(b.IsBitSet(5)); // bit 5 = 1 + Assert.False(b.IsBitSet(6)); // bit 6 = 0 + Assert.True(b.IsBitSet(7)); // MSB = 1 + } + + /// + /// Tests that IsBitSet returns false for all bit positions when the byte value is 0x00 (all bits unset). + /// Expected: Returns false for all positions 0-7. + /// + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + public void IsBitSet_ByteWithAllBitsUnset_ReturnsFalseForAllPositions(int pos) + { + // Arrange + byte b = 0x00; + + // Act + bool result = b.IsBitSet(pos); + + // Assert + Assert.False(result); + } + + /// + /// Tests that IsBitSet returns true for all bit positions when the byte value is 0xFF (all bits set). + /// Expected: Returns true for all positions 0-7. + /// + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + public void IsBitSet_ByteWithAllBitsSet_ReturnsTrueForAllPositions(int pos) + { + // Arrange + byte b = 0xFF; + + // Act + bool result = b.IsBitSet(pos); + + // Assert + Assert.True(result); + } + + /// + /// Tests that IsBitSet correctly identifies a single set bit at various positions. + /// Expected: Returns true only for the position where the bit is set, false for all others. + /// + [Theory] + [InlineData(0, 0b0000_0001)] + [InlineData(1, 0b0000_0010)] + [InlineData(2, 0b0000_0100)] + [InlineData(3, 0b0000_1000)] + [InlineData(4, 0b0001_0000)] + [InlineData(5, 0b0010_0000)] + [InlineData(6, 0b0100_0000)] + [InlineData(7, 0b1000_0000)] + public void IsBitSet_ByteWithSingleBitSet_ReturnsTrueOnlyForThatPosition(int pos, byte byteValue) + { + // Arrange + byte b = byteValue; + + // Act + bool result = b.IsBitSet(pos); + + // Assert + Assert.True(result); + } + + /// + /// Tests that IsBitSet returns false when checking a position that is not set in a byte with a single bit set. + /// Expected: Returns false for all positions except the one that is set. + /// + [Theory] + [InlineData(0, 0b0000_0010)] // bit 0 unset, bit 1 set + [InlineData(1, 0b0000_0001)] // bit 1 unset, bit 0 set + [InlineData(3, 0b1000_0000)] // bit 3 unset, bit 7 set + [InlineData(7, 0b0000_0001)] // bit 7 unset, bit 0 set + public void IsBitSet_ByteWithSingleBitSet_ReturnsFalseForOtherPositions(int pos, byte byteValue) + { + // Arrange + byte b = byteValue; + + // Act + bool result = b.IsBitSet(pos); + + // Assert + Assert.False(result); + } + + /// + /// Tests behavior of IsBitSet when position is negative. + /// Expected: Negative positions throw IndexOutOfRangeException. + /// + [Theory] + [InlineData(-1)] + [InlineData(-8)] + public void IsBitSet_NegativeBytePosition_ThrowsIndexOutOfRange(int pos) + { + // Arrange + byte b = 0xFF; + + // Act & Assert + Assert.Throws(() => b.IsBitSet(pos)); + } + + /// + /// Tests behavior of IsBitSet when position is greater than 7 (beyond byte boundary). + /// Expected: Positions beyond 7 throw IndexOutOfRangeException. + /// + [Theory] + [InlineData(8)] + [InlineData(9)] + [InlineData(16)] + [InlineData(31)] + public void IsBitSet_PositionBeyondByteBoundary_ThrowsIndexOutOfRange(int pos) + { + // Arrange + byte b = 0b1010_0101; + + // Act & Assert + Assert.Throws(() => b.IsBitSet(pos)); + } + + /// + /// Tests behavior of IsBitSet with extreme position values. + /// Expected: int.MaxValue throws IndexOutOfRangeException. + /// + [Fact] + public void IsBitSet_PositionMaxValue_ThrowsIndexOutOfRange() + { + // Arrange + byte b = 0xFF; + int pos = int.MaxValue; + + // Act + Assert.Throws(() => b.IsBitSet(pos)); + } + + /// + /// Tests behavior of IsBitSet with extreme negative position value. + /// Expected: int.MinValue throws IndexOutOfRangeException. + /// + [Fact] + public void IsBitSet_PositionMinValue_ThrowsIndexOutOfRange() + { + // Arrange + byte b = 0xFF; + int pos = int.MinValue; + + // Act + Assert.Throws(() => b.IsBitSet(pos)); + } + + /// + /// Tests that IsBitSet correctly handles boundary byte values (byte.MinValue and byte.MaxValue). + /// Expected: byte.MinValue (0) should return false for all positions, byte.MaxValue (255) should return true. + /// + [Theory] + [InlineData(byte.MinValue, 0, false)] + [InlineData(byte.MinValue, 7, false)] + [InlineData(byte.MaxValue, 0, true)] + [InlineData(byte.MaxValue, 7, true)] + public void IsBitSet_BoundaryByteValues_ReturnsExpectedResult(byte b, int pos, bool expected) + { + // Act + Assert.Equal(expected, b.IsBitSet(pos)); + } + + /// + /// Tests that IsBitSet throws IndexOutOfRangeException when the ReadOnlySpan is empty, regardless of position. + /// Input: Empty span, position 0. + /// + [Fact] + public void IsBitSet_EmptySpan_ThrowsIndexOutOfRange() + { + // Arrange + ReadOnlySpan data = []; + int pos = 0; + + // Act + AssertThrowsIndexOutOfRange(data, pos); + } + + /// + /// Tests that IsBitSet throws IndexOutOfRangeException when position is far beyond the span length. + /// Input: Single byte span, position 100. + /// + [Fact] + public void IsBitSet_PositionBeyondLength_ThrowsIndexOutOfRange() + { + // Arrange + byte[] backing = [0b1111_1111]; + ReadOnlySpan data = new(backing); + int pos = 100; + + // Act + AssertThrowsIndexOutOfRange(data, pos); + } + + /// + /// Tests that IsBitSet reads the MSB of a single byte at position 7 and throws beyond it. + /// Input: Single byte span, positions 7 and 8. + /// Expected: Position 7 returns true (byteIndex = 0, bitIndex = 7); position 8 throws (byteIndex = 1, equals length). + /// + [Fact] + public void IsBitSet_PositionAtExactBoundary_ReturnsCorrectResult() + { + // Arrange + byte[] backing = [0b1111_1111]; + ReadOnlySpan data = new(backing); + + // Act & Assert + Assert.True(data.IsBitSet(7)); // byte[0] bit 7 (MSB), valid + AssertThrowsIndexOutOfRange(data, 8); // byteIndex = 1, equals data.Length + } + + /// + /// Tests that IsBitSet throws IndexOutOfRangeException when position is int.MaxValue. + /// Input: Single byte span, position int.MaxValue. + /// + [Fact] + public void IsBitSet_PositionIntMaxValue_ThrowsIndexOutOfRange() + { + // Arrange + byte[] backing = [0xFF]; + ReadOnlySpan data = new(backing); + int pos = int.MaxValue; + + // Act + AssertThrowsIndexOutOfRange(data, pos); + } + + /// + /// Tests that IsBitSet handles negative positions correctly. + /// Input: Single byte span with all bits set, position -1. + /// Note: -1 / 8 = 0, -1 % 8 = -1, which will call byte.IsBitSet(-1) and throw. + /// + [Fact] + public void IsBitSet_SpanNegativePosition_ThrowsIndexOutOfRange() + { + // Arrange + byte[] backing = [0b1111_1111]; + ReadOnlySpan data = new(backing); + int pos = -1; + + // Act + AssertThrowsIndexOutOfRange(data, pos); + } + + /// + /// Tests that IsBitSet handles int.MinValue position correctly. + /// Input: Single byte span, position int.MinValue. + /// Expected: Throws IndexOutOfRangeException (extreme negative value). + /// + [Fact] + public void IsBitSet_PositionIntMinValue_ThrowsIndexOutOfRange() + { + // Arrange + byte[] backing = [0xFF]; + ReadOnlySpan data = new(backing); + int pos = int.MinValue; + + // Act + AssertThrowsIndexOutOfRange(data, pos); + } + + /// + /// Tests that IsBitSet correctly checks bits in the first byte using standard bit numbering. + /// Input: Various positions 0-6 mapping to the first byte. + /// Expected: Returns true/false based on bit pattern. + /// + [Theory] + [InlineData(0, false)] // Bit 0 of 0b0100_0000 is not set + [InlineData(1, false)] // Bit 1 is not set + [InlineData(2, false)] // Bit 2 is not set + [InlineData(3, false)] // Bit 3 is not set + [InlineData(4, false)] // Bit 4 is not set + [InlineData(5, false)] // Bit 5 is not set + [InlineData(6, true)] // Bit 6 is set + public void IsBitSet_PositionsInFirstByte_ReturnsCorrectResult(int pos, bool expected) + { + // Arrange + byte[] backing = [0b0100_0000]; // Only bit 6 is set + ReadOnlySpan data = new(backing); + Assert.Equal(expected, data.IsBitSet(pos)); + } + + /// + /// Tests that IsBitSet correctly uses standard bit numbering to map positions across multiple bytes. + /// Input: Positions 7 and 8 to verify transition from first to second byte. + /// Expected: Position 7 maps to byte 0 bit 7 (MSB), position 8 maps to byte 1 bit 0 (LSB). + /// + [Fact] + public void IsBitSet_PositionsCrossingByteBoundary_MapsCorrectly() + { + // Arrange + // First byte: bit 7 set (0b1000_0000) + // Second byte: bit 0 set (0b0000_0001) + byte[] backing = [0b1000_0000, 0b0000_0001]; + ReadOnlySpan data = new(backing); + + // Act & Assert + // pos = 7 -> byteIndex = 0, bitIndex = 7 -> first byte, bit 7 + Assert.True(data.IsBitSet(7)); + + // pos = 8 -> byteIndex = 1, bitIndex = 0 -> second byte, bit 0 + Assert.True(data.IsBitSet(8)); + + // pos = 6 -> byteIndex = 0, bitIndex = 6 -> first byte, bit 6 (not set) + Assert.False(data.IsBitSet(6)); + + // pos = 9 -> byteIndex = 1, bitIndex = 1 -> second byte, bit 1 (not set) + Assert.False(data.IsBitSet(9)); + } + + /// + /// Tests that IsBitSet correctly handles positions mapping to the second byte. + /// Input: Positions 8-15 mapping to second byte using standard bit numbering. + /// Expected: Returns correct bit values from second byte. + /// + [Theory] + [InlineData(8, true)] // pos 8 -> byte 1, bit 0 (set) + [InlineData(9, false)] // pos 9 -> byte 1, bit 1 (not set) + [InlineData(10, true)] // pos 10 -> byte 1, bit 2 (set) + [InlineData(11, false)] // pos 11 -> byte 1, bit 3 (not set) + [InlineData(12, true)] // pos 12 -> byte 1, bit 4 (set) + [InlineData(13, false)] // pos 13 -> byte 1, bit 5 (not set) + [InlineData(14, true)] // pos 14 -> byte 1, bit 6 (set) + [InlineData(15, false)] // pos 15 -> byte 1, bit 7 (not set) + public void IsBitSet_PositionsInSecondByte_ReturnsCorrectResult(int pos, bool expected) + { + // Arrange + // First byte: all zeros + // Second byte: 0b0101_0101 (bits 0, 2, 4, 6 set) + byte[] backing = [0b0000_0000, 0b0101_0101]; + ReadOnlySpan data = new(backing); + Assert.Equal(expected, data.IsBitSet(pos)); + } + + /// + /// Tests that IsBitSet works correctly with all bits set in multiple bytes. + /// Input: Two bytes with all bits set, various positions. + /// Expected: All positions within valid range return true. + /// + [Theory] + [InlineData(0)] + [InlineData(3)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + [InlineData(10)] + [InlineData(13)] + [InlineData(15)] + public void IsBitSet_AllBitsSet_ReturnsTrue(int pos) + { + // Arrange + byte[] backing = [0xFF, 0xFF]; + ReadOnlySpan data = new(backing); + Assert.True(data.IsBitSet(pos)); + } + + /// + /// Tests that IsBitSet works correctly with no bits set. + /// Input: Multiple bytes with all bits cleared, various positions. + /// Expected: All positions return false. + /// + [Theory] + [InlineData(0)] + [InlineData(3)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + [InlineData(10)] + [InlineData(13)] + public void IsBitSet_NoBitsSet_ReturnsFalse(int pos) + { + // Arrange + byte[] backing = [0x00, 0x00, 0x00]; + ReadOnlySpan data = new(backing); + Assert.False(data.IsBitSet(pos)); + } + + /// + /// Tests that IsBitSet throws IndexOutOfRangeException when position maps to a non-existent byte. + /// Input: Two byte span, position 16 (maps to third byte). + /// + [Fact] + public void IsBitSet_PositionMapsToNonExistentThirdByte_ThrowsIndexOutOfRange() + { + // Arrange + byte[] backing = [0xFF, 0xFF]; + ReadOnlySpan data = new(backing); + int pos = 16; // byteIndex = 16/8 = 2, but only indices 0-1 exist + + // Act + AssertThrowsIndexOutOfRange(data, pos); + } + + /// + /// Tests that IsBitSet handles position zero correctly with single byte. + /// Input: Single byte with specific pattern, position 0. + /// Expected: Returns correct bit value at position 0. + /// + [Theory] + [InlineData(0b0000_0001, true)] // Bit 0 set + [InlineData(0b0000_0000, false)] // Bit 0 not set + [InlineData(0b1111_1110, false)] // Bit 0 not set + public void IsBitSet_PositionZero_ReturnsCorrectResult(byte byteValue, bool expected) + { + // Arrange + byte[] backing = [byteValue]; + ReadOnlySpan data = new(backing); + int pos = 0; + + // Act + bool result = data.IsBitSet(pos); + Assert.Equal(expected, result); + } + + /// + /// Tests that IsBitSet throws IndexOutOfRangeException when the position is out of bounds of the array. + /// Uses standard bit numbering, so position is mapped to byteIndex = pos / 8. + /// + [Theory] + [InlineData(8, 1)] // pos=8 maps to byteIndex=1, array has only 1 byte + [InlineData(9, 1)] // pos=9 maps to byteIndex=1, array has only 1 byte + [InlineData(16, 2)] // pos=16 maps to byteIndex=2, array has only 2 bytes + [InlineData(100, 3)] // pos=100 maps to byteIndex=12, array has only 3 bytes + [InlineData(int.MaxValue, 10)] // Very large position, array has 10 bytes + public void IsBitSet_PositionOutOfBounds_ThrowsIndexOutOfRange(int pos, int arrayLength) + { + // Arrange + byte[] data = new byte[arrayLength]; + + // Act + Assert.Throws(() => data.IsBitSet(pos)); + } + + /// + /// Tests that IsBitSet throws NullReferenceException when the array is null. + /// + [Fact] + public void IsBitSet_NullArray_ThrowsNullReferenceException() + { + // Arrange + byte[]? data = null; + + // Act & Assert + Assert.Throws(() => data!.IsBitSet(0)); + } + + /// + /// Tests that IsBitSet throws IndexOutOfRangeException for any valid position when the array is empty. + /// + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(10)] + [InlineData(int.MaxValue)] + public void IsBitSet_EmptyArray_ThrowsIndexOutOfRange(int pos) + { + // Arrange + byte[] data = []; + + // Act + Assert.Throws(() => data.IsBitSet(pos)); + } + + /// + /// Tests IsBitSet with various positions and bit patterns using standard bit numbering. + /// Verifies correct mapping: pos/8 gives byte index, pos%8 gives bit index within that byte. + /// + [Theory] + [InlineData(0, new byte[] { 0b0000_0001 }, true)] // pos=0: byte[0] bit 0 is set + [InlineData(0, new byte[] { 0b0000_0000 }, false)] // pos=0: byte[0] bit 0 is not set + [InlineData(1, new byte[] { 0b0000_0010 }, true)] // pos=1: byte[0] bit 1 is set + [InlineData(6, new byte[] { 0b0100_0000 }, true)] // pos=6: byte[0] bit 6 is set + [InlineData(6, new byte[] { 0b0000_0000 }, false)] // pos=6: byte[0] bit 6 is not set + [InlineData(7, new byte[] { 0b1000_0000 }, true)] // pos=7: byte[0] bit 7 is set + [InlineData(7, new byte[] { 0b1111_1111, 0b0000_0000 }, true)] // pos=7: byte[0] bit 7 is set, byte[1] bit 0 is not relevant + [InlineData(8, new byte[] { 0b0000_0000, 0b0000_0010 }, false)] // pos=8: byte[1] bit 1 is set, not bit 0 + [InlineData(8, new byte[] { 0b0000_0000, 0b0000_0001 }, true)] // pos=8: byte[1] bit 0 is set + [InlineData(13, new byte[] { 0b0000_0000, 0b0010_0000 }, true)] // pos=13: byte[1] bit 5 is set + [InlineData(16, new byte[] { 0b0000_0000, 0b0000_0000, 0b0000_0001 }, true)] // pos=16: byte[2] bit 0 is set + public void IsBitSet_ValidPositionWithBitPattern_ReturnsExpectedResult(int pos, byte[] data, bool expectedResult) + { + // Act + Assert.Equal(expectedResult, data.IsBitSet(pos)); + } + + /// + /// Tests IsBitSet at byte boundary positions (where position transitions from one byte to the next). + /// With standard bit numbering: positions 7→8, 15→16, etc. are boundaries. + /// + [Fact] + public void IsBitSet_BoundaryPositions_MapsCorrectlyAcrossBytes() + { + // Arrange + // byte[0] has bit 6 set, byte[1] has bit 0 set, byte[2] has bit 6 set + byte[] data = [0b0100_0000, 0b0000_0001, 0b0100_0000]; + + // Act & Assert + Assert.True(data.IsBitSet(6)); // byte[0] bit 6 + Assert.False(data.IsBitSet(7)); // byte[0] bit 7 not set + Assert.True(data.IsBitSet(8)); // byte[1] bit 0 + Assert.False(data.IsBitSet(13)); // byte[1] bit 5 not set + Assert.False(data.IsBitSet(14)); // byte[1] bit 6 not set + Assert.False(data.IsBitSet(16)); // byte[2] bit 0 not set + Assert.True(data.IsBitSet(22)); // byte[2] bit 6 + } + + /// + /// Tests IsBitSet with negative positions where byteIndex becomes negative, + /// which should throw IndexOutOfRangeException when accessing the array. + /// + [Theory] + [InlineData(-8)] + [InlineData(-9)] + [InlineData(-16)] + [InlineData(int.MinValue)] + public void IsBitSet_NegativePositionWithNegativeByteIndex_ThrowsIndexOutOfRangeException(int pos) + { + // Arrange + byte[] data = [0xFF]; + + // Act & Assert + Assert.Throws(() => data.IsBitSet(pos)); + } + + /// + /// Tests IsBitSet with negative positions in range [-7, -1] where byteIndex = 0 but bitIndex is negative. + /// The byte.IsBitSet implementation throws for negative bit indices. + /// + [Theory] + [InlineData(-1)] + [InlineData(-2)] + [InlineData(-7)] + public void IsBitSet_NegativePositionWithZeroByteIndex_ThrowsIndexOutOfRangeException(int pos) + { + // Arrange + byte[] data = [0xFF]; + + // Act + Assert.Throws(() => data.IsBitSet(pos)); + } + + /// + /// Tests IsBitSet with position 0 on various byte patterns. + /// Position 0 maps to byte[0] bit 0 (LSB). + /// + [Theory] + [InlineData(0b0000_0001, true)] // LSB set + [InlineData(0b0000_0000, false)] // LSB not set + [InlineData(0b1111_1110, false)] // All bits except LSB set + [InlineData(0b1111_1111, true)] // All bits set + public void IsBitSet_Position0_ChecksFirstBitOfFirstByte(byte firstByte, bool expectedResult) + { + // Arrange + byte[] data = [firstByte]; + + // Act + bool result = data.IsBitSet(0); + + // Assert + Assert.Equal(expectedResult, result); + } + + /// + /// Tests IsBitSet with a comprehensive bit pattern across multiple bytes + /// to verify correct standard bit numbering throughout the array. + /// + [Fact] + public void IsBitSet_MultipleBytes_UsesStandardBitMappingCorrectly() + { + // Arrange + // Create a pattern where specific positions are set + byte[] data = + [ + 0b0101_0101, // byte[0]: bits 0,2,4,6 set + 0b1010_1010, // byte[1]: bits 1,3,5,7 set + 0b0000_1111 // byte[2]: bits 0,1,2,3 set + ]; + + // Act & Assert + // Positions 0-7 map to byte[0] + Assert.True(data.IsBitSet(0)); // bit 0 set + Assert.False(data.IsBitSet(1)); // bit 1 not set + Assert.True(data.IsBitSet(2)); // bit 2 set + Assert.False(data.IsBitSet(3)); // bit 3 not set + Assert.True(data.IsBitSet(4)); // bit 4 set + Assert.False(data.IsBitSet(5)); // bit 5 not set + Assert.True(data.IsBitSet(6)); // bit 6 set + Assert.False(data.IsBitSet(7)); // bit 7 not set + + // Positions 8-15 map to byte[1] + Assert.False(data.IsBitSet(8)); // bit 0 not set + Assert.True(data.IsBitSet(9)); // bit 1 set + Assert.False(data.IsBitSet(10)); // bit 2 not set + Assert.True(data.IsBitSet(11)); // bit 3 set + Assert.False(data.IsBitSet(12)); // bit 4 not set + Assert.True(data.IsBitSet(13)); // bit 5 set + Assert.False(data.IsBitSet(14)); // bit 6 not set + Assert.True(data.IsBitSet(15)); // bit 7 set + + // Positions 16-23 map to byte[2] + Assert.True(data.IsBitSet(16)); // bit 0 set + Assert.True(data.IsBitSet(17)); // bit 1 set + Assert.True(data.IsBitSet(18)); // bit 2 set + Assert.True(data.IsBitSet(19)); // bit 3 set + Assert.False(data.IsBitSet(20)); // bit 4 not set + Assert.False(data.IsBitSet(21)); // bit 5 not set + Assert.False(data.IsBitSet(22)); // bit 6 not set + Assert.False(data.IsBitSet(23)); // bit 7 not set + } + + /// + /// Tests IsBitSet with position exactly at the upper boundary that maps to the last valid byte. + /// Verifies behavior when position maps exactly to the last byte's last usable bit (bit 7). + /// + [Fact] + public void IsBitSet_PositionAtExactArrayBoundary_ReturnsCorrectResult() + { + // Arrange + // Array with 3 bytes, so valid positions are 0-23 + // Position 23 = byteIndex 2, bitIndex 7 (last valid position) + byte[] data = [0x00, 0x00, 0b1000_0000]; + + // Act & Assert + Assert.True(data.IsBitSet(23)); // Exactly at boundary, bit is set + AssertThrowsIndexOutOfRange(data, 24); // Beyond boundary, throws + } + + /// + /// Tests that the MSB of the first byte is bit 7 using standard bit numbering. + /// Expected: pos 7 reads byte[0] bit 7, pos 15 reads byte[1] bit 7. + /// + [Fact] + public void IsBitSet_StandardBitNumbering_MsbOfFirstByteIsBit7() + { + byte[] data = [0b1000_0000, 0b0000_0000]; + + Assert.True(data.IsBitSet(7)); + Assert.False(data.IsBitSet(15)); // byte[1] bit 7, not set + } + + /// + /// Tests that the LSB of the second byte is bit 8 using standard bit numbering. + /// Expected: pos 8 reads byte[1] bit 0, pos 7 reads byte[0] bit 7. + /// + [Fact] + public void IsBitSet_StandardBitNumbering_LsbOfSecondByteIsBit8() + { + byte[] data = [0b0000_0000, 0b0000_0001]; + + Assert.True(data.IsBitSet(8)); + Assert.False(data.IsBitSet(7)); // byte[0] bit 7, not set + } + + /// + /// Tests that a single byte with its MSB set does not overflow at position 7. + /// Expected: pos 7 returns true instead of throwing IndexOutOfRangeException. + /// + [Fact] + public void IsBitSet_SingleByte_MsbDoesNotOverflow() + { + byte[] data = [0b1000_0000]; + + Assert.True(data.IsBitSet(7)); // byte[0] bit 7 (MSB) + } + + // Helper for ReadOnlySpan exception assertion + private static void AssertThrowsIndexOutOfRange(ReadOnlySpan span, int pos) + { + try + { + span.IsBitSet(pos); + Assert.Fail("Expected IndexOutOfRangeException was not thrown."); + } + catch (IndexOutOfRangeException) + { + // Expected + } + } } \ No newline at end of file diff --git a/FTMS.NET.Tests/Utils/GenericMathTests.cs b/FTMS.NET.Tests/Utils/GenericMathTests.cs index 1a73a76..1e4461d 100644 --- a/FTMS.NET.Tests/Utils/GenericMathTests.cs +++ b/FTMS.NET.Tests/Utils/GenericMathTests.cs @@ -1,750 +1,750 @@ -namespace FTMS.NET.Tests.Utils; - -using FTMS.NET.Utils; - -/// -/// Unit tests for the class. -/// -public sealed class GenericMathTests -{ - /// - /// Tests that Clamp returns the minimum value when the input value is below the minimum. - /// - [Theory] - [InlineData(5, 10, 20, 10)] - [InlineData(-10, 0, 100, 0)] - [InlineData(int.MinValue, 0, 100, 0)] - public void Clamp_ValueBelowMin_ReturnsMin(int value, int min, int max, int expected) - { - // Act - int result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Clamp returns the maximum value when the input value is above the maximum. - /// - [Theory] - [InlineData(25, 10, 20, 20)] - [InlineData(150, 0, 100, 100)] - [InlineData(int.MaxValue, 0, 100, 100)] - public void Clamp_ValueAboveMax_ReturnsMax(int value, int min, int max, int expected) - { - // Act - int result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Clamp returns the original value when it is within the valid range. - /// - [Theory] - [InlineData(15, 10, 20, 15)] - [InlineData(50, 0, 100, 50)] - [InlineData(0, -100, 100, 0)] - public void Clamp_ValueWithinRange_ReturnsValue(int value, int min, int max, int expected) - { - // Act - int result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Clamp returns the value when it equals the minimum boundary. - /// - [Theory] - [InlineData(10, 10, 20, 10)] - [InlineData(0, 0, 100, 0)] - [InlineData(int.MinValue, int.MinValue, int.MaxValue, int.MinValue)] - public void Clamp_ValueEqualsMin_ReturnsValue(int value, int min, int max, int expected) - { - // Act - int result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Clamp returns the value when it equals the maximum boundary. - /// - [Theory] - [InlineData(20, 10, 20, 20)] - [InlineData(100, 0, 100, 100)] - [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MaxValue)] - public void Clamp_ValueEqualsMax_ReturnsValue(int value, int min, int max, int expected) - { - // Act - int result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Clamp handles extreme integer boundary values correctly. - /// - [Fact] - public void Clamp_IntegerBoundaryValues_HandlesCorrectly() - { - // Arrange & Act - int result1 = GenericMath.Clamp(int.MinValue, int.MinValue, int.MaxValue); - int result2 = GenericMath.Clamp(int.MaxValue, int.MinValue, int.MaxValue); - int result3 = GenericMath.Clamp(0, int.MinValue, int.MaxValue); - - // Assert - Assert.Equal(int.MinValue, result1); - Assert.Equal(int.MaxValue, result2); - Assert.Equal(0, result3); - } - - /// - /// Tests that Clamp works with different numeric types using saturating conversion. - /// - [Fact] - public void Clamp_CrossTypeConversion_UsesCreateSaturating() - { - // Arrange - int value with byte min and short max - int value = 150; - byte min = 50; - short max = 200; - - // Act - int result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(150, result); - } - - /// - /// Tests that Clamp saturates when max value exceeds TValue's representable range. - /// - [Fact] - public void Clamp_MaxExceedsByteRange_SaturatesToByteMax() - { - // Arrange - byte value with int max that exceeds byte range - byte value = 250; - byte min = 0; - int max = 1000; // Exceeds byte.MaxValue (255), should saturate to 255 - - // Act - byte result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(250, result); - } - - /// - /// Tests that Clamp saturates when min value exceeds TValue's representable range. - /// - [Fact] - public void Clamp_MinExceedsByteRange_SaturatesToByteMax() - { - // Arrange - byte value with int min that exceeds byte range - byte value = 10; - int min = 300; // Exceeds byte.MaxValue (255), should saturate to 255 - byte max = 255; - - // Act - byte result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(255, result); // value (10) < saturated min (255), returns saturated min - } - - /// - /// Tests that Clamp handles negative minimum values with unsigned types. - /// - [Fact] - public void Clamp_NegativeMinWithUnsignedType_SaturatesToZero() - { - // Arrange - byte value with negative min (saturates to 0 for unsigned byte) - byte value = 50; - int min = -100; // Negative, saturates to 0 for byte - byte max = 200; - - // Act - byte result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(50, result); - } - - /// - /// Tests that Clamp works correctly with double precision floating-point values. - /// - [Theory] - [InlineData(5.5, 1.0, 10.0, 5.5)] - [InlineData(0.5, 1.0, 10.0, 1.0)] - [InlineData(15.5, 1.0, 10.0, 10.0)] - public void Clamp_DoubleValues_ClampsCorrectly(double value, double min, double max, double expected) - { - // Act - double result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Clamp handles positive infinity by clamping to max. - /// - [Fact] - public void Clamp_PositiveInfinity_ClampsToMax() - { - // Arrange - double value = double.PositiveInfinity; - double min = 0.0; - double max = 100.0; - - // Act - double result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(100.0, result); - } - - /// - /// Tests that Clamp handles negative infinity by clamping to min. - /// - [Fact] - public void Clamp_NegativeInfinity_ClampsToMin() - { - // Arrange - double value = double.NegativeInfinity; - double min = -100.0; - double max = 100.0; - - // Act - double result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(-100.0, result); - } - - /// - /// Tests that Clamp handles NaN values, which have special comparison behavior. - /// NaN comparisons always return false, so NaN > tMin is false and NaN < tMax is false. - /// - [Fact] - public void Clamp_NaNValue_ReturnsNaN() - { - // Arrange - double value = double.NaN; - double min = 0.0; - double max = 100.0; - - // Act - double result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.True(double.IsNaN(result)); - } - - /// - /// Tests that Clamp works correctly with long integer values at boundaries. - /// - [Fact] - public void Clamp_LongBoundaryValues_HandlesCorrectly() - { - // Arrange & Act - long result1 = GenericMath.Clamp(long.MinValue, long.MinValue, long.MaxValue); - long result2 = GenericMath.Clamp(long.MaxValue, long.MinValue, long.MaxValue); - long result3 = GenericMath.Clamp(0L, long.MinValue, long.MaxValue); - - // Assert - Assert.Equal(long.MinValue, result1); - Assert.Equal(long.MaxValue, result2); - Assert.Equal(0L, result3); - } - - /// - /// Tests that Clamp works correctly with float values. - /// - [Theory] - [InlineData(5.5f, 1.0f, 10.0f, 5.5f)] - [InlineData(0.5f, 1.0f, 10.0f, 1.0f)] - [InlineData(15.5f, 1.0f, 10.0f, 10.0f)] - public void Clamp_FloatValues_ClampsCorrectly(float value, float min, float max, float expected) - { - // Act - float result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Clamp works correctly with decimal values. - /// - [Fact] - public void Clamp_DecimalValues_ClampsCorrectly() - { - // Arrange - decimal value1 = 5.5m; - decimal value2 = 0.5m; - decimal value3 = 15.5m; - decimal min = 1.0m; - decimal max = 10.0m; - - // Act - decimal result1 = GenericMath.Clamp(value1, min, max); - decimal result2 = GenericMath.Clamp(value2, min, max); - decimal result3 = GenericMath.Clamp(value3, min, max); - - // Assert - Assert.Equal(5.5m, result1); - Assert.Equal(1.0m, result2); - Assert.Equal(10.0m, result3); - } - - /// - /// Tests that Clamp handles the scenario where min is greater than max. - /// In this case, tMin > value is checked first, so if value < min, min is returned. - /// If value >= min (which is > max), then tMax < value will be true, so max is returned. - /// - [Fact] - public void Clamp_MinGreaterThanMax_ReturnsBasedOnFirstCheck() - { - // Arrange - int value1 = 5; - int value2 = 20; - int value3 = 25; - int min = 20; - int max = 10; - - // Act - int result1 = GenericMath.Clamp(value1, min, max); - int result2 = GenericMath.Clamp(value2, min, max); - int result3 = GenericMath.Clamp(value3, min, max); - - // Assert - Assert.Equal(20, result1); // value < min, returns min - Assert.Equal(10, result2); // min check fails, value > max, returns max - Assert.Equal(10, result3); // min check fails, value > max, returns max - } - - /// - /// Tests that Clamp works with short integer values. - /// - [Fact] - public void Clamp_ShortValues_ClampsCorrectly() - { - // Arrange - short value = 150; - short min = 100; - short max = 200; - - // Act - short result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal((short)150, result); - } - - /// - /// Tests that Clamp works with short values at boundaries. - /// - [Fact] - public void Clamp_ShortBoundaryValues_HandlesCorrectly() - { - // Arrange & Act - short result1 = GenericMath.Clamp(short.MinValue, short.MinValue, short.MaxValue); - short result2 = GenericMath.Clamp(short.MaxValue, short.MinValue, short.MaxValue); - - // Assert - Assert.Equal(short.MinValue, result1); - Assert.Equal(short.MaxValue, result2); - } - - /// - /// Tests that Clamp works correctly with zero values for all parameters. - /// - [Fact] - public void Clamp_AllZeroValues_ReturnsZero() - { - // Arrange - int value = 0; - int min = 0; - int max = 0; - - // Act - int result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(0, result); - } - - /// - /// Tests that Clamp works with negative values for all parameters. - /// - [Theory] - [InlineData(-15, -20, -10, -15)] - [InlineData(-25, -20, -10, -20)] - [InlineData(-5, -20, -10, -10)] - public void Clamp_NegativeValues_ClampsCorrectly(int value, int min, int max, int expected) - { - // Act - int result = GenericMath.Clamp(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that IsInRange returns true when value is within the inclusive range [min, max]. - /// - /// The value to test. - /// The minimum bound. - /// The maximum bound. - [Theory] - [InlineData(5, 1, 10)] - [InlineData(0, -10, 10)] - [InlineData(-5, -10, 0)] - [InlineData(100, 100, 200)] - [InlineData(200, 100, 200)] - [InlineData(0, 0, 0)] - public void IsInRange_ValueWithinRange_ReturnsTrue(int value, int min, int max) - { - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.True(result); - } - - /// - /// Tests that IsInRange returns false when value is outside the range [min, max]. - /// - /// The value to test. - /// The minimum bound. - /// The maximum bound. - [Theory] - [InlineData(0, 1, 10)] - [InlineData(11, 1, 10)] - [InlineData(-11, -10, 0)] - [InlineData(1, 2, 10)] - [InlineData(99, 100, 200)] - [InlineData(201, 100, 200)] - public void IsInRange_ValueOutsideRange_ReturnsFalse(int value, int min, int max) - { - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.False(result); - } - - /// - /// Tests that IsInRange correctly handles boundary values including int.MinValue and int.MaxValue. - /// - /// The value to test. - /// The minimum bound. - /// The maximum bound. - /// The expected result. - [Theory] - [InlineData(int.MinValue, int.MinValue, int.MaxValue, true)] - [InlineData(int.MaxValue, int.MinValue, int.MaxValue, true)] - [InlineData(0, int.MinValue, int.MaxValue, true)] - [InlineData(int.MinValue, int.MinValue, int.MinValue, true)] - [InlineData(int.MaxValue, int.MaxValue, int.MaxValue, true)] - [InlineData(int.MinValue, 0, int.MaxValue, false)] - [InlineData(int.MaxValue, int.MinValue, 0, false)] - public void IsInRange_BoundaryValues_ReturnsExpected(int value, int min, int max, bool expected) - { - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that IsInRange works correctly with double precision floating point values. - /// - /// The value to test. - /// The minimum bound. - /// The maximum bound. - /// The expected result. - [Theory] - [InlineData(5.5, 1.0, 10.0, true)] - [InlineData(1.0, 1.0, 10.0, true)] - [InlineData(10.0, 1.0, 10.0, true)] - [InlineData(0.5, 1.0, 10.0, false)] - [InlineData(10.5, 1.0, 10.0, false)] - [InlineData(-0.001, -0.001, 0.001, true)] - public void IsInRange_DoubleValues_ReturnsExpected(double value, double min, double max, bool expected) - { - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that IsInRange handles double.NaN correctly. NaN comparisons always return false. - /// - [Fact] - public void IsInRange_ValueIsNaN_ReturnsFalse() - { - // Arrange - double value = double.NaN; - double min = 0.0; - double max = 10.0; - - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.False(result); - } - - /// - /// Tests that IsInRange handles double.PositiveInfinity correctly. - /// - /// The value to test. - /// The minimum bound. - /// The maximum bound. - /// The expected result. - [Theory] - [InlineData(double.PositiveInfinity, 0.0, double.PositiveInfinity, true)] - [InlineData(double.PositiveInfinity, 0.0, 100.0, false)] - [InlineData(5.0, 0.0, double.PositiveInfinity, true)] - public void IsInRange_PositiveInfinity_ReturnsExpected(double value, double min, double max, bool expected) - { - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that IsInRange handles double.NegativeInfinity correctly. - /// - /// The value to test. - /// The minimum bound. - /// The maximum bound. - /// The expected result. - [Theory] - [InlineData(double.NegativeInfinity, double.NegativeInfinity, 0.0, true)] - [InlineData(double.NegativeInfinity, -100.0, 0.0, false)] - [InlineData(5.0, double.NegativeInfinity, 10.0, true)] - public void IsInRange_NegativeInfinity_ReturnsExpected(double value, double min, double max, bool expected) - { - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that IsInRange works correctly with mixed numeric types, relying on CreateSaturating conversions. - /// - [Fact] - public void IsInRange_MixedNumericTypes_IntValueShortMinLongMax_ReturnsTrue() - { - // Arrange - int value = 1000; - short min = 100; - long max = 10000L; - - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.True(result); - } - - /// - /// Tests that IsInRange with mixed types returns false when value is outside range. - /// - [Fact] - public void IsInRange_MixedNumericTypes_IntValueOutsideRange_ReturnsFalse() - { - // Arrange - int value = 50; - short min = 100; - long max = 10000L; - - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.False(result); - } - - /// - /// Tests that IsInRange handles saturation when min/max types have larger range than TValue. - /// When converting a value larger than byte.MaxValue to byte, it saturates to 255. - /// - [Fact] - public void IsInRange_TypeSaturation_MaxExceedsByteRange_Saturates() - { - // Arrange - byte value = 200; - int min = -100; // Saturates to 0 when converted to byte - int max = 300; // Saturates to 255 when converted to byte - - // Act - // After saturation: IsInRange(200, 0, 255) - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.True(result); - } - - /// - /// Tests that IsInRange handles saturation when min is negative but TValue is unsigned. - /// - [Fact] - public void IsInRange_TypeSaturation_NegativeMinWithUnsignedValue_Saturates() - { - // Arrange - byte value = 5; - int min = -100; // Saturates to 0 when converted to byte - int max = 10; - - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.True(result); - } - - /// - /// Tests that IsInRange returns false for all values when min > max (invalid range). - /// - /// The value to test. - [Theory] - [InlineData(0)] - [InlineData(5)] - [InlineData(10)] - [InlineData(15)] - [InlineData(-5)] - public void IsInRange_MinGreaterThanMax_ReturnsFalse(int value) - { - // Arrange - int min = 10; - int max = 5; - - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.False(result); - } - - /// - /// Tests that IsInRange works correctly with decimal type values. - /// - /// The value to test. - /// The minimum bound. - /// The maximum bound. - /// The expected result. - [Theory] - [InlineData(5.5, 1.0, 10.0, true)] - [InlineData(0.5, 1.0, 10.0, false)] - [InlineData(10.5, 1.0, 10.0, false)] - public void IsInRange_DecimalValues_ReturnsExpected(double valueDouble, double minDouble, double maxDouble, bool expected) - { - // Arrange - decimal value = (decimal)valueDouble; - decimal min = (decimal)minDouble; - decimal max = (decimal)maxDouble; - - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that IsInRange works correctly with float type values. - /// - /// The value to test. - /// The minimum bound. - /// The maximum bound. - /// The expected result. - [Theory] - [InlineData(5.5f, 1.0f, 10.0f, true)] - [InlineData(1.0f, 1.0f, 10.0f, true)] - [InlineData(10.0f, 1.0f, 10.0f, true)] - [InlineData(0.5f, 1.0f, 10.0f, false)] - [InlineData(10.5f, 1.0f, 10.0f, false)] - public void IsInRange_FloatValues_ReturnsExpected(float value, float min, float max, bool expected) - { - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that IsInRange handles float.NaN correctly. NaN comparisons always return false. - /// - [Fact] - public void IsInRange_FloatNaN_ReturnsFalse() - { - // Arrange - float value = float.NaN; - float min = 0.0f; - float max = 10.0f; - - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.False(result); - } - - /// - /// Tests that IsInRange works with long type values and boundaries. - /// - [Fact] - public void IsInRange_LongBoundaryValues_ReturnsTrue() - { - // Arrange - long value = long.MaxValue; - long min = long.MinValue; - long max = long.MaxValue; - - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.True(result); - } - - /// - /// Tests that IsInRange works with negative values across the range. - /// - /// The value to test. - /// The minimum bound. - /// The maximum bound. - /// The expected result. - [Theory] - [InlineData(-5, -10, -1, true)] - [InlineData(-10, -10, -1, true)] - [InlineData(-1, -10, -1, true)] - [InlineData(0, -10, -1, false)] - [InlineData(-11, -10, -1, false)] - public void IsInRange_NegativeValues_ReturnsExpected(int value, int min, int max, bool expected) - { - // Act - bool result = GenericMath.IsInRange(value, min, max); - - // Assert - Assert.Equal(expected, result); - } +namespace FTMS.NET.Tests.Utils; + +using FTMS.NET.Utils; + +/// +/// Unit tests for the class. +/// +public sealed class GenericMathTests +{ + /// + /// Tests that Clamp returns the minimum value when the input value is below the minimum. + /// + [Theory] + [InlineData(5, 10, 20, 10)] + [InlineData(-10, 0, 100, 0)] + [InlineData(int.MinValue, 0, 100, 0)] + public void Clamp_ValueBelowMin_ReturnsMin(int value, int min, int max, int expected) + { + // Act + int result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Clamp returns the maximum value when the input value is above the maximum. + /// + [Theory] + [InlineData(25, 10, 20, 20)] + [InlineData(150, 0, 100, 100)] + [InlineData(int.MaxValue, 0, 100, 100)] + public void Clamp_ValueAboveMax_ReturnsMax(int value, int min, int max, int expected) + { + // Act + int result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Clamp returns the original value when it is within the valid range. + /// + [Theory] + [InlineData(15, 10, 20, 15)] + [InlineData(50, 0, 100, 50)] + [InlineData(0, -100, 100, 0)] + public void Clamp_ValueWithinRange_ReturnsValue(int value, int min, int max, int expected) + { + // Act + int result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Clamp returns the value when it equals the minimum boundary. + /// + [Theory] + [InlineData(10, 10, 20, 10)] + [InlineData(0, 0, 100, 0)] + [InlineData(int.MinValue, int.MinValue, int.MaxValue, int.MinValue)] + public void Clamp_ValueEqualsMin_ReturnsValue(int value, int min, int max, int expected) + { + // Act + int result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Clamp returns the value when it equals the maximum boundary. + /// + [Theory] + [InlineData(20, 10, 20, 20)] + [InlineData(100, 0, 100, 100)] + [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MaxValue)] + public void Clamp_ValueEqualsMax_ReturnsValue(int value, int min, int max, int expected) + { + // Act + int result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Clamp handles extreme integer boundary values correctly. + /// + [Fact] + public void Clamp_IntegerBoundaryValues_HandlesCorrectly() + { + // Arrange & Act + int result1 = GenericMath.Clamp(int.MinValue, int.MinValue, int.MaxValue); + int result2 = GenericMath.Clamp(int.MaxValue, int.MinValue, int.MaxValue); + int result3 = GenericMath.Clamp(0, int.MinValue, int.MaxValue); + + // Assert + Assert.Equal(int.MinValue, result1); + Assert.Equal(int.MaxValue, result2); + Assert.Equal(0, result3); + } + + /// + /// Tests that Clamp works with different numeric types using saturating conversion. + /// + [Fact] + public void Clamp_CrossTypeConversion_UsesCreateSaturating() + { + // Arrange - int value with byte min and short max + int value = 150; + byte min = 50; + short max = 200; + + // Act + int result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(150, result); + } + + /// + /// Tests that Clamp saturates when max value exceeds TValue's representable range. + /// + [Fact] + public void Clamp_MaxExceedsByteRange_SaturatesToByteMax() + { + // Arrange - byte value with int max that exceeds byte range + byte value = 250; + byte min = 0; + int max = 1000; // Exceeds byte.MaxValue (255), should saturate to 255 + + // Act + byte result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(250, result); + } + + /// + /// Tests that Clamp saturates when min value exceeds TValue's representable range. + /// + [Fact] + public void Clamp_MinExceedsByteRange_SaturatesToByteMax() + { + // Arrange - byte value with int min that exceeds byte range + byte value = 10; + int min = 300; // Exceeds byte.MaxValue (255), should saturate to 255 + byte max = 255; + + // Act + byte result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(255, result); // value (10) < saturated min (255), returns saturated min + } + + /// + /// Tests that Clamp handles negative minimum values with unsigned types. + /// + [Fact] + public void Clamp_NegativeMinWithUnsignedType_SaturatesToZero() + { + // Arrange - byte value with negative min (saturates to 0 for unsigned byte) + byte value = 50; + int min = -100; // Negative, saturates to 0 for byte + byte max = 200; + + // Act + byte result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(50, result); + } + + /// + /// Tests that Clamp works correctly with double precision floating-point values. + /// + [Theory] + [InlineData(5.5, 1.0, 10.0, 5.5)] + [InlineData(0.5, 1.0, 10.0, 1.0)] + [InlineData(15.5, 1.0, 10.0, 10.0)] + public void Clamp_DoubleValues_ClampsCorrectly(double value, double min, double max, double expected) + { + // Act + double result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Clamp handles positive infinity by clamping to max. + /// + [Fact] + public void Clamp_PositiveInfinity_ClampsToMax() + { + // Arrange + double value = double.PositiveInfinity; + double min = 0.0; + double max = 100.0; + + // Act + double result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(100.0, result); + } + + /// + /// Tests that Clamp handles negative infinity by clamping to min. + /// + [Fact] + public void Clamp_NegativeInfinity_ClampsToMin() + { + // Arrange + double value = double.NegativeInfinity; + double min = -100.0; + double max = 100.0; + + // Act + double result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(-100.0, result); + } + + /// + /// Tests that Clamp handles NaN values, which have special comparison behavior. + /// NaN comparisons always return false, so NaN > tMin is false and NaN < tMax is false. + /// + [Fact] + public void Clamp_NaNValue_ReturnsNaN() + { + // Arrange + double value = double.NaN; + double min = 0.0; + double max = 100.0; + + // Act + double result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.True(double.IsNaN(result)); + } + + /// + /// Tests that Clamp works correctly with long integer values at boundaries. + /// + [Fact] + public void Clamp_LongBoundaryValues_HandlesCorrectly() + { + // Arrange & Act + long result1 = GenericMath.Clamp(long.MinValue, long.MinValue, long.MaxValue); + long result2 = GenericMath.Clamp(long.MaxValue, long.MinValue, long.MaxValue); + long result3 = GenericMath.Clamp(0L, long.MinValue, long.MaxValue); + + // Assert + Assert.Equal(long.MinValue, result1); + Assert.Equal(long.MaxValue, result2); + Assert.Equal(0L, result3); + } + + /// + /// Tests that Clamp works correctly with float values. + /// + [Theory] + [InlineData(5.5f, 1.0f, 10.0f, 5.5f)] + [InlineData(0.5f, 1.0f, 10.0f, 1.0f)] + [InlineData(15.5f, 1.0f, 10.0f, 10.0f)] + public void Clamp_FloatValues_ClampsCorrectly(float value, float min, float max, float expected) + { + // Act + float result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Clamp works correctly with decimal values. + /// + [Fact] + public void Clamp_DecimalValues_ClampsCorrectly() + { + // Arrange + decimal value1 = 5.5m; + decimal value2 = 0.5m; + decimal value3 = 15.5m; + decimal min = 1.0m; + decimal max = 10.0m; + + // Act + decimal result1 = GenericMath.Clamp(value1, min, max); + decimal result2 = GenericMath.Clamp(value2, min, max); + decimal result3 = GenericMath.Clamp(value3, min, max); + + // Assert + Assert.Equal(5.5m, result1); + Assert.Equal(1.0m, result2); + Assert.Equal(10.0m, result3); + } + + /// + /// Tests that Clamp handles the scenario where min is greater than max. + /// In this case, tMin > value is checked first, so if value < min, min is returned. + /// If value >= min (which is > max), then tMax < value will be true, so max is returned. + /// + [Fact] + public void Clamp_MinGreaterThanMax_ReturnsBasedOnFirstCheck() + { + // Arrange + int value1 = 5; + int value2 = 20; + int value3 = 25; + int min = 20; + int max = 10; + + // Act + int result1 = GenericMath.Clamp(value1, min, max); + int result2 = GenericMath.Clamp(value2, min, max); + int result3 = GenericMath.Clamp(value3, min, max); + + // Assert + Assert.Equal(20, result1); // value < min, returns min + Assert.Equal(10, result2); // min check fails, value > max, returns max + Assert.Equal(10, result3); // min check fails, value > max, returns max + } + + /// + /// Tests that Clamp works with short integer values. + /// + [Fact] + public void Clamp_ShortValues_ClampsCorrectly() + { + // Arrange + short value = 150; + short min = 100; + short max = 200; + + // Act + short result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal((short)150, result); + } + + /// + /// Tests that Clamp works with short values at boundaries. + /// + [Fact] + public void Clamp_ShortBoundaryValues_HandlesCorrectly() + { + // Arrange & Act + short result1 = GenericMath.Clamp(short.MinValue, short.MinValue, short.MaxValue); + short result2 = GenericMath.Clamp(short.MaxValue, short.MinValue, short.MaxValue); + + // Assert + Assert.Equal(short.MinValue, result1); + Assert.Equal(short.MaxValue, result2); + } + + /// + /// Tests that Clamp works correctly with zero values for all parameters. + /// + [Fact] + public void Clamp_AllZeroValues_ReturnsZero() + { + // Arrange + int value = 0; + int min = 0; + int max = 0; + + // Act + int result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(0, result); + } + + /// + /// Tests that Clamp works with negative values for all parameters. + /// + [Theory] + [InlineData(-15, -20, -10, -15)] + [InlineData(-25, -20, -10, -20)] + [InlineData(-5, -20, -10, -10)] + public void Clamp_NegativeValues_ClampsCorrectly(int value, int min, int max, int expected) + { + // Act + int result = GenericMath.Clamp(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that IsInRange returns true when value is within the inclusive range [min, max]. + /// + /// The value to test. + /// The minimum bound. + /// The maximum bound. + [Theory] + [InlineData(5, 1, 10)] + [InlineData(0, -10, 10)] + [InlineData(-5, -10, 0)] + [InlineData(100, 100, 200)] + [InlineData(200, 100, 200)] + [InlineData(0, 0, 0)] + public void IsInRange_ValueWithinRange_ReturnsTrue(int value, int min, int max) + { + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.True(result); + } + + /// + /// Tests that IsInRange returns false when value is outside the range [min, max]. + /// + /// The value to test. + /// The minimum bound. + /// The maximum bound. + [Theory] + [InlineData(0, 1, 10)] + [InlineData(11, 1, 10)] + [InlineData(-11, -10, 0)] + [InlineData(1, 2, 10)] + [InlineData(99, 100, 200)] + [InlineData(201, 100, 200)] + public void IsInRange_ValueOutsideRange_ReturnsFalse(int value, int min, int max) + { + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.False(result); + } + + /// + /// Tests that IsInRange correctly handles boundary values including int.MinValue and int.MaxValue. + /// + /// The value to test. + /// The minimum bound. + /// The maximum bound. + /// The expected result. + [Theory] + [InlineData(int.MinValue, int.MinValue, int.MaxValue, true)] + [InlineData(int.MaxValue, int.MinValue, int.MaxValue, true)] + [InlineData(0, int.MinValue, int.MaxValue, true)] + [InlineData(int.MinValue, int.MinValue, int.MinValue, true)] + [InlineData(int.MaxValue, int.MaxValue, int.MaxValue, true)] + [InlineData(int.MinValue, 0, int.MaxValue, false)] + [InlineData(int.MaxValue, int.MinValue, 0, false)] + public void IsInRange_BoundaryValues_ReturnsExpected(int value, int min, int max, bool expected) + { + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that IsInRange works correctly with double precision floating point values. + /// + /// The value to test. + /// The minimum bound. + /// The maximum bound. + /// The expected result. + [Theory] + [InlineData(5.5, 1.0, 10.0, true)] + [InlineData(1.0, 1.0, 10.0, true)] + [InlineData(10.0, 1.0, 10.0, true)] + [InlineData(0.5, 1.0, 10.0, false)] + [InlineData(10.5, 1.0, 10.0, false)] + [InlineData(-0.001, -0.001, 0.001, true)] + public void IsInRange_DoubleValues_ReturnsExpected(double value, double min, double max, bool expected) + { + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that IsInRange handles double.NaN correctly. NaN comparisons always return false. + /// + [Fact] + public void IsInRange_ValueIsNaN_ReturnsFalse() + { + // Arrange + double value = double.NaN; + double min = 0.0; + double max = 10.0; + + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.False(result); + } + + /// + /// Tests that IsInRange handles double.PositiveInfinity correctly. + /// + /// The value to test. + /// The minimum bound. + /// The maximum bound. + /// The expected result. + [Theory] + [InlineData(double.PositiveInfinity, 0.0, double.PositiveInfinity, true)] + [InlineData(double.PositiveInfinity, 0.0, 100.0, false)] + [InlineData(5.0, 0.0, double.PositiveInfinity, true)] + public void IsInRange_PositiveInfinity_ReturnsExpected(double value, double min, double max, bool expected) + { + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that IsInRange handles double.NegativeInfinity correctly. + /// + /// The value to test. + /// The minimum bound. + /// The maximum bound. + /// The expected result. + [Theory] + [InlineData(double.NegativeInfinity, double.NegativeInfinity, 0.0, true)] + [InlineData(double.NegativeInfinity, -100.0, 0.0, false)] + [InlineData(5.0, double.NegativeInfinity, 10.0, true)] + public void IsInRange_NegativeInfinity_ReturnsExpected(double value, double min, double max, bool expected) + { + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that IsInRange works correctly with mixed numeric types, relying on CreateSaturating conversions. + /// + [Fact] + public void IsInRange_MixedNumericTypes_IntValueShortMinLongMax_ReturnsTrue() + { + // Arrange + int value = 1000; + short min = 100; + long max = 10000L; + + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.True(result); + } + + /// + /// Tests that IsInRange with mixed types returns false when value is outside range. + /// + [Fact] + public void IsInRange_MixedNumericTypes_IntValueOutsideRange_ReturnsFalse() + { + // Arrange + int value = 50; + short min = 100; + long max = 10000L; + + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.False(result); + } + + /// + /// Tests that IsInRange handles saturation when min/max types have larger range than TValue. + /// When converting a value larger than byte.MaxValue to byte, it saturates to 255. + /// + [Fact] + public void IsInRange_TypeSaturation_MaxExceedsByteRange_Saturates() + { + // Arrange + byte value = 200; + int min = -100; // Saturates to 0 when converted to byte + int max = 300; // Saturates to 255 when converted to byte + + // Act + // After saturation: IsInRange(200, 0, 255) + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.True(result); + } + + /// + /// Tests that IsInRange handles saturation when min is negative but TValue is unsigned. + /// + [Fact] + public void IsInRange_TypeSaturation_NegativeMinWithUnsignedValue_Saturates() + { + // Arrange + byte value = 5; + int min = -100; // Saturates to 0 when converted to byte + int max = 10; + + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.True(result); + } + + /// + /// Tests that IsInRange returns false for all values when min > max (invalid range). + /// + /// The value to test. + [Theory] + [InlineData(0)] + [InlineData(5)] + [InlineData(10)] + [InlineData(15)] + [InlineData(-5)] + public void IsInRange_MinGreaterThanMax_ReturnsFalse(int value) + { + // Arrange + int min = 10; + int max = 5; + + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.False(result); + } + + /// + /// Tests that IsInRange works correctly with decimal type values. + /// + /// The value to test. + /// The minimum bound. + /// The maximum bound. + /// The expected result. + [Theory] + [InlineData(5.5, 1.0, 10.0, true)] + [InlineData(0.5, 1.0, 10.0, false)] + [InlineData(10.5, 1.0, 10.0, false)] + public void IsInRange_DecimalValues_ReturnsExpected(double valueDouble, double minDouble, double maxDouble, bool expected) + { + // Arrange + decimal value = (decimal)valueDouble; + decimal min = (decimal)minDouble; + decimal max = (decimal)maxDouble; + + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that IsInRange works correctly with float type values. + /// + /// The value to test. + /// The minimum bound. + /// The maximum bound. + /// The expected result. + [Theory] + [InlineData(5.5f, 1.0f, 10.0f, true)] + [InlineData(1.0f, 1.0f, 10.0f, true)] + [InlineData(10.0f, 1.0f, 10.0f, true)] + [InlineData(0.5f, 1.0f, 10.0f, false)] + [InlineData(10.5f, 1.0f, 10.0f, false)] + public void IsInRange_FloatValues_ReturnsExpected(float value, float min, float max, bool expected) + { + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that IsInRange handles float.NaN correctly. NaN comparisons always return false. + /// + [Fact] + public void IsInRange_FloatNaN_ReturnsFalse() + { + // Arrange + float value = float.NaN; + float min = 0.0f; + float max = 10.0f; + + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.False(result); + } + + /// + /// Tests that IsInRange works with long type values and boundaries. + /// + [Fact] + public void IsInRange_LongBoundaryValues_ReturnsTrue() + { + // Arrange + long value = long.MaxValue; + long min = long.MinValue; + long max = long.MaxValue; + + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.True(result); + } + + /// + /// Tests that IsInRange works with negative values across the range. + /// + /// The value to test. + /// The minimum bound. + /// The maximum bound. + /// The expected result. + [Theory] + [InlineData(-5, -10, -1, true)] + [InlineData(-10, -10, -1, true)] + [InlineData(-1, -10, -1, true)] + [InlineData(0, -10, -1, false)] + [InlineData(-11, -10, -1, false)] + public void IsInRange_NegativeValues_ReturnsExpected(int value, int min, int max, bool expected) + { + // Act + bool result = GenericMath.IsInRange(value, min, max); + + // Assert + Assert.Equal(expected, result); + } } \ No newline at end of file diff --git a/FTMS.NET.Tests/Utils/UInt24Tests.cs b/FTMS.NET.Tests/Utils/UInt24Tests.cs new file mode 100644 index 0000000..8035e85 --- /dev/null +++ b/FTMS.NET.Tests/Utils/UInt24Tests.cs @@ -0,0 +1,37 @@ +namespace FTMS.NET.Tests.Utils; + +using FTMS.NET.Utils; + +/// +/// Unit tests for the struct. +/// +public sealed class UInt24Tests +{ + /// + /// Tests that GetBytes returns the value encoded as three little-endian bytes (LSO...MSO). + /// + [Theory] + [InlineData(0u, new byte[] { 0x00, 0x00, 0x00 })] + [InlineData(1000u, new byte[] { 0xE8, 0x03, 0x00 })] // 0x0003E8 + [InlineData(0x010203u, new byte[] { 0x03, 0x02, 0x01 })] + [InlineData(0xFFFFFFu, new byte[] { 0xFF, 0xFF, 0xFF })] + public void GetBytes_ReturnsLittleEndianBytes(uint value, byte[] expected) + { + var u24 = new UInt24(value); + + byte[] actual = u24.GetBytes(); + + Assert.Equal(expected, actual); + } + + /// + /// Tests that GetBytes on MaxValue returns three bytes of 0xFF. + /// + [Fact] + public void GetBytes_MaxValue_ReturnsThreeBytesOfFF() + { + byte[] actual = UInt24.MaxValue.GetBytes(); + + Assert.Equal(new byte[] { 0xFF, 0xFF, 0xFF }, actual); + } +} \ No newline at end of file diff --git a/FTMS.NET.Tests/Utils/ValueCalculationTests.cs b/FTMS.NET.Tests/Utils/ValueCalculationTests.cs index 418c848..08475a5 100644 --- a/FTMS.NET.Tests/Utils/ValueCalculationTests.cs +++ b/FTMS.NET.Tests/Utils/ValueCalculationTests.cs @@ -1,257 +1,257 @@ -namespace FTMS.NET.Tests.Utils; - -using FTMS.NET.Utils; - -/// -/// Unit tests for the record's Calculate method. -/// -public sealed partial class ValueCalculationTests -{ - /// - /// Tests that Calculate returns the correct result with default parameters (multiplier=1, exponents=0). - /// The constant multiplier should be 1, so the result should equal the raw value. - /// - [Theory] - [InlineData(0L, 0.0)] - [InlineData(1L, 1.0)] - [InlineData(-1L, -1.0)] - [InlineData(100L, 100.0)] - [InlineData(-100L, -100.0)] - [InlineData(long.MaxValue, 9223372036854775807.0)] - [InlineData(long.MinValue, -9223372036854775808.0)] - public void Calculate_WithDefaultParameters_ReturnsRawValue(long rawValue, double expected) - { - // Arrange - var calculation = new ValueCalculation(); - - // Act - double result = calculation.Calculate(rawValue); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Calculate returns zero when the raw value is zero, regardless of the multiplier. - /// - [Theory] - [InlineData(1, 0, 0)] - [InlineData(5, 0, 0)] - [InlineData(-5, 0, 0)] - [InlineData(100, 2, 3)] - [InlineData(int.MaxValue, 10, 10)] - public void Calculate_WithZeroRawValue_ReturnsZero(int multiplier, int decimalExponent, int binaryExponent) - { - // Arrange - var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); - - // Act - double result = calculation.Calculate(0L); - - // Assert - Assert.Equal(0.0, result); - } - - /// - /// Tests that Calculate correctly applies the multiplier to the raw value. - /// With decimal and binary exponents at 0, the constant multiplier equals the multiplier parameter. - /// - [Theory] - [InlineData(2, 10L, 20.0)] - [InlineData(5, 3L, 15.0)] - [InlineData(-2, 10L, -20.0)] - [InlineData(-3, -5L, 15.0)] - [InlineData(0, 100L, 0.0)] - [InlineData(1, long.MaxValue, 9223372036854775807.0)] - [InlineData(10, 100L, 1000.0)] - public void Calculate_WithMultiplier_ReturnsScaledValue(int multiplier, long rawValue, double expected) - { - // Arrange - var calculation = new ValueCalculation(Multiplier: multiplier, DecimalExponent: 0, BinaryExponent: 0); - - // Act - double result = calculation.Calculate(rawValue); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Calculate correctly applies the decimal exponent (power of 10). - /// The constant multiplier should be multiplied by 10^decimalExponent. - /// - [Theory] - [InlineData(1, 2, 0, 5L, 500.0)] // 5 * (1 * 10^2 * 2^0) = 5 * 100 = 500 - [InlineData(1, 3, 0, 2L, 2000.0)] // 2 * (1 * 10^3 * 2^0) = 2 * 1000 = 2000 - [InlineData(1, -2, 0, 100L, 1.0)] // 100 * (1 * 10^-2 * 2^0) = 100 * 0.01 = 1 - [InlineData(2, 1, 0, 5L, 100.0)] // 5 * (2 * 10^1 * 2^0) = 5 * 20 = 100 - [InlineData(1, 0, 0, 10L, 10.0)] // 10 * (1 * 10^0 * 2^0) = 10 * 1 = 10 - public void Calculate_WithDecimalExponent_ReturnsCorrectValue(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) - { - // Arrange - var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); - - // Act - double result = calculation.Calculate(rawValue); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Calculate correctly applies the binary exponent (power of 2). - /// The constant multiplier should be multiplied by 2^binaryExponent. - /// - [Theory] - [InlineData(1, 0, 3, 5L, 40.0)] // 5 * (1 * 10^0 * 2^3) = 5 * 8 = 40 - [InlineData(1, 0, 4, 2L, 32.0)] // 2 * (1 * 10^0 * 2^4) = 2 * 16 = 32 - [InlineData(1, 0, -2, 16L, 4.0)] // 16 * (1 * 10^0 * 2^-2) = 16 * 0.25 = 4 - [InlineData(2, 0, 2, 5L, 40.0)] // 5 * (2 * 10^0 * 2^2) = 5 * 8 = 40 - [InlineData(1, 0, 0, 10L, 10.0)] // 10 * (1 * 10^0 * 2^0) = 10 * 1 = 10 - public void Calculate_WithBinaryExponent_ReturnsCorrectValue(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) - { - // Arrange - var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); - - // Act - double result = calculation.Calculate(rawValue); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Calculate correctly combines all three parameters (multiplier, decimal exponent, binary exponent). - /// - [Theory] - [InlineData(2, 1, 2, 5L, 400.0)] // 5 * (2 * 10^1 * 2^2) = 5 * (2 * 10 * 4) = 5 * 80 = 400 - [InlineData(3, 2, 3, 1L, 2400.0)] // 1 * (3 * 10^2 * 2^3) = 3 * 100 * 8 = 2400 - [InlineData(5, -1, 1, 10L, 10.0)] // 10 * (5 * 10^-1 * 2^1) = 10 * (0.5 * 2) = 10 * 1 = 10 - [InlineData(1, 1, 1, 50L, 1000.0)] // 50 * (1 * 10^1 * 2^1) = 50 * 20 = 1000 - [InlineData(-2, 1, 1, 5L, -200.0)] // 5 * (-2 * 10^1 * 2^1) = 5 * (-40) = -200 - public void Calculate_WithCombinedParameters_ReturnsCorrectValue(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) - { - // Arrange - var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); - - // Act - double result = calculation.Calculate(rawValue); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Calculate handles negative raw values correctly. - /// The sign of the raw value should be preserved in the result. - /// - [Theory] - [InlineData(1, 0, 0, -10L, -10.0)] - [InlineData(2, 0, 0, -5L, -10.0)] - [InlineData(1, 2, 0, -1L, -100.0)] - [InlineData(-2, 0, 0, -10L, 20.0)] // Negative multiplier with negative rawValue = positive - [InlineData(5, 1, 1, -2L, -200.0)] - public void Calculate_WithNegativeRawValue_ReturnsCorrectSignedResult(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) - { - // Arrange - var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); - - // Act - double result = calculation.Calculate(rawValue); - - // Assert - Assert.Equal(expected, result); - } - - /// - /// Tests that Calculate handles extreme raw values (long.MinValue and long.MaxValue) correctly. - /// These tests verify that the calculation doesn't cause unexpected overflow behavior. - /// - [Theory] - [InlineData(2, 0, 0, long.MaxValue, 1.8446744073709552E+19)] // long.MaxValue * 2 - [InlineData(2, 0, 0, long.MinValue, -1.8446744073709552E+19)] // long.MinValue * 2 - [InlineData(1, 1, 0, long.MaxValue, 9.223372036854776E+19)] // long.MaxValue * 10 - [InlineData(1, -10, 0, long.MaxValue, 922337203.6854776)] // long.MaxValue * 10^-10 - public void Calculate_WithExtremeRawValues_ReturnsCorrectValue(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) - { - // Arrange - var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); - - // Act - double result = calculation.Calculate(rawValue); - - // Assert - Assert.Equal(expected, result, precision: 10); - } - - /// - /// Tests that Calculate can produce positive infinity when the result exceeds double's maximum value. - /// - [Fact] - public void Calculate_WithVeryLargeMultiplier_ReturnsPositiveInfinity() - { - // Arrange - var calculation = new ValueCalculation(Multiplier: 1, DecimalExponent: 300, BinaryExponent: 10); - - // Act - double result = calculation.Calculate(long.MaxValue); - - // Assert - Assert.Equal(double.PositiveInfinity, result); - } - - /// - /// Tests that Calculate can produce negative infinity when the result is below double's minimum value. - /// - [Fact] - public void Calculate_WithVeryLargeMultiplierAndNegativeValue_ReturnsNegativeInfinity() - { - // Arrange - var calculation = new ValueCalculation(Multiplier: 1, DecimalExponent: 300, BinaryExponent: 10); - - // Act - double result = calculation.Calculate(long.MinValue); - - // Assert - Assert.Equal(double.NegativeInfinity, result); - } - - /// - /// Tests that Calculate returns values very close to zero when using large negative exponents. - /// - [Theory] - [InlineData(1, -300, 0, 100L)] - [InlineData(1, 0, -1000, 100L)] - [InlineData(1, -150, -150, 1000L)] - public void Calculate_WithVerySmallMultiplier_ReturnsValueCloseToZero(int multiplier, int decimalExponent, int binaryExponent, long rawValue) - { - // Arrange - var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); - - // Act - double result = calculation.Calculate(rawValue); - - // Assert - Assert.True(result >= 0.0 && result < 1e-100); - } - - /// - /// Tests that Calculate with a zero multiplier always returns zero. - /// - [Theory] - [InlineData(0, 0, 0, 100L)] - [InlineData(0, 5, 3, long.MaxValue)] - [InlineData(0, -5, -3, long.MinValue)] - [InlineData(0, 10, 10, -1000L)] - public void Calculate_WithZeroMultiplier_ReturnsZero(int multiplier, int decimalExponent, int binaryExponent, long rawValue) - { - // Arrange - var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); - - // Act - double result = calculation.Calculate(rawValue); - - // Assert - Assert.Equal(0.0, result); - } +namespace FTMS.NET.Tests.Utils; + +using FTMS.NET.Utils; + +/// +/// Unit tests for the record's Calculate method. +/// +public sealed partial class ValueCalculationTests +{ + /// + /// Tests that Calculate returns the correct result with default parameters (multiplier=1, exponents=0). + /// The constant multiplier should be 1, so the result should equal the raw value. + /// + [Theory] + [InlineData(0L, 0.0)] + [InlineData(1L, 1.0)] + [InlineData(-1L, -1.0)] + [InlineData(100L, 100.0)] + [InlineData(-100L, -100.0)] + [InlineData(long.MaxValue, 9223372036854775807.0)] + [InlineData(long.MinValue, -9223372036854775808.0)] + public void Calculate_WithDefaultParameters_ReturnsRawValue(long rawValue, double expected) + { + // Arrange + var calculation = new ValueCalculation(); + + // Act + double result = calculation.Calculate(rawValue); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Calculate returns zero when the raw value is zero, regardless of the multiplier. + /// + [Theory] + [InlineData(1, 0, 0)] + [InlineData(5, 0, 0)] + [InlineData(-5, 0, 0)] + [InlineData(100, 2, 3)] + [InlineData(int.MaxValue, 10, 10)] + public void Calculate_WithZeroRawValue_ReturnsZero(int multiplier, int decimalExponent, int binaryExponent) + { + // Arrange + var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); + + // Act + double result = calculation.Calculate(0L); + + // Assert + Assert.Equal(0.0, result); + } + + /// + /// Tests that Calculate correctly applies the multiplier to the raw value. + /// With decimal and binary exponents at 0, the constant multiplier equals the multiplier parameter. + /// + [Theory] + [InlineData(2, 10L, 20.0)] + [InlineData(5, 3L, 15.0)] + [InlineData(-2, 10L, -20.0)] + [InlineData(-3, -5L, 15.0)] + [InlineData(0, 100L, 0.0)] + [InlineData(1, long.MaxValue, 9223372036854775807.0)] + [InlineData(10, 100L, 1000.0)] + public void Calculate_WithMultiplier_ReturnsScaledValue(int multiplier, long rawValue, double expected) + { + // Arrange + var calculation = new ValueCalculation(Multiplier: multiplier, DecimalExponent: 0, BinaryExponent: 0); + + // Act + double result = calculation.Calculate(rawValue); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Calculate correctly applies the decimal exponent (power of 10). + /// The constant multiplier should be multiplied by 10^decimalExponent. + /// + [Theory] + [InlineData(1, 2, 0, 5L, 500.0)] // 5 * (1 * 10^2 * 2^0) = 5 * 100 = 500 + [InlineData(1, 3, 0, 2L, 2000.0)] // 2 * (1 * 10^3 * 2^0) = 2 * 1000 = 2000 + [InlineData(1, -2, 0, 100L, 1.0)] // 100 * (1 * 10^-2 * 2^0) = 100 * 0.01 = 1 + [InlineData(2, 1, 0, 5L, 100.0)] // 5 * (2 * 10^1 * 2^0) = 5 * 20 = 100 + [InlineData(1, 0, 0, 10L, 10.0)] // 10 * (1 * 10^0 * 2^0) = 10 * 1 = 10 + public void Calculate_WithDecimalExponent_ReturnsCorrectValue(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) + { + // Arrange + var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); + + // Act + double result = calculation.Calculate(rawValue); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Calculate correctly applies the binary exponent (power of 2). + /// The constant multiplier should be multiplied by 2^binaryExponent. + /// + [Theory] + [InlineData(1, 0, 3, 5L, 40.0)] // 5 * (1 * 10^0 * 2^3) = 5 * 8 = 40 + [InlineData(1, 0, 4, 2L, 32.0)] // 2 * (1 * 10^0 * 2^4) = 2 * 16 = 32 + [InlineData(1, 0, -2, 16L, 4.0)] // 16 * (1 * 10^0 * 2^-2) = 16 * 0.25 = 4 + [InlineData(2, 0, 2, 5L, 40.0)] // 5 * (2 * 10^0 * 2^2) = 5 * 8 = 40 + [InlineData(1, 0, 0, 10L, 10.0)] // 10 * (1 * 10^0 * 2^0) = 10 * 1 = 10 + public void Calculate_WithBinaryExponent_ReturnsCorrectValue(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) + { + // Arrange + var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); + + // Act + double result = calculation.Calculate(rawValue); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Calculate correctly combines all three parameters (multiplier, decimal exponent, binary exponent). + /// + [Theory] + [InlineData(2, 1, 2, 5L, 400.0)] // 5 * (2 * 10^1 * 2^2) = 5 * (2 * 10 * 4) = 5 * 80 = 400 + [InlineData(3, 2, 3, 1L, 2400.0)] // 1 * (3 * 10^2 * 2^3) = 3 * 100 * 8 = 2400 + [InlineData(5, -1, 1, 10L, 10.0)] // 10 * (5 * 10^-1 * 2^1) = 10 * (0.5 * 2) = 10 * 1 = 10 + [InlineData(1, 1, 1, 50L, 1000.0)] // 50 * (1 * 10^1 * 2^1) = 50 * 20 = 1000 + [InlineData(-2, 1, 1, 5L, -200.0)] // 5 * (-2 * 10^1 * 2^1) = 5 * (-40) = -200 + public void Calculate_WithCombinedParameters_ReturnsCorrectValue(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) + { + // Arrange + var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); + + // Act + double result = calculation.Calculate(rawValue); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Calculate handles negative raw values correctly. + /// The sign of the raw value should be preserved in the result. + /// + [Theory] + [InlineData(1, 0, 0, -10L, -10.0)] + [InlineData(2, 0, 0, -5L, -10.0)] + [InlineData(1, 2, 0, -1L, -100.0)] + [InlineData(-2, 0, 0, -10L, 20.0)] // Negative multiplier with negative rawValue = positive + [InlineData(5, 1, 1, -2L, -200.0)] + public void Calculate_WithNegativeRawValue_ReturnsCorrectSignedResult(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) + { + // Arrange + var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); + + // Act + double result = calculation.Calculate(rawValue); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Tests that Calculate handles extreme raw values (long.MinValue and long.MaxValue) correctly. + /// These tests verify that the calculation doesn't cause unexpected overflow behavior. + /// + [Theory] + [InlineData(2, 0, 0, long.MaxValue, 1.8446744073709552E+19)] // long.MaxValue * 2 + [InlineData(2, 0, 0, long.MinValue, -1.8446744073709552E+19)] // long.MinValue * 2 + [InlineData(1, 1, 0, long.MaxValue, 9.223372036854776E+19)] // long.MaxValue * 10 + [InlineData(1, -10, 0, long.MaxValue, 922337203.6854776)] // long.MaxValue * 10^-10 + public void Calculate_WithExtremeRawValues_ReturnsCorrectValue(int multiplier, int decimalExponent, int binaryExponent, long rawValue, double expected) + { + // Arrange + var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); + + // Act + double result = calculation.Calculate(rawValue); + + // Assert + Assert.Equal(expected, result, precision: 10); + } + + /// + /// Tests that Calculate can produce positive infinity when the result exceeds double's maximum value. + /// + [Fact] + public void Calculate_WithVeryLargeMultiplier_ReturnsPositiveInfinity() + { + // Arrange + var calculation = new ValueCalculation(Multiplier: 1, DecimalExponent: 300, BinaryExponent: 10); + + // Act + double result = calculation.Calculate(long.MaxValue); + + // Assert + Assert.Equal(double.PositiveInfinity, result); + } + + /// + /// Tests that Calculate can produce negative infinity when the result is below double's minimum value. + /// + [Fact] + public void Calculate_WithVeryLargeMultiplierAndNegativeValue_ReturnsNegativeInfinity() + { + // Arrange + var calculation = new ValueCalculation(Multiplier: 1, DecimalExponent: 300, BinaryExponent: 10); + + // Act + double result = calculation.Calculate(long.MinValue); + + // Assert + Assert.Equal(double.NegativeInfinity, result); + } + + /// + /// Tests that Calculate returns values very close to zero when using large negative exponents. + /// + [Theory] + [InlineData(1, -300, 0, 100L)] + [InlineData(1, 0, -1000, 100L)] + [InlineData(1, -150, -150, 1000L)] + public void Calculate_WithVerySmallMultiplier_ReturnsValueCloseToZero(int multiplier, int decimalExponent, int binaryExponent, long rawValue) + { + // Arrange + var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); + + // Act + double result = calculation.Calculate(rawValue); + + // Assert + Assert.True(result >= 0.0 && result < 1e-100); + } + + /// + /// Tests that Calculate with a zero multiplier always returns zero. + /// + [Theory] + [InlineData(0, 0, 0, 100L)] + [InlineData(0, 5, 3, long.MaxValue)] + [InlineData(0, -5, -3, long.MinValue)] + [InlineData(0, 10, 10, -1000L)] + public void Calculate_WithZeroMultiplier_ReturnsZero(int multiplier, int decimalExponent, int binaryExponent, long rawValue) + { + // Arrange + var calculation = new ValueCalculation(multiplier, decimalExponent, binaryExponent); + + // Act + double result = calculation.Calculate(rawValue); + + // Assert + Assert.Equal(0.0, result); + } } \ No newline at end of file diff --git a/FTMS.NET/Control/ControlExtensions.cs b/FTMS.NET/Control/ControlExtensions.cs index c21df59..67c8483 100644 --- a/FTMS.NET/Control/ControlExtensions.cs +++ b/FTMS.NET/Control/ControlExtensions.cs @@ -1,142 +1,145 @@ -namespace FTMS.NET.Control; - -using FTMS.NET.Utils; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading.Tasks; - -public static class ControlExtensions -{ - public static Task RequestControl(this IFitnessMachineControl fitnessMachineControl) - => fitnessMachineControl.ExecuteWithoutValue(EControlOpCode.RequestControl); - - public static Task Reset(this IFitnessMachineControl fitnessMachineControl) - => fitnessMachineControl.ExecuteWithoutValue(EControlOpCode.Reset); - - public static Task SetTargetSpeed(this IFitnessMachineControl fitnessMachineControl, ushort speed) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetSpeed, speed); - - public static Task SetTargetInclination(this IFitnessMachineControl fitnessMachineControl, short inclination) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetInclination, inclination); - - public static Task SetTargetResistanceLevel(this IFitnessMachineControl fitnessMachineControl, byte resistanceLevel) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetResistanceLevel, resistanceLevel); - - public static Task SetTargetPower(this IFitnessMachineControl fitnessMachineControl, short power) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetPower, power); - - public static Task SetTargetHeartRate(this IFitnessMachineControl fitnessMachineControl, byte beatsPerMinute) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetHeartRate, beatsPerMinute); - - public static Task StartOrResume(this IFitnessMachineControl fitnessMachineControl) - => fitnessMachineControl.ExecuteWithoutValue(EControlOpCode.StartOrResume); - - public static Task Stop(this IFitnessMachineControl fitnessMachineControl) - => fitnessMachineControl.StopOrPause(0x01); - - public static Task Pause(this IFitnessMachineControl fitnessMachineControl) - => fitnessMachineControl.StopOrPause(0x02); - - public static Task SetTargetedExpendedEnergy(this IFitnessMachineControl fitnessMachineControl, ushort expendedEnergy) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedExpendedEnergy, expendedEnergy); - - public static Task SetTargetedNumberOfSteps(this IFitnessMachineControl fitnessMachineControl, ushort steps) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedNumberOfSteps, steps); - - public static Task SetTargetedNumberOfStrides(this IFitnessMachineControl fitnessMachineControl, ushort strides) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedNumberOfStrides, strides); - - public static Task SetTargetedDistance(this IFitnessMachineControl fitnessMachineControl, UInt24 distance) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedDistance, distance); - - public static Task SetTargetedTrainingTime(this IFitnessMachineControl fitnessMachineControl, ushort time) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedTrainingTime, time); - - public static Task SetTargetedTimeInTwoHeartRateZones(this IFitnessMachineControl fitnessMachineControl, ushort timeInFatBurnZone, ushort timeInFitnessZone) - => fitnessMachineControl.ExecuteWithMultipleValues(EControlOpCode.SetTargetedTimeInTwoHeartRateZones, timeInFatBurnZone, timeInFitnessZone); - - public static Task SetTargetedTimeInThreeHeartRateZones(this IFitnessMachineControl fitnessMachineControl, ushort timeInLightZone, ushort timeInModerateZone, ushort timeInHardZone) - => fitnessMachineControl.ExecuteWithMultipleValues(EControlOpCode.SetTargetedTimeInThreeHeartRateZones, timeInLightZone, timeInModerateZone, timeInHardZone); - - public static Task SetTargetedTimeInFiveHeartRateZones(this IFitnessMachineControl fitnessMachineControl, ushort timeInVeryLightZone, ushort timeInLightZone, ushort timeInModerateZone, ushort timeInHardZone, ushort timeInMaximumZone) - => fitnessMachineControl.ExecuteWithMultipleValues(EControlOpCode.SetTargetedTimeInFiveHeartRateZones, timeInVeryLightZone, timeInLightZone, timeInModerateZone, timeInHardZone, timeInMaximumZone); - - public static Task SetIndoorBikeSimulationParameters(this IFitnessMachineControl fitnessMachineControl, short windspeed, short grade, byte crr, byte cw) - { - var windspeedBytes = BitConverter.GetBytes(windspeed); - var gradeBytes = BitConverter.GetBytes(grade); - - byte[] parameterBytes = [.. windspeedBytes, .. gradeBytes, crr, cw]; - var request = new ControlRequest(EControlOpCode.SetIndoorBikeSimulationParameters, parameterBytes); - return fitnessMachineControl.Execute(request); - } - - public static Task SetWheelCircumference(this IFitnessMachineControl fitnessMachineControl, ushort wheelCircumference) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetWheelCircumference, wheelCircumference); - - public static async Task<(ushort targetSpeedLow, ushort targetSpeedHigh)> StartSpinDownControl(this IFitnessMachineControl fitnessMachineControl) - { - var response = await fitnessMachineControl.SendSpinDownControl(0x01); - using MemoryStream responseStream = new(response.ResponseParameter); - using BinaryReader responseReader = new(responseStream); - - ushort targetSpeedLow = responseReader.ReadUInt16(); - ushort targetSpeedHigh = responseReader.ReadUInt16(); - - return (targetSpeedLow, targetSpeedHigh); - } - - public static Task IgnoreSpinDownControl(this IFitnessMachineControl fitnessMachineControl) - => fitnessMachineControl.SendSpinDownControl(0x02); - - private static Task SendSpinDownControl(this IFitnessMachineControl fitnessMachineControl, byte startOrIgnore) - { - var request = new ControlRequest(EControlOpCode.SpinDownControl, [startOrIgnore]); - return fitnessMachineControl.Execute(request); - } - - public static Task SetTargetedCadence(this IFitnessMachineControl fitnessMachineControl, ushort cadence) - => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedCadence, cadence); - - private static Task ExecuteWithoutValue(this IFitnessMachineControl fitnessMachineControl, EControlOpCode opCode) - { - var request = new ControlRequest(opCode, []); - return fitnessMachineControl.Execute(request); - } - - private static Task ExecuteWithValue(this IFitnessMachineControl fitnessMachineControl, EControlOpCode opCode, T value) - { - var valueBytes = GetBytes(); - var request = new ControlRequest(opCode, valueBytes); - return fitnessMachineControl.Execute(request); - - byte[] GetBytes() - { - if (typeof(T).IsPrimitive) - { - int size = Marshal.SizeOf(); - byte[] bytes = new byte[size]; - Unsafe.As(ref bytes[0]) = value; - return bytes; - } - - throw new InvalidOperationException(); - } - } - - private static Task ExecuteWithMultipleValues( - this IFitnessMachineControl fitnessMachineControl, - EControlOpCode opCode, - params ushort[] values) - { - var valuesBytes = values.SelectMany(BitConverter.GetBytes).ToArray(); - var request = new ControlRequest(opCode, valuesBytes); - return fitnessMachineControl.Execute(request); - } - - private static Task StopOrPause(this IFitnessMachineControl fitnessMachineControl, byte stopOrPause) - { - var request = new ControlRequest(EControlOpCode.StopOrPause, [stopOrPause]); - return fitnessMachineControl.Execute(request); - } -} +namespace FTMS.NET.Control; + +using FTMS.NET.Utils; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +public static class ControlExtensions +{ + public static Task RequestControl(this IFitnessMachineControl fitnessMachineControl) + => fitnessMachineControl.ExecuteWithoutValue(EControlOpCode.RequestControl); + + public static Task Reset(this IFitnessMachineControl fitnessMachineControl) + => fitnessMachineControl.ExecuteWithoutValue(EControlOpCode.Reset); + + public static Task SetTargetSpeed(this IFitnessMachineControl fitnessMachineControl, ushort speed) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetSpeed, speed); + + public static Task SetTargetInclination(this IFitnessMachineControl fitnessMachineControl, short inclination) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetInclination, inclination); + + public static Task SetTargetResistanceLevel(this IFitnessMachineControl fitnessMachineControl, byte resistanceLevel) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetResistanceLevel, resistanceLevel); + + public static Task SetTargetPower(this IFitnessMachineControl fitnessMachineControl, short power) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetPower, power); + + public static Task SetTargetHeartRate(this IFitnessMachineControl fitnessMachineControl, byte beatsPerMinute) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetHeartRate, beatsPerMinute); + + public static Task StartOrResume(this IFitnessMachineControl fitnessMachineControl) + => fitnessMachineControl.ExecuteWithoutValue(EControlOpCode.StartOrResume); + + public static Task Stop(this IFitnessMachineControl fitnessMachineControl) + => fitnessMachineControl.StopOrPause(0x01); + + public static Task Pause(this IFitnessMachineControl fitnessMachineControl) + => fitnessMachineControl.StopOrPause(0x02); + + public static Task SetTargetedExpendedEnergy(this IFitnessMachineControl fitnessMachineControl, ushort expendedEnergy) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedExpendedEnergy, expendedEnergy); + + public static Task SetTargetedNumberOfSteps(this IFitnessMachineControl fitnessMachineControl, ushort steps) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedNumberOfSteps, steps); + + public static Task SetTargetedNumberOfStrides(this IFitnessMachineControl fitnessMachineControl, ushort strides) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedNumberOfStrides, strides); + + public static Task SetTargetedDistance(this IFitnessMachineControl fitnessMachineControl, UInt24 distance) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedDistance, distance); + + public static Task SetTargetedTrainingTime(this IFitnessMachineControl fitnessMachineControl, ushort time) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedTrainingTime, time); + + public static Task SetTargetedTimeInTwoHeartRateZones(this IFitnessMachineControl fitnessMachineControl, ushort timeInFatBurnZone, ushort timeInFitnessZone) + => fitnessMachineControl.ExecuteWithMultipleValues(EControlOpCode.SetTargetedTimeInTwoHeartRateZones, timeInFatBurnZone, timeInFitnessZone); + + public static Task SetTargetedTimeInThreeHeartRateZones(this IFitnessMachineControl fitnessMachineControl, ushort timeInLightZone, ushort timeInModerateZone, ushort timeInHardZone) + => fitnessMachineControl.ExecuteWithMultipleValues(EControlOpCode.SetTargetedTimeInThreeHeartRateZones, timeInLightZone, timeInModerateZone, timeInHardZone); + + public static Task SetTargetedTimeInFiveHeartRateZones(this IFitnessMachineControl fitnessMachineControl, ushort timeInVeryLightZone, ushort timeInLightZone, ushort timeInModerateZone, ushort timeInHardZone, ushort timeInMaximumZone) + => fitnessMachineControl.ExecuteWithMultipleValues(EControlOpCode.SetTargetedTimeInFiveHeartRateZones, timeInVeryLightZone, timeInLightZone, timeInModerateZone, timeInHardZone, timeInMaximumZone); + + public static Task SetIndoorBikeSimulationParameters(this IFitnessMachineControl fitnessMachineControl, short windspeed, short grade, byte crr, byte cw) + { + var windspeedBytes = BitConverter.GetBytes(windspeed); + var gradeBytes = BitConverter.GetBytes(grade); + + byte[] parameterBytes = [.. windspeedBytes, .. gradeBytes, crr, cw]; + var request = new ControlRequest(EControlOpCode.SetIndoorBikeSimulationParameters, parameterBytes); + return fitnessMachineControl.Execute(request); + } + + public static Task SetWheelCircumference(this IFitnessMachineControl fitnessMachineControl, ushort wheelCircumference) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetWheelCircumference, wheelCircumference); + + public static async Task<(ushort targetSpeedLow, ushort targetSpeedHigh)> StartSpinDownControl(this IFitnessMachineControl fitnessMachineControl) + { + var response = await fitnessMachineControl.SendSpinDownControl(0x01); + using MemoryStream responseStream = new(response.ResponseParameter); + using BinaryReader responseReader = new(responseStream); + + ushort targetSpeedLow = responseReader.ReadUInt16(); + ushort targetSpeedHigh = responseReader.ReadUInt16(); + + return (targetSpeedLow, targetSpeedHigh); + } + + public static Task IgnoreSpinDownControl(this IFitnessMachineControl fitnessMachineControl) + => fitnessMachineControl.SendSpinDownControl(0x02); + + private static Task SendSpinDownControl(this IFitnessMachineControl fitnessMachineControl, byte startOrIgnore) + { + var request = new ControlRequest(EControlOpCode.SpinDownControl, [startOrIgnore]); + return fitnessMachineControl.Execute(request); + } + + public static Task SetTargetedCadence(this IFitnessMachineControl fitnessMachineControl, ushort cadence) + => fitnessMachineControl.ExecuteWithValue(EControlOpCode.SetTargetedCadence, cadence); + + private static Task ExecuteWithoutValue(this IFitnessMachineControl fitnessMachineControl, EControlOpCode opCode) + { + var request = new ControlRequest(opCode, []); + return fitnessMachineControl.Execute(request); + } + + private static Task ExecuteWithValue(this IFitnessMachineControl fitnessMachineControl, EControlOpCode opCode, T value) + { + var valueBytes = GetBytes(); + var request = new ControlRequest(opCode, valueBytes); + return fitnessMachineControl.Execute(request); + + byte[] GetBytes() + { + if (value is UInt24 u24) + return u24.GetBytes(); + + if (typeof(T).IsPrimitive) + { + int size = Unsafe.SizeOf(); + byte[] bytes = new byte[size]; + Unsafe.As(ref bytes[0]) = value; + return bytes; + } + + throw new InvalidOperationException(); + } + } + + private static Task ExecuteWithMultipleValues( + this IFitnessMachineControl fitnessMachineControl, + EControlOpCode opCode, + params ushort[] values) + { + var valuesBytes = values.SelectMany(BitConverter.GetBytes).ToArray(); + var request = new ControlRequest(opCode, valuesBytes); + return fitnessMachineControl.Execute(request); + } + + private static Task StopOrPause(this IFitnessMachineControl fitnessMachineControl, byte stopOrPause) + { + var request = new ControlRequest(EControlOpCode.StopOrPause, [stopOrPause]); + return fitnessMachineControl.Execute(request); + } +} \ No newline at end of file diff --git a/FTMS.NET/Control/ControlRequest.cs b/FTMS.NET/Control/ControlRequest.cs index 137ad4b..4c5e882 100644 --- a/FTMS.NET/Control/ControlRequest.cs +++ b/FTMS.NET/Control/ControlRequest.cs @@ -1,5 +1,5 @@ -namespace FTMS.NET.Control; - -public sealed record ControlRequest( - EControlOpCode OpCode, +namespace FTMS.NET.Control; + +public sealed record ControlRequest( + EControlOpCode OpCode, byte[] Parameter); \ No newline at end of file diff --git a/FTMS.NET/Control/ControlResponse.cs b/FTMS.NET/Control/ControlResponse.cs index 2c20611..f0f9e84 100644 --- a/FTMS.NET/Control/ControlResponse.cs +++ b/FTMS.NET/Control/ControlResponse.cs @@ -1,6 +1,6 @@ -namespace FTMS.NET.Control; - -public sealed record ControlResponse( - EControlOpCode RequestedOpCode, - EControlResultCode ResultCode, +namespace FTMS.NET.Control; + +public sealed record ControlResponse( + EControlOpCode RequestedOpCode, + EControlResultCode ResultCode, byte[] ResponseParameter); \ No newline at end of file diff --git a/FTMS.NET/Control/EControlOpCode.cs b/FTMS.NET/Control/EControlOpCode.cs index 15804bd..d3af188 100644 --- a/FTMS.NET/Control/EControlOpCode.cs +++ b/FTMS.NET/Control/EControlOpCode.cs @@ -1,25 +1,26 @@ -namespace FTMS.NET.Control; -public enum EControlOpCode : byte -{ - RequestControl = 0x00, - Reset = 0x01, - SetTargetSpeed = 0x02, - SetTargetInclination = 0x03, - SetTargetResistanceLevel = 0x04, - SetTargetPower = 0x05, - SetTargetHeartRate = 0x06, - StartOrResume = 0x07, - StopOrPause = 0x08, - SetTargetedExpendedEnergy = 0x09, - SetTargetedNumberOfSteps = 0x0A, - SetTargetedNumberOfStrides = 0x0B, - SetTargetedDistance = 0x0C, - SetTargetedTrainingTime = 0x0D, - SetTargetedTimeInTwoHeartRateZones = 0x0E, - SetTargetedTimeInThreeHeartRateZones = 0x0F, - SetTargetedTimeInFiveHeartRateZones = 0x10, - SetIndoorBikeSimulationParameters = 0x11, - SetWheelCircumference = 0x12, - SpinDownControl = 0x13, - SetTargetedCadence = 0x14, -} +namespace FTMS.NET.Control; + +public enum EControlOpCode : byte +{ + RequestControl = 0x00, + Reset = 0x01, + SetTargetSpeed = 0x02, + SetTargetInclination = 0x03, + SetTargetResistanceLevel = 0x04, + SetTargetPower = 0x05, + SetTargetHeartRate = 0x06, + StartOrResume = 0x07, + StopOrPause = 0x08, + SetTargetedExpendedEnergy = 0x09, + SetTargetedNumberOfSteps = 0x0A, + SetTargetedNumberOfStrides = 0x0B, + SetTargetedDistance = 0x0C, + SetTargetedTrainingTime = 0x0D, + SetTargetedTimeInTwoHeartRateZones = 0x0E, + SetTargetedTimeInThreeHeartRateZones = 0x0F, + SetTargetedTimeInFiveHeartRateZones = 0x10, + SetIndoorBikeSimulationParameters = 0x11, + SetWheelCircumference = 0x12, + SpinDownControl = 0x13, + SetTargetedCadence = 0x14, +} \ No newline at end of file diff --git a/FTMS.NET/Control/EControlResultCode.cs b/FTMS.NET/Control/EControlResultCode.cs index bc34714..65d77a1 100644 --- a/FTMS.NET/Control/EControlResultCode.cs +++ b/FTMS.NET/Control/EControlResultCode.cs @@ -1,9 +1,10 @@ -namespace FTMS.NET.Control; -public enum EControlResultCode : byte -{ - Success = 0x01, - OpCodeNotSupported = 0x02, - InvalidParameter = 0x03, - OperationFailed = 0x04, - ControlNotPermitted = 0x05 -} +namespace FTMS.NET.Control; + +public enum EControlResultCode : byte +{ + Success = 0x01, + OpCodeNotSupported = 0x02, + InvalidParameter = 0x03, + OperationFailed = 0x04, + ControlNotPermitted = 0x05 +} \ No newline at end of file diff --git a/FTMS.NET/Control/EStopOrPauseCode.cs b/FTMS.NET/Control/EStopOrPauseCode.cs index 0e4f35d..7a82f39 100644 --- a/FTMS.NET/Control/EStopOrPauseCode.cs +++ b/FTMS.NET/Control/EStopOrPauseCode.cs @@ -1,6 +1,7 @@ -namespace FTMS.NET.Control; -public enum EStopOrPauseCode : byte -{ - Stop = 0x01, - Pause = 0x02 -} +namespace FTMS.NET.Control; + +public enum EStopOrPauseCode : byte +{ + Stop = 0x01, + Pause = 0x02 +} \ No newline at end of file diff --git a/FTMS.NET/Control/FitnessMachineControl.cs b/FTMS.NET/Control/FitnessMachineControl.cs index efe4323..f841629 100644 --- a/FTMS.NET/Control/FitnessMachineControl.cs +++ b/FTMS.NET/Control/FitnessMachineControl.cs @@ -1,76 +1,82 @@ -namespace FTMS.NET.Control; - -using FTMS.NET.Exceptions; -using System; -using System.Linq; -using System.Reactive.Concurrency; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Threading.Tasks; -using System.Threading.Tasks; - -internal sealed class FitnessMachineControl : IFitnessMachineControl -{ - private readonly Func writeControlPoint; - private readonly CancellationDisposable cancellationDisposable = new(); - private readonly IObservable responseObservable; - - internal FitnessMachineControl( - IObservable observeControlPoint, - Func writeControlPoint, - IScheduler? scheduler = null) - { - this.writeControlPoint = writeControlPoint; - this.responseObservable = observeControlPoint - .TakeUntil(this.cancellationDisposable.Token) - .Select(this.ReadResponseData) - .Publish() - .RefCount(TimeSpan.FromSeconds(5), scheduler ?? DefaultScheduler.Instance); - } - - public async Task Execute(ControlRequest request) - { - ArgumentNullException.ThrowIfNull(request); - - var responseTask = this.responseObservable - .FirstAsync(response => response.RequestedOpCode == request.OpCode) - .ToTask(); - - byte[] writeValue = [(byte)request.OpCode, .. request.Parameter]; - - try - { - await this.writeControlPoint(writeValue); - - var response = await responseTask; - - if (response.ResultCode == EControlResultCode.Success) - return response; - - throw new ControlRequestException(request.OpCode, response.ResultCode); - } - catch (Exception ex) - { - throw new ControlRequestException(request.OpCode, ex); - } - } - - private ControlResponse ReadResponseData(byte[] data) - { - var responseCode = data[0]; - - if (responseCode != 0x80) - throw new InvalidOperationException(); - - var requestedOpCode = (EControlOpCode)data[1]; - var resultCode = (EControlResultCode)data[2]; - byte[] parameter = [.. data.Skip(3)]; - - return new ControlResponse(requestedOpCode, resultCode, parameter); - } - - public void Dispose() - { - this.cancellationDisposable.Dispose(); - } -} +namespace FTMS.NET.Control; + +using FTMS.NET.Exceptions; +using System; +using System.Linq; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Threading.Tasks; + +internal sealed class FitnessMachineControl : IFitnessMachineControl +{ + private readonly Func writeControlPoint; + private readonly CancellationDisposable cancellationDisposable = new(); + private readonly IObservable responseObservable; + private readonly TimeSpan responseTimeout; + private readonly IScheduler scheduler; + + internal FitnessMachineControl( + IObservable observeControlPoint, + Func writeControlPoint, + TimeSpan? responseTimeout = null, + IScheduler? scheduler = null) + { + this.writeControlPoint = writeControlPoint; + this.scheduler = scheduler ?? DefaultScheduler.Instance; + this.responseTimeout = responseTimeout ?? TimeSpan.FromSeconds(5); + this.responseObservable = observeControlPoint + .TakeUntil(this.cancellationDisposable.Token) + .Select(this.ReadResponseData) + .Publish() + .RefCount(TimeSpan.FromSeconds(5), this.scheduler); + } + + public async Task Execute(ControlRequest request) + { + ArgumentNullException.ThrowIfNull(request); + + var responseTask = this.responseObservable + .FirstAsync(response => response.RequestedOpCode == request.OpCode) + .Timeout(this.responseTimeout, this.scheduler) + .ToTask(); + + byte[] writeValue = [(byte)request.OpCode, .. request.Parameter]; + + try + { + await this.writeControlPoint(writeValue); + + var response = await responseTask; + + if (response.ResultCode == EControlResultCode.Success) + return response; + + throw new ControlRequestException(request.OpCode, response.ResultCode); + } + catch (Exception ex) + { + throw new ControlRequestException(request.OpCode, ex); + } + } + + private ControlResponse ReadResponseData(byte[] data) + { + var responseCode = data[0]; + + if (responseCode != 0x80) + throw new InvalidOperationException(); + + var requestedOpCode = (EControlOpCode)data[1]; + var resultCode = (EControlResultCode)data[2]; + byte[] parameter = [.. data.Skip(3)]; + + return new ControlResponse(requestedOpCode, resultCode, parameter); + } + + public void Dispose() + { + this.cancellationDisposable.Dispose(); + } +} \ No newline at end of file diff --git a/FTMS.NET/Control/IFitnessMachineControl.cs b/FTMS.NET/Control/IFitnessMachineControl.cs index 6f8aac7..e265a0b 100644 --- a/FTMS.NET/Control/IFitnessMachineControl.cs +++ b/FTMS.NET/Control/IFitnessMachineControl.cs @@ -1,8 +1,8 @@ -namespace FTMS.NET.Control; - -using System.Threading.Tasks; - -public interface IFitnessMachineControl : IDisposable -{ - Task Execute(ControlRequest request); -} +namespace FTMS.NET.Control; + +using System.Threading.Tasks; + +public interface IFitnessMachineControl : IDisposable +{ + Task Execute(ControlRequest request); +} \ No newline at end of file diff --git a/FTMS.NET/Data/CrossTrainerData.cs b/FTMS.NET/Data/CrossTrainerData.cs index ddede3c..64e30ee 100644 --- a/FTMS.NET/Data/CrossTrainerData.cs +++ b/FTMS.NET/Data/CrossTrainerData.cs @@ -1,32 +1,32 @@ -namespace FTMS.NET.Data; - -using DynamicData; -using System; - -public sealed class CrossTrainerData(IFitnessMachineData data) : ICrossTrainerData -{ - public IFitnessMachineValue? InstantaneousSpeed => data.GetValue(FtmsUuids.InstantaneousSpeed); - public IFitnessMachineValue? AverageSpeed => data.GetValue(FtmsUuids.AverageSpeed); - public IFitnessMachineValue? TotalDistance => data.GetValue(FtmsUuids.TotalDistance); - public IFitnessMachineValue? StepsPerMinute => data.GetValue(FtmsUuids.StepsPerMinute); - public IFitnessMachineValue? AverageStepRate => data.GetValue(FtmsUuids.AverageStepRate); - public IFitnessMachineValue? StrideCount => data.GetValue(FtmsUuids.StrideCount); - public IFitnessMachineValue? PositiveElevationGain => data.GetValue(FtmsUuids.PositiveElevationGain); - public IFitnessMachineValue? NegativeElevationGain => data.GetValue(FtmsUuids.NegativeElevationGain); - public IFitnessMachineValue? Inclination => data.GetValue(FtmsUuids.Inclination); - public IFitnessMachineValue? RampAngleSetting => data.GetValue(FtmsUuids.RampAngleSetting); - public IFitnessMachineValue? ResistantLevel => data.GetValue(FtmsUuids.ResistantLevel); - public IFitnessMachineValue? InstantaneousPower => data.GetValue(FtmsUuids.InstantaneousPower); - public IFitnessMachineValue? AveragePower => data.GetValue(FtmsUuids.AveragePower); - public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); - public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); - public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); - public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); - public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); - public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); - public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); - - public IObservable> Connect() => data.Connect(); - public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); - public void Dispose() => data.Dispose(); -} +namespace FTMS.NET.Data; + +using DynamicData; +using System; + +public sealed class CrossTrainerData(IFitnessMachineData data) : ICrossTrainerData +{ + public IFitnessMachineValue? InstantaneousSpeed => data.GetValue(FtmsUuids.InstantaneousSpeed); + public IFitnessMachineValue? AverageSpeed => data.GetValue(FtmsUuids.AverageSpeed); + public IFitnessMachineValue? TotalDistance => data.GetValue(FtmsUuids.TotalDistance); + public IFitnessMachineValue? StepsPerMinute => data.GetValue(FtmsUuids.StepsPerMinute); + public IFitnessMachineValue? AverageStepRate => data.GetValue(FtmsUuids.AverageStepRate); + public IFitnessMachineValue? StrideCount => data.GetValue(FtmsUuids.StrideCount); + public IFitnessMachineValue? PositiveElevationGain => data.GetValue(FtmsUuids.PositiveElevationGain); + public IFitnessMachineValue? NegativeElevationGain => data.GetValue(FtmsUuids.NegativeElevationGain); + public IFitnessMachineValue? Inclination => data.GetValue(FtmsUuids.Inclination); + public IFitnessMachineValue? RampAngleSetting => data.GetValue(FtmsUuids.RampAngleSetting); + public IFitnessMachineValue? ResistantLevel => data.GetValue(FtmsUuids.ResistantLevel); + public IFitnessMachineValue? InstantaneousPower => data.GetValue(FtmsUuids.InstantaneousPower); + public IFitnessMachineValue? AveragePower => data.GetValue(FtmsUuids.AveragePower); + public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); + public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); + public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); + public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); + public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); + public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); + public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); + + public IObservable> Connect() => data.Connect(); + public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); + public void Dispose() => data.Dispose(); +} \ No newline at end of file diff --git a/FTMS.NET/Data/FitnessMachineData.cs b/FTMS.NET/Data/FitnessMachineData.cs index 9ade6a1..dade2d3 100644 --- a/FTMS.NET/Data/FitnessMachineData.cs +++ b/FTMS.NET/Data/FitnessMachineData.cs @@ -1,24 +1,24 @@ -namespace FTMS.NET.Data; - -using DynamicData; -using System; -using System.Reactive.Linq; - -internal sealed class FitnessMachineData( - IObservable observeData, - FitnessMachineDataReader dataReader) - : IFitnessMachineData -{ - private readonly IObservableCache valueCache = observeData - .Select(dataReader.Read) - .ToObservableChangeSet(v => v.Uuid) - .AsObservableCache(); - - public IObservable> Connect() - => this.valueCache.Connect(); - - public IFitnessMachineValue? GetValue(Guid uuid) - => this.valueCache.KeyValues.GetValueOrDefault(uuid); - - public void Dispose() => this.valueCache.Dispose(); -} +namespace FTMS.NET.Data; + +using DynamicData; +using System; +using System.Reactive.Linq; + +internal sealed class FitnessMachineData( + IObservable observeData, + FitnessMachineDataReader dataReader) + : IFitnessMachineData +{ + private readonly IObservableCache valueCache = observeData + .Select(dataReader.Read) + .ToObservableChangeSet(v => v.Uuid) + .AsObservableCache(); + + public IObservable> Connect() + => this.valueCache.Connect(); + + public IFitnessMachineValue? GetValue(Guid uuid) + => this.valueCache.KeyValues.GetValueOrDefault(uuid); + + public void Dispose() => this.valueCache.Dispose(); +} \ No newline at end of file diff --git a/FTMS.NET/Data/FitnessMachineDataReader.cs b/FTMS.NET/Data/FitnessMachineDataReader.cs index 1f1d8c6..ce43286 100644 --- a/FTMS.NET/Data/FitnessMachineDataReader.cs +++ b/FTMS.NET/Data/FitnessMachineDataReader.cs @@ -1,15 +1,15 @@ -namespace FTMS.NET.Data; - -using System.Collections.Generic; - -internal sealed class FitnessMachineDataReader(SingleFrameStrategy singleFrameStrategy) -{ - public IEnumerable Read(byte[] dataFrame) - { - if (dataFrame.Length is 0) - return []; - - using var singleFrameReader = new SingleFrameReader(dataFrame, singleFrameStrategy); - return singleFrameReader.ReadFrame(); - } +namespace FTMS.NET.Data; + +using System.Collections.Generic; + +internal sealed class FitnessMachineDataReader(SingleFrameStrategy singleFrameStrategy) +{ + public IEnumerable Read(byte[] dataFrame) + { + if (dataFrame.Length is 0) + return []; + + using var singleFrameReader = new SingleFrameReader(dataFrame, singleFrameStrategy); + return singleFrameReader.ReadFrame(); + } } \ No newline at end of file diff --git a/FTMS.NET/Data/FitnessMachineValue.cs b/FTMS.NET/Data/FitnessMachineValue.cs index d6903f0..555b2fb 100644 --- a/FTMS.NET/Data/FitnessMachineValue.cs +++ b/FTMS.NET/Data/FitnessMachineValue.cs @@ -1,4 +1,5 @@ -namespace FTMS.NET.Data; -using System; - -public sealed record FitnessMachineValue(Guid Uuid, double Value, string Name) : IFitnessMachineValue; +namespace FTMS.NET.Data; + +using System; + +public sealed record FitnessMachineValue(Guid Uuid, double Value, string Name) : IFitnessMachineValue; \ No newline at end of file diff --git a/FTMS.NET/Data/ICrossTrainerData.cs b/FTMS.NET/Data/ICrossTrainerData.cs index 0bf0536..5d8cdda 100644 --- a/FTMS.NET/Data/ICrossTrainerData.cs +++ b/FTMS.NET/Data/ICrossTrainerData.cs @@ -1,25 +1,25 @@ -namespace FTMS.NET.Data; - -public interface ICrossTrainerData : IFitnessMachineData -{ - IFitnessMachineValue? InstantaneousSpeed { get; } - IFitnessMachineValue? AverageSpeed { get; } - IFitnessMachineValue? TotalDistance { get; } - IFitnessMachineValue? StepsPerMinute { get; } - IFitnessMachineValue? AverageStepRate { get; } - IFitnessMachineValue? StrideCount { get; } - IFitnessMachineValue? PositiveElevationGain { get; } - IFitnessMachineValue? NegativeElevationGain { get; } - IFitnessMachineValue? Inclination { get; } - IFitnessMachineValue? RampAngleSetting { get; } - IFitnessMachineValue? ResistantLevel { get; } - IFitnessMachineValue? InstantaneousPower { get; } - IFitnessMachineValue? AveragePower { get; } - IFitnessMachineValue? TotalEnergy { get; } - IFitnessMachineValue? EnergyPerHour { get; } - IFitnessMachineValue? EnergyPerMinute { get; } - IFitnessMachineValue? HeartRate { get; } - IFitnessMachineValue? MetabolicEquivalent { get; } - IFitnessMachineValue? ElapsedTime { get; } - IFitnessMachineValue? RemainingTime { get; } -} +namespace FTMS.NET.Data; + +public interface ICrossTrainerData : IFitnessMachineData +{ + IFitnessMachineValue? InstantaneousSpeed { get; } + IFitnessMachineValue? AverageSpeed { get; } + IFitnessMachineValue? TotalDistance { get; } + IFitnessMachineValue? StepsPerMinute { get; } + IFitnessMachineValue? AverageStepRate { get; } + IFitnessMachineValue? StrideCount { get; } + IFitnessMachineValue? PositiveElevationGain { get; } + IFitnessMachineValue? NegativeElevationGain { get; } + IFitnessMachineValue? Inclination { get; } + IFitnessMachineValue? RampAngleSetting { get; } + IFitnessMachineValue? ResistantLevel { get; } + IFitnessMachineValue? InstantaneousPower { get; } + IFitnessMachineValue? AveragePower { get; } + IFitnessMachineValue? TotalEnergy { get; } + IFitnessMachineValue? EnergyPerHour { get; } + IFitnessMachineValue? EnergyPerMinute { get; } + IFitnessMachineValue? HeartRate { get; } + IFitnessMachineValue? MetabolicEquivalent { get; } + IFitnessMachineValue? ElapsedTime { get; } + IFitnessMachineValue? RemainingTime { get; } +} \ No newline at end of file diff --git a/FTMS.NET/Data/IFitnessMachineData.cs b/FTMS.NET/Data/IFitnessMachineData.cs index d806191..ac9aa99 100644 --- a/FTMS.NET/Data/IFitnessMachineData.cs +++ b/FTMS.NET/Data/IFitnessMachineData.cs @@ -1,10 +1,10 @@ -namespace FTMS.NET.Data; - -using DynamicData; - -public interface IFitnessMachineData : IDisposable -{ - IObservable> Connect(); - - IFitnessMachineValue? GetValue(Guid uuid); -} +namespace FTMS.NET.Data; + +using DynamicData; + +public interface IFitnessMachineData : IDisposable +{ + IObservable> Connect(); + + IFitnessMachineValue? GetValue(Guid uuid); +} \ No newline at end of file diff --git a/FTMS.NET/Data/IFitnessMachineValue.cs b/FTMS.NET/Data/IFitnessMachineValue.cs index 87b83a2..75ae4c4 100644 --- a/FTMS.NET/Data/IFitnessMachineValue.cs +++ b/FTMS.NET/Data/IFitnessMachineValue.cs @@ -1,10 +1,10 @@ -namespace FTMS.NET.Data; - -using System; - -public interface IFitnessMachineValue -{ - Guid Uuid { get; } - double Value { get; } - string Name { get; } -} +namespace FTMS.NET.Data; + +using System; + +public interface IFitnessMachineValue +{ + Guid Uuid { get; } + double Value { get; } + string Name { get; } +} \ No newline at end of file diff --git a/FTMS.NET/Data/IIndoorBikeData.cs b/FTMS.NET/Data/IIndoorBikeData.cs index abdf15c..2b2c5a3 100644 --- a/FTMS.NET/Data/IIndoorBikeData.cs +++ b/FTMS.NET/Data/IIndoorBikeData.cs @@ -1,19 +1,20 @@ -namespace FTMS.NET.Data; -public interface IIndoorBikeData : IFitnessMachineData -{ - IFitnessMachineValue? InstantaneousSpeed { get; } - IFitnessMachineValue? AverageSpeed { get; } - IFitnessMachineValue? InstantaneousCadence { get; } - IFitnessMachineValue? AverageCadence { get; } - IFitnessMachineValue? TotalDistance { get; } - IFitnessMachineValue? ResistantLevel { get; } - IFitnessMachineValue? InstantaneousPower { get; } - IFitnessMachineValue? AveragePower { get; } - IFitnessMachineValue? TotalEnergy { get; } - IFitnessMachineValue? EnergyPerHour { get; } - IFitnessMachineValue? EnergyPerMinute { get; } - IFitnessMachineValue? HeartRate { get; } - IFitnessMachineValue? MetabolicEquivalent { get; } - IFitnessMachineValue? ElapsedTime { get; } - IFitnessMachineValue? RemainingTime { get; } -} +namespace FTMS.NET.Data; + +public interface IIndoorBikeData : IFitnessMachineData +{ + IFitnessMachineValue? InstantaneousSpeed { get; } + IFitnessMachineValue? AverageSpeed { get; } + IFitnessMachineValue? InstantaneousCadence { get; } + IFitnessMachineValue? AverageCadence { get; } + IFitnessMachineValue? TotalDistance { get; } + IFitnessMachineValue? ResistantLevel { get; } + IFitnessMachineValue? InstantaneousPower { get; } + IFitnessMachineValue? AveragePower { get; } + IFitnessMachineValue? TotalEnergy { get; } + IFitnessMachineValue? EnergyPerHour { get; } + IFitnessMachineValue? EnergyPerMinute { get; } + IFitnessMachineValue? HeartRate { get; } + IFitnessMachineValue? MetabolicEquivalent { get; } + IFitnessMachineValue? ElapsedTime { get; } + IFitnessMachineValue? RemainingTime { get; } +} \ No newline at end of file diff --git a/FTMS.NET/Data/IRowerData.cs b/FTMS.NET/Data/IRowerData.cs index c85bbb4..4f91ec2 100644 --- a/FTMS.NET/Data/IRowerData.cs +++ b/FTMS.NET/Data/IRowerData.cs @@ -1,21 +1,21 @@ -namespace FTMS.NET.Data; - -public interface IRowerData : IFitnessMachineData -{ - IFitnessMachineValue? StrokeRate { get; } - IFitnessMachineValue? StrokeCount { get; } - IFitnessMachineValue? AverageStrokeRate { get; } - IFitnessMachineValue? TotalDistance { get; } - IFitnessMachineValue? InstantaneousPace { get; } - IFitnessMachineValue? AveragePace { get; } - IFitnessMachineValue? InstantaneousPower { get; } - IFitnessMachineValue? AveragePower { get; } - IFitnessMachineValue? ResistantLevel { get; } - IFitnessMachineValue? TotalEnergy { get; } - IFitnessMachineValue? EnergyPerHour { get; } - IFitnessMachineValue? EnergyPerMinute { get; } - IFitnessMachineValue? HeartRate { get; } - IFitnessMachineValue? MetabolicEquivalent { get; } - IFitnessMachineValue? ElapsedTime { get; } - IFitnessMachineValue? RemainingTime { get; } -} +namespace FTMS.NET.Data; + +public interface IRowerData : IFitnessMachineData +{ + IFitnessMachineValue? StrokeRate { get; } + IFitnessMachineValue? StrokeCount { get; } + IFitnessMachineValue? AverageStrokeRate { get; } + IFitnessMachineValue? TotalDistance { get; } + IFitnessMachineValue? InstantaneousPace { get; } + IFitnessMachineValue? AveragePace { get; } + IFitnessMachineValue? InstantaneousPower { get; } + IFitnessMachineValue? AveragePower { get; } + IFitnessMachineValue? ResistantLevel { get; } + IFitnessMachineValue? TotalEnergy { get; } + IFitnessMachineValue? EnergyPerHour { get; } + IFitnessMachineValue? EnergyPerMinute { get; } + IFitnessMachineValue? HeartRate { get; } + IFitnessMachineValue? MetabolicEquivalent { get; } + IFitnessMachineValue? ElapsedTime { get; } + IFitnessMachineValue? RemainingTime { get; } +} \ No newline at end of file diff --git a/FTMS.NET/Data/IStairClimberData.cs b/FTMS.NET/Data/IStairClimberData.cs index db6a9a9..97a64ae 100644 --- a/FTMS.NET/Data/IStairClimberData.cs +++ b/FTMS.NET/Data/IStairClimberData.cs @@ -1,17 +1,17 @@ -namespace FTMS.NET.Data; - -public interface IStairClimberData : IFitnessMachineData -{ - IFitnessMachineValue? Floors { get; } - IFitnessMachineValue? StepsPerMinute { get; } - IFitnessMachineValue? AverageStepRate { get; } - IFitnessMachineValue? PositiveElevationGain { get; } - IFitnessMachineValue? StrideCount { get; } - IFitnessMachineValue? TotalEnergy { get; } - IFitnessMachineValue? EnergyPerHour { get; } - IFitnessMachineValue? EnergyPerMinute { get; } - IFitnessMachineValue? HeartRate { get; } - IFitnessMachineValue? MetabolicEquivalent { get; } - IFitnessMachineValue? ElapsedTime { get; } - IFitnessMachineValue? RemainingTime { get; } -} +namespace FTMS.NET.Data; + +public interface IStairClimberData : IFitnessMachineData +{ + IFitnessMachineValue? Floors { get; } + IFitnessMachineValue? StepsPerMinute { get; } + IFitnessMachineValue? AverageStepRate { get; } + IFitnessMachineValue? PositiveElevationGain { get; } + IFitnessMachineValue? StrideCount { get; } + IFitnessMachineValue? TotalEnergy { get; } + IFitnessMachineValue? EnergyPerHour { get; } + IFitnessMachineValue? EnergyPerMinute { get; } + IFitnessMachineValue? HeartRate { get; } + IFitnessMachineValue? MetabolicEquivalent { get; } + IFitnessMachineValue? ElapsedTime { get; } + IFitnessMachineValue? RemainingTime { get; } +} \ No newline at end of file diff --git a/FTMS.NET/Data/IStepClimberData.cs b/FTMS.NET/Data/IStepClimberData.cs index e44a3f4..35e8b7f 100644 --- a/FTMS.NET/Data/IStepClimberData.cs +++ b/FTMS.NET/Data/IStepClimberData.cs @@ -1,17 +1,17 @@ -namespace FTMS.NET.Data; - -public interface IStepClimberData : IFitnessMachineData -{ - IFitnessMachineValue? Floors { get; } - IFitnessMachineValue? StepCount { get; } - IFitnessMachineValue? StepsPerMinute { get; } - IFitnessMachineValue? AverageStepRate { get; } - IFitnessMachineValue? PositiveElevationGain { get; } - IFitnessMachineValue? TotalEnergy { get; } - IFitnessMachineValue? EnergyPerHour { get; } - IFitnessMachineValue? EnergyPerMinute { get; } - IFitnessMachineValue? HeartRate { get; } - IFitnessMachineValue? MetabolicEquivalent { get; } - IFitnessMachineValue? ElapsedTime { get; } - IFitnessMachineValue? RemainingTime { get; } -} +namespace FTMS.NET.Data; + +public interface IStepClimberData : IFitnessMachineData +{ + IFitnessMachineValue? Floors { get; } + IFitnessMachineValue? StepCount { get; } + IFitnessMachineValue? StepsPerMinute { get; } + IFitnessMachineValue? AverageStepRate { get; } + IFitnessMachineValue? PositiveElevationGain { get; } + IFitnessMachineValue? TotalEnergy { get; } + IFitnessMachineValue? EnergyPerHour { get; } + IFitnessMachineValue? EnergyPerMinute { get; } + IFitnessMachineValue? HeartRate { get; } + IFitnessMachineValue? MetabolicEquivalent { get; } + IFitnessMachineValue? ElapsedTime { get; } + IFitnessMachineValue? RemainingTime { get; } +} \ No newline at end of file diff --git a/FTMS.NET/Data/IThreadmillData.cs b/FTMS.NET/Data/IThreadmillData.cs index 3c961a9..d542062 100644 --- a/FTMS.NET/Data/IThreadmillData.cs +++ b/FTMS.NET/Data/IThreadmillData.cs @@ -1,23 +1,23 @@ -namespace FTMS.NET.Data; - -public interface IThreadmillData : IFitnessMachineData -{ - IFitnessMachineValue? InstantaneousSpeed { get; } - IFitnessMachineValue? AverageSpeed { get; } - IFitnessMachineValue? TotalDistance { get; } - IFitnessMachineValue? Inclination { get; } - IFitnessMachineValue? RampAngleSetting { get; } - IFitnessMachineValue? PositiveElevationGain { get; } - IFitnessMachineValue? NegativeElevationGain { get; } - IFitnessMachineValue? InstantaneousPace { get; } - IFitnessMachineValue? AveragePace { get; } - IFitnessMachineValue? TotalEnergy { get; } - IFitnessMachineValue? EnergyPerHour { get; } - IFitnessMachineValue? EnergyPerMinute { get; } - IFitnessMachineValue? HeartRate { get; } - IFitnessMachineValue? MetabolicEquivalent { get; } - IFitnessMachineValue? ElapsedTime { get; } - IFitnessMachineValue? RemainingTime { get; } - IFitnessMachineValue? ForceOnBelt { get; } - IFitnessMachineValue? PowerOutput { get; } -} +namespace FTMS.NET.Data; + +public interface IThreadmillData : IFitnessMachineData +{ + IFitnessMachineValue? InstantaneousSpeed { get; } + IFitnessMachineValue? AverageSpeed { get; } + IFitnessMachineValue? TotalDistance { get; } + IFitnessMachineValue? Inclination { get; } + IFitnessMachineValue? RampAngleSetting { get; } + IFitnessMachineValue? PositiveElevationGain { get; } + IFitnessMachineValue? NegativeElevationGain { get; } + IFitnessMachineValue? InstantaneousPace { get; } + IFitnessMachineValue? AveragePace { get; } + IFitnessMachineValue? TotalEnergy { get; } + IFitnessMachineValue? EnergyPerHour { get; } + IFitnessMachineValue? EnergyPerMinute { get; } + IFitnessMachineValue? HeartRate { get; } + IFitnessMachineValue? MetabolicEquivalent { get; } + IFitnessMachineValue? ElapsedTime { get; } + IFitnessMachineValue? RemainingTime { get; } + IFitnessMachineValue? ForceOnBelt { get; } + IFitnessMachineValue? PowerOutput { get; } +} \ No newline at end of file diff --git a/FTMS.NET/Data/IndoorBikeData.cs b/FTMS.NET/Data/IndoorBikeData.cs index bdfc344..ae86116 100644 --- a/FTMS.NET/Data/IndoorBikeData.cs +++ b/FTMS.NET/Data/IndoorBikeData.cs @@ -1,27 +1,27 @@ -namespace FTMS.NET.Data; - -using DynamicData; -using System; - -public sealed class IndoorBikeData(IFitnessMachineData data) : IIndoorBikeData -{ - public IFitnessMachineValue? InstantaneousSpeed => data.GetValue(FtmsUuids.InstantaneousSpeed); - public IFitnessMachineValue? AverageSpeed => data.GetValue(FtmsUuids.AverageSpeed); - public IFitnessMachineValue? InstantaneousCadence => data.GetValue(FtmsUuids.InstantaneousCadence); - public IFitnessMachineValue? AverageCadence => data.GetValue(FtmsUuids.AverageCadence); - public IFitnessMachineValue? TotalDistance => data.GetValue(FtmsUuids.TotalDistance); - public IFitnessMachineValue? ResistantLevel => data.GetValue(FtmsUuids.ResistantLevel); - public IFitnessMachineValue? InstantaneousPower => data.GetValue(FtmsUuids.InstantaneousPower); - public IFitnessMachineValue? AveragePower => data.GetValue(FtmsUuids.AveragePower); - public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); - public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); - public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); - public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); - public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); - public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); - public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); - - public IObservable> Connect() => data.Connect(); - public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); - public void Dispose() => data.Dispose(); -} +namespace FTMS.NET.Data; + +using DynamicData; +using System; + +public sealed class IndoorBikeData(IFitnessMachineData data) : IIndoorBikeData +{ + public IFitnessMachineValue? InstantaneousSpeed => data.GetValue(FtmsUuids.InstantaneousSpeed); + public IFitnessMachineValue? AverageSpeed => data.GetValue(FtmsUuids.AverageSpeed); + public IFitnessMachineValue? InstantaneousCadence => data.GetValue(FtmsUuids.InstantaneousCadence); + public IFitnessMachineValue? AverageCadence => data.GetValue(FtmsUuids.AverageCadence); + public IFitnessMachineValue? TotalDistance => data.GetValue(FtmsUuids.TotalDistance); + public IFitnessMachineValue? ResistantLevel => data.GetValue(FtmsUuids.ResistantLevel); + public IFitnessMachineValue? InstantaneousPower => data.GetValue(FtmsUuids.InstantaneousPower); + public IFitnessMachineValue? AveragePower => data.GetValue(FtmsUuids.AveragePower); + public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); + public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); + public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); + public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); + public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); + public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); + public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); + + public IObservable> Connect() => data.Connect(); + public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); + public void Dispose() => data.Dispose(); +} \ No newline at end of file diff --git a/FTMS.NET/Data/RowerData.cs b/FTMS.NET/Data/RowerData.cs index 6df0e3d..29bd7cf 100644 --- a/FTMS.NET/Data/RowerData.cs +++ b/FTMS.NET/Data/RowerData.cs @@ -1,28 +1,28 @@ -namespace FTMS.NET.Data; - -using DynamicData; -using System; - -public sealed class RowerData(IFitnessMachineData data) : IRowerData -{ - public IFitnessMachineValue? StrokeRate => data.GetValue(FtmsUuids.StrokeRate); - public IFitnessMachineValue? StrokeCount => data.GetValue(FtmsUuids.StrokeCount); - public IFitnessMachineValue? AverageStrokeRate => data.GetValue(FtmsUuids.AverageStrokeRate); - public IFitnessMachineValue? TotalDistance => data.GetValue(FtmsUuids.TotalDistance); - public IFitnessMachineValue? InstantaneousPace => data.GetValue(FtmsUuids.InstantaneousPace); - public IFitnessMachineValue? AveragePace => data.GetValue(FtmsUuids.AveragePace); - public IFitnessMachineValue? InstantaneousPower => data.GetValue(FtmsUuids.InstantaneousPower); - public IFitnessMachineValue? AveragePower => data.GetValue(FtmsUuids.AveragePower); - public IFitnessMachineValue? ResistantLevel => data.GetValue(FtmsUuids.ResistantLevel); - public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); - public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); - public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); - public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); - public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); - public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); - public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); - - public IObservable> Connect() => data.Connect(); - public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); - public void Dispose() => data.Dispose(); -} +namespace FTMS.NET.Data; + +using DynamicData; +using System; + +public sealed class RowerData(IFitnessMachineData data) : IRowerData +{ + public IFitnessMachineValue? StrokeRate => data.GetValue(FtmsUuids.StrokeRate); + public IFitnessMachineValue? StrokeCount => data.GetValue(FtmsUuids.StrokeCount); + public IFitnessMachineValue? AverageStrokeRate => data.GetValue(FtmsUuids.AverageStrokeRate); + public IFitnessMachineValue? TotalDistance => data.GetValue(FtmsUuids.TotalDistance); + public IFitnessMachineValue? InstantaneousPace => data.GetValue(FtmsUuids.InstantaneousPace); + public IFitnessMachineValue? AveragePace => data.GetValue(FtmsUuids.AveragePace); + public IFitnessMachineValue? InstantaneousPower => data.GetValue(FtmsUuids.InstantaneousPower); + public IFitnessMachineValue? AveragePower => data.GetValue(FtmsUuids.AveragePower); + public IFitnessMachineValue? ResistantLevel => data.GetValue(FtmsUuids.ResistantLevel); + public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); + public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); + public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); + public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); + public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); + public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); + public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); + + public IObservable> Connect() => data.Connect(); + public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); + public void Dispose() => data.Dispose(); +} \ No newline at end of file diff --git a/FTMS.NET/Data/SingleFrameReader.cs b/FTMS.NET/Data/SingleFrameReader.cs index 191aa3b..ebfd3e5 100644 --- a/FTMS.NET/Data/SingleFrameReader.cs +++ b/FTMS.NET/Data/SingleFrameReader.cs @@ -1,68 +1,68 @@ -namespace FTMS.NET.Data; - -using FTMS.NET.Utils; -using System; -using System.Data; - -internal sealed class SingleFrameReader : IDisposable -{ - private readonly MemoryStream dataStream; - private readonly BinaryReader dataReader; - private readonly SingleFrameStrategy singleFrameStrategy; - - public SingleFrameReader(byte[] dataFrame, SingleFrameStrategy singleFrameStrategy) - { - this.dataStream = new(dataFrame); - this.dataReader = new(this.dataStream); - this.singleFrameStrategy = singleFrameStrategy; - } - - public IReadOnlyList ReadFrame() - { - byte[] flagFields = this.dataReader.ReadBytes(this.singleFrameStrategy.FlagFieldLength); - - return [.. this.singleFrameStrategy - .SingleValueRules - .Where(rule => flagFields.IsBitSet(rule.BitPosition) == rule.CheckIfBitIsSet) - .Select(rule => - { - long readValue = ReadValue(rule.RawValueType); - double calculatedValue = rule.Calculation.Calculate(readValue); - return new FitnessMachineValue( - rule.ValueUuid, - calculatedValue, - FtmsUuids.GetName(rule.ValueUuid)); - })]; - - long ReadValue(RawValueType rawValueType) => rawValueType switch - { - RawValueType.Byte => this.dataReader.ReadByte(), - RawValueType.SByte => this.dataReader.ReadSByte(), - RawValueType.Short => this.dataReader.ReadInt16(), - RawValueType.UShort => this.dataReader.ReadUInt16(), - RawValueType.Int => this.dataReader.ReadInt32(), - RawValueType.UInt => this.dataReader.ReadUInt32(), - //RawValueType.Long => this.dataReader.ReadInt64(), - //RawValueType.ULong => (long)this.dataReader.ReadUInt64(), - //RawValueType.Float => (long)this.dataReader.ReadSingle(), - //RawValueType.Double => (long)this.dataReader.ReadDouble(), - RawValueType.UInt24 => this.ReadUInt24(), - _ => throw new NotSupportedException($"Unsupported RawValueType: {rawValueType}") - }; - } - - private UInt24 ReadUInt24() - { - byte b0 = this.dataReader.ReadByte(); - byte b1 = this.dataReader.ReadByte(); - byte b2 = this.dataReader.ReadByte(); - return new UInt24(b0, b1, b2); - } - - public void Dispose() - { - this.dataReader.Dispose(); - this.dataStream.Dispose(); - GC.SuppressFinalize(this); - } -} +namespace FTMS.NET.Data; + +using FTMS.NET.Utils; +using System; +using System.Data; + +internal sealed class SingleFrameReader : IDisposable +{ + private readonly MemoryStream dataStream; + private readonly BinaryReader dataReader; + private readonly SingleFrameStrategy singleFrameStrategy; + + public SingleFrameReader(byte[] dataFrame, SingleFrameStrategy singleFrameStrategy) + { + this.dataStream = new(dataFrame); + this.dataReader = new(this.dataStream); + this.singleFrameStrategy = singleFrameStrategy; + } + + public IReadOnlyList ReadFrame() + { + byte[] flagFields = this.dataReader.ReadBytes(this.singleFrameStrategy.FlagFieldLength); + + return [.. this.singleFrameStrategy + .SingleValueRules + .Where(rule => flagFields.IsBitSet(rule.BitPosition) == rule.CheckIfBitIsSet) + .Select(rule => + { + long readValue = ReadValue(rule.RawValueType); + double calculatedValue = rule.Calculation.Calculate(readValue); + return new FitnessMachineValue( + rule.ValueUuid, + calculatedValue, + FtmsUuids.GetName(rule.ValueUuid)); + })]; + + long ReadValue(RawValueType rawValueType) => rawValueType switch + { + RawValueType.Byte => this.dataReader.ReadByte(), + RawValueType.SByte => this.dataReader.ReadSByte(), + RawValueType.Short => this.dataReader.ReadInt16(), + RawValueType.UShort => this.dataReader.ReadUInt16(), + RawValueType.Int => this.dataReader.ReadInt32(), + RawValueType.UInt => this.dataReader.ReadUInt32(), + //RawValueType.Long => this.dataReader.ReadInt64(), + //RawValueType.ULong => (long)this.dataReader.ReadUInt64(), + //RawValueType.Float => (long)this.dataReader.ReadSingle(), + //RawValueType.Double => (long)this.dataReader.ReadDouble(), + RawValueType.UInt24 => this.ReadUInt24(), + _ => throw new NotSupportedException($"Unsupported RawValueType: {rawValueType}") + }; + } + + private UInt24 ReadUInt24() + { + byte b0 = this.dataReader.ReadByte(); + byte b1 = this.dataReader.ReadByte(); + byte b2 = this.dataReader.ReadByte(); + return new UInt24(b0, b1, b2); + } + + public void Dispose() + { + this.dataReader.Dispose(); + this.dataStream.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/FTMS.NET/Data/SingleFrameStrategies.cs b/FTMS.NET/Data/SingleFrameStrategies.cs index 2cf290a..2eb81b7 100644 --- a/FTMS.NET/Data/SingleFrameStrategies.cs +++ b/FTMS.NET/Data/SingleFrameStrategies.cs @@ -1,158 +1,158 @@ -namespace FTMS.NET.Data; - -using System; - -internal static class SingleFrameStrategies -{ - public static SingleFrameStrategy GetFor(EFitnessMachineType type) => type switch - { - EFitnessMachineType.Threadmill => CreateThreadmillStrategy(), - EFitnessMachineType.CrossTrainer => CreateCrossTrainerStrategy(), - EFitnessMachineType.StepClimber => CreateStepClimberStrategy(), - EFitnessMachineType.StairClimber => CreateStairClimberStrategy(), - EFitnessMachineType.Rower => CreateRowerStrategy(), - EFitnessMachineType.IndoorBike => CreateIndoorBikeStrategy(), - _ => throw new ArgumentException($"Unsupported fitness machine type: {type}") - }; - - private static SingleFrameStrategy CreateThreadmillStrategy() => new() - { - FlagFieldLength = 2, - SingleValueRules = - [ - new(FtmsUuids.InstantaneousSpeed, false, 0, RawValueType.UShort, new(1, -2, 0)), - new(FtmsUuids.AverageSpeed, true, 1, RawValueType.UShort, new(1, -2, 0)), - new(FtmsUuids.TotalDistance, true, 2, RawValueType.UInt24, new(1, 0, 0)), - new(FtmsUuids.Inclination, true, 3, RawValueType.Short, new(1, -1, 0)), - new(FtmsUuids.RampAngleSetting, true, 3, RawValueType.Short, new(1, -1, 0)), - new(FtmsUuids.PositiveElevationGain, true, 4, RawValueType.UShort, new(1, -1, 0)), - new(FtmsUuids.NegativeElevationGain, true, 4, RawValueType.UShort, new(1, -1, 0)), - new(FtmsUuids.InstantaneousPace, true, 5, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.AveragePace, true, 6, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.TotalEnergy, true, 7, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerHour, true, 7, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerMinute, true, 7, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.HeartRate, true, 8, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.MetabolicEquivalent, true, 9, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.ElapsedTime, true, 10, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.RemainingTime, true, 11, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.ForceOnBelt, true, 12, RawValueType.Short, new(1, 0, 0)), - new(FtmsUuids.PowerOutput, true, 12, RawValueType.Short, new(1, 0, 0)), - ] - }; - - private static SingleFrameStrategy CreateCrossTrainerStrategy() => new() - { - FlagFieldLength = 3, - SingleValueRules = - [ - new(FtmsUuids.InstantaneousSpeed, false, 0, RawValueType.UShort, new(1, -2, 0)), - new(FtmsUuids.AverageSpeed, true, 1, RawValueType.UShort, new(1, -2, 0)), - new(FtmsUuids.TotalDistance, true, 2, RawValueType.UInt24, new(1, 0, 0)), - new(FtmsUuids.StepsPerMinute, true, 3, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.AverageStepRate, true, 3, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.StrideCount, true, 4, RawValueType.UShort, new(1, -1, 0)), - new(FtmsUuids.PositiveElevationGain, true, 5, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.NegativeElevationGain, true, 5, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.Inclination, true, 6, RawValueType.Short, new(1, -1, 0)), - new(FtmsUuids.RampAngleSetting, true, 6, RawValueType.Short, new(1, -1, 0)), - new(FtmsUuids.ResistantLevel, true, 7, RawValueType.Byte, new(1, 1, 0)), - new(FtmsUuids.InstantaneousPower, true, 8, RawValueType.Short, new(1, 0, 0)), - new(FtmsUuids.AveragePower, true, 9, RawValueType.Short, new(1, 0, 0)), - new(FtmsUuids.TotalEnergy, true, 10, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerHour, true, 10, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerMinute, true, 10, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.HeartRate, true, 11, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.MetabolicEquivalent, true, 12, RawValueType.Byte, new(1, -1, 0)), - new(FtmsUuids.ElapsedTime, true, 13, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.RemainingTime, true, 14, RawValueType.UShort, new(1, 0, 0)), - ] - }; - - private static SingleFrameStrategy CreateStepClimberStrategy() => new() - { - FlagFieldLength = 2, - SingleValueRules = - [ - new(FtmsUuids.Floors, false, 0, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.StepCount, false, 0, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.StepsPerMinute, true, 1, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.AverageStepRate, true, 2, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.PositiveElevationGain, true, 3, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.TotalEnergy, true, 4, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerHour, true, 4, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerMinute, true, 4, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.HeartRate, true, 5, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.MetabolicEquivalent, true, 6, RawValueType.Byte, new(1, -1, 0)), - new(FtmsUuids.ElapsedTime, true, 7, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.RemainingTime, true, 8, RawValueType.UShort, new(1, 0, 0)) - ] - }; - - private static SingleFrameStrategy CreateStairClimberStrategy() => new() - { - FlagFieldLength = 2, - SingleValueRules = - [ - new(FtmsUuids.Floors, false, 0, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.StepsPerMinute, true, 1, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.AverageStepRate, true, 2, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.PositiveElevationGain, true, 3, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.StrideCount, true, 4, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.TotalEnergy, true, 5, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerHour, true, 5, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerMinute, true, 5, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.HeartRate, true, 6, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.MetabolicEquivalent, true, 7, RawValueType.Byte, new(1, -1, 0)), - new(FtmsUuids.ElapsedTime, true, 8, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.RemainingTime, true, 9, RawValueType.UShort, new(1, 0, 0)) - ] - }; - - private static SingleFrameStrategy CreateRowerStrategy() => new() - { - FlagFieldLength = 2, - SingleValueRules = - [ - new(FtmsUuids.StrokeRate, false, 0, RawValueType.Byte, new(1, 0, -1)), - new(FtmsUuids.StrokeCount, false, 0, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.AverageStrokeRate, true, 1, RawValueType.Byte, new(1, 0, -1)), - new(FtmsUuids.TotalDistance, true, 2, RawValueType.UInt24, new(1, 0, 0)), - new(FtmsUuids.InstantaneousPace, true, 3, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.AveragePace, true, 4, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.InstantaneousPower, true, 5, RawValueType.Short, new(1, 0, 0)), - new(FtmsUuids.AveragePower, true, 6, RawValueType.Short, new(1, 0, 0)), - new(FtmsUuids.ResistantLevel, true, 7, RawValueType.Byte, new(1, 1, 0)), - new(FtmsUuids.TotalEnergy, true, 8, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerHour, true, 8, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerMinute, true, 8, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.HeartRate, true, 9, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.MetabolicEquivalent, true, 10, RawValueType.Byte, new(1, -1, 0)), - new(FtmsUuids.ElapsedTime, true, 11, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.RemainingTime, true, 12, RawValueType.UShort, new(1, 0, 0)) - ] - }; - - private static SingleFrameStrategy CreateIndoorBikeStrategy() => new() - { - FlagFieldLength = 2, - SingleValueRules = - [ - new(FtmsUuids.InstantaneousSpeed, false, 0, RawValueType.UShort, new(1, -2, 0)), - new(FtmsUuids.AverageSpeed, true, 1, RawValueType.UShort, new(1, -2, 0)), - new(FtmsUuids.InstantaneousCadence, true, 2, RawValueType.UShort, new(1, 0, -1)), - new(FtmsUuids.AverageCadence, true, 3, RawValueType.UShort, new(1, 0, -1)), - new(FtmsUuids.TotalDistance, true, 4, RawValueType.UInt24, new(1, 0, 0)), - new(FtmsUuids.ResistantLevel, true, 5, RawValueType.Byte, new(1, 1, 0)), - new(FtmsUuids.InstantaneousPower, true, 6, RawValueType.Short, new(1, 0, 0)), - new(FtmsUuids.AveragePower, true, 7, RawValueType.Short, new(1, 0, 0)), - new(FtmsUuids.TotalEnergy, true, 8, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerHour, true, 8, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.EnergyPerMinute, true, 8, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.HeartRate, true, 9, RawValueType.Byte, new(1, 0, 0)), - new(FtmsUuids.MetabolicEquivalent, true, 10, RawValueType.Byte, new(1, -1, 0)), - new(FtmsUuids.ElapsedTime, true, 11, RawValueType.UShort, new(1, 0, 0)), - new(FtmsUuids.RemainingTime, true, 12, RawValueType.UShort, new(1, 0, 0)) - ] - }; -} +namespace FTMS.NET.Data; + +using System; + +internal static class SingleFrameStrategies +{ + public static SingleFrameStrategy GetFor(EFitnessMachineType type) => type switch + { + EFitnessMachineType.Threadmill => CreateThreadmillStrategy(), + EFitnessMachineType.CrossTrainer => CreateCrossTrainerStrategy(), + EFitnessMachineType.StepClimber => CreateStepClimberStrategy(), + EFitnessMachineType.StairClimber => CreateStairClimberStrategy(), + EFitnessMachineType.Rower => CreateRowerStrategy(), + EFitnessMachineType.IndoorBike => CreateIndoorBikeStrategy(), + _ => throw new ArgumentException($"Unsupported fitness machine type: {type}") + }; + + private static SingleFrameStrategy CreateThreadmillStrategy() => new() + { + FlagFieldLength = 2, + SingleValueRules = + [ + new(FtmsUuids.InstantaneousSpeed, false, 0, RawValueType.UShort, new(1, -2, 0)), + new(FtmsUuids.AverageSpeed, true, 1, RawValueType.UShort, new(1, -2, 0)), + new(FtmsUuids.TotalDistance, true, 2, RawValueType.UInt24, new(1, 0, 0)), + new(FtmsUuids.Inclination, true, 3, RawValueType.Short, new(1, -1, 0)), + new(FtmsUuids.RampAngleSetting, true, 3, RawValueType.Short, new(1, -1, 0)), + new(FtmsUuids.PositiveElevationGain, true, 4, RawValueType.UShort, new(1, -1, 0)), + new(FtmsUuids.NegativeElevationGain, true, 4, RawValueType.UShort, new(1, -1, 0)), + new(FtmsUuids.InstantaneousPace, true, 5, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.AveragePace, true, 6, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.TotalEnergy, true, 7, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerHour, true, 7, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerMinute, true, 7, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.HeartRate, true, 8, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.MetabolicEquivalent, true, 9, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.ElapsedTime, true, 10, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.RemainingTime, true, 11, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.ForceOnBelt, true, 12, RawValueType.Short, new(1, 0, 0)), + new(FtmsUuids.PowerOutput, true, 12, RawValueType.Short, new(1, 0, 0)), + ] + }; + + private static SingleFrameStrategy CreateCrossTrainerStrategy() => new() + { + FlagFieldLength = 3, + SingleValueRules = + [ + new(FtmsUuids.InstantaneousSpeed, false, 0, RawValueType.UShort, new(1, -2, 0)), + new(FtmsUuids.AverageSpeed, true, 1, RawValueType.UShort, new(1, -2, 0)), + new(FtmsUuids.TotalDistance, true, 2, RawValueType.UInt24, new(1, 0, 0)), + new(FtmsUuids.StepsPerMinute, true, 3, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.AverageStepRate, true, 3, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.StrideCount, true, 4, RawValueType.UShort, new(1, -1, 0)), + new(FtmsUuids.PositiveElevationGain, true, 5, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.NegativeElevationGain, true, 5, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.Inclination, true, 6, RawValueType.Short, new(1, -1, 0)), + new(FtmsUuids.RampAngleSetting, true, 6, RawValueType.Short, new(1, -1, 0)), + new(FtmsUuids.ResistantLevel, true, 7, RawValueType.Byte, new(1, 1, 0)), + new(FtmsUuids.InstantaneousPower, true, 8, RawValueType.Short, new(1, 0, 0)), + new(FtmsUuids.AveragePower, true, 9, RawValueType.Short, new(1, 0, 0)), + new(FtmsUuids.TotalEnergy, true, 10, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerHour, true, 10, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerMinute, true, 10, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.HeartRate, true, 11, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.MetabolicEquivalent, true, 12, RawValueType.Byte, new(1, -1, 0)), + new(FtmsUuids.ElapsedTime, true, 13, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.RemainingTime, true, 14, RawValueType.UShort, new(1, 0, 0)), + ] + }; + + private static SingleFrameStrategy CreateStepClimberStrategy() => new() + { + FlagFieldLength = 2, + SingleValueRules = + [ + new(FtmsUuids.Floors, false, 0, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.StepCount, false, 0, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.StepsPerMinute, true, 1, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.AverageStepRate, true, 2, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.PositiveElevationGain, true, 3, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.TotalEnergy, true, 4, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerHour, true, 4, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerMinute, true, 4, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.HeartRate, true, 5, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.MetabolicEquivalent, true, 6, RawValueType.Byte, new(1, -1, 0)), + new(FtmsUuids.ElapsedTime, true, 7, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.RemainingTime, true, 8, RawValueType.UShort, new(1, 0, 0)) + ] + }; + + private static SingleFrameStrategy CreateStairClimberStrategy() => new() + { + FlagFieldLength = 2, + SingleValueRules = + [ + new(FtmsUuids.Floors, false, 0, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.StepsPerMinute, true, 1, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.AverageStepRate, true, 2, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.PositiveElevationGain, true, 3, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.StrideCount, true, 4, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.TotalEnergy, true, 5, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerHour, true, 5, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerMinute, true, 5, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.HeartRate, true, 6, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.MetabolicEquivalent, true, 7, RawValueType.Byte, new(1, -1, 0)), + new(FtmsUuids.ElapsedTime, true, 8, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.RemainingTime, true, 9, RawValueType.UShort, new(1, 0, 0)) + ] + }; + + private static SingleFrameStrategy CreateRowerStrategy() => new() + { + FlagFieldLength = 2, + SingleValueRules = + [ + new(FtmsUuids.StrokeRate, false, 0, RawValueType.Byte, new(1, 0, -1)), + new(FtmsUuids.StrokeCount, false, 0, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.AverageStrokeRate, true, 1, RawValueType.Byte, new(1, 0, -1)), + new(FtmsUuids.TotalDistance, true, 2, RawValueType.UInt24, new(1, 0, 0)), + new(FtmsUuids.InstantaneousPace, true, 3, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.AveragePace, true, 4, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.InstantaneousPower, true, 5, RawValueType.Short, new(1, 0, 0)), + new(FtmsUuids.AveragePower, true, 6, RawValueType.Short, new(1, 0, 0)), + new(FtmsUuids.ResistantLevel, true, 7, RawValueType.Byte, new(1, 1, 0)), + new(FtmsUuids.TotalEnergy, true, 8, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerHour, true, 8, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerMinute, true, 8, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.HeartRate, true, 9, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.MetabolicEquivalent, true, 10, RawValueType.Byte, new(1, -1, 0)), + new(FtmsUuids.ElapsedTime, true, 11, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.RemainingTime, true, 12, RawValueType.UShort, new(1, 0, 0)) + ] + }; + + private static SingleFrameStrategy CreateIndoorBikeStrategy() => new() + { + FlagFieldLength = 2, + SingleValueRules = + [ + new(FtmsUuids.InstantaneousSpeed, false, 0, RawValueType.UShort, new(1, -2, 0)), + new(FtmsUuids.AverageSpeed, true, 1, RawValueType.UShort, new(1, -2, 0)), + new(FtmsUuids.InstantaneousCadence, true, 2, RawValueType.UShort, new(1, 0, -1)), + new(FtmsUuids.AverageCadence, true, 3, RawValueType.UShort, new(1, 0, -1)), + new(FtmsUuids.TotalDistance, true, 4, RawValueType.UInt24, new(1, 0, 0)), + new(FtmsUuids.ResistantLevel, true, 5, RawValueType.Byte, new(1, 1, 0)), + new(FtmsUuids.InstantaneousPower, true, 6, RawValueType.Short, new(1, 0, 0)), + new(FtmsUuids.AveragePower, true, 7, RawValueType.Short, new(1, 0, 0)), + new(FtmsUuids.TotalEnergy, true, 8, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerHour, true, 8, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.EnergyPerMinute, true, 8, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.HeartRate, true, 9, RawValueType.Byte, new(1, 0, 0)), + new(FtmsUuids.MetabolicEquivalent, true, 10, RawValueType.Byte, new(1, -1, 0)), + new(FtmsUuids.ElapsedTime, true, 11, RawValueType.UShort, new(1, 0, 0)), + new(FtmsUuids.RemainingTime, true, 12, RawValueType.UShort, new(1, 0, 0)) + ] + }; +} \ No newline at end of file diff --git a/FTMS.NET/Data/SingleFrameStrategy.cs b/FTMS.NET/Data/SingleFrameStrategy.cs index 8c16af3..4347e60 100644 --- a/FTMS.NET/Data/SingleFrameStrategy.cs +++ b/FTMS.NET/Data/SingleFrameStrategy.cs @@ -1,34 +1,34 @@ -namespace FTMS.NET.Data; - -using FTMS.NET.Utils; -using System; -using System.Collections.Immutable; - -internal sealed class SingleFrameStrategy -{ - public required int FlagFieldLength { get; init; } - - public required IImmutableList SingleValueRules { get; init; } -} - -internal enum RawValueType -{ - Byte, - SByte, - Short, - UShort, - Int, - UInt, - UInt24, - Long, - ULong, - Float, - Double -} - -internal sealed record SingleValueRule( - Guid ValueUuid, - bool CheckIfBitIsSet, - int BitPosition, - RawValueType RawValueType, - ValueCalculation Calculation); +namespace FTMS.NET.Data; + +using FTMS.NET.Utils; +using System; +using System.Collections.Immutable; + +internal sealed class SingleFrameStrategy +{ + public required int FlagFieldLength { get; init; } + + public required IImmutableList SingleValueRules { get; init; } +} + +internal enum RawValueType +{ + Byte, + SByte, + Short, + UShort, + Int, + UInt, + UInt24, + Long, + ULong, + Float, + Double +} + +internal sealed record SingleValueRule( + Guid ValueUuid, + bool CheckIfBitIsSet, + int BitPosition, + RawValueType RawValueType, + ValueCalculation Calculation); \ No newline at end of file diff --git a/FTMS.NET/Data/StairClimberData.cs b/FTMS.NET/Data/StairClimberData.cs index eb08f97..a1858bb 100644 --- a/FTMS.NET/Data/StairClimberData.cs +++ b/FTMS.NET/Data/StairClimberData.cs @@ -1,24 +1,24 @@ -namespace FTMS.NET.Data; - -using DynamicData; -using System; - -public sealed class StairClimberData(IFitnessMachineData data) : IStairClimberData -{ - public IFitnessMachineValue? Floors => data.GetValue(FtmsUuids.Floors); - public IFitnessMachineValue? StepsPerMinute => data.GetValue(FtmsUuids.StepsPerMinute); - public IFitnessMachineValue? AverageStepRate => data.GetValue(FtmsUuids.AverageStepRate); - public IFitnessMachineValue? PositiveElevationGain => data.GetValue(FtmsUuids.PositiveElevationGain); - public IFitnessMachineValue? StrideCount => data.GetValue(FtmsUuids.StrideCount); - public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); - public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); - public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); - public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); - public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); - public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); - public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); - - public IObservable> Connect() => data.Connect(); - public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); - public void Dispose() => data.Dispose(); -} +namespace FTMS.NET.Data; + +using DynamicData; +using System; + +public sealed class StairClimberData(IFitnessMachineData data) : IStairClimberData +{ + public IFitnessMachineValue? Floors => data.GetValue(FtmsUuids.Floors); + public IFitnessMachineValue? StepsPerMinute => data.GetValue(FtmsUuids.StepsPerMinute); + public IFitnessMachineValue? AverageStepRate => data.GetValue(FtmsUuids.AverageStepRate); + public IFitnessMachineValue? PositiveElevationGain => data.GetValue(FtmsUuids.PositiveElevationGain); + public IFitnessMachineValue? StrideCount => data.GetValue(FtmsUuids.StrideCount); + public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); + public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); + public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); + public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); + public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); + public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); + public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); + + public IObservable> Connect() => data.Connect(); + public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); + public void Dispose() => data.Dispose(); +} \ No newline at end of file diff --git a/FTMS.NET/Data/StepClimberData.cs b/FTMS.NET/Data/StepClimberData.cs index 2087e5d..df2be88 100644 --- a/FTMS.NET/Data/StepClimberData.cs +++ b/FTMS.NET/Data/StepClimberData.cs @@ -1,24 +1,24 @@ -namespace FTMS.NET.Data; - -using DynamicData; -using System; - -public sealed class StepClimberData(IFitnessMachineData data) : IStepClimberData -{ - public IFitnessMachineValue? Floors => data.GetValue(FtmsUuids.Floors); - public IFitnessMachineValue? StepCount => data.GetValue(FtmsUuids.StepCount); - public IFitnessMachineValue? StepsPerMinute => data.GetValue(FtmsUuids.StepsPerMinute); - public IFitnessMachineValue? AverageStepRate => data.GetValue(FtmsUuids.AverageStepRate); - public IFitnessMachineValue? PositiveElevationGain => data.GetValue(FtmsUuids.PositiveElevationGain); - public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); - public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); - public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); - public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); - public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); - public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); - public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); - - public IObservable> Connect() => data.Connect(); - public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); - public void Dispose() => data.Dispose(); -} +namespace FTMS.NET.Data; + +using DynamicData; +using System; + +public sealed class StepClimberData(IFitnessMachineData data) : IStepClimberData +{ + public IFitnessMachineValue? Floors => data.GetValue(FtmsUuids.Floors); + public IFitnessMachineValue? StepCount => data.GetValue(FtmsUuids.StepCount); + public IFitnessMachineValue? StepsPerMinute => data.GetValue(FtmsUuids.StepsPerMinute); + public IFitnessMachineValue? AverageStepRate => data.GetValue(FtmsUuids.AverageStepRate); + public IFitnessMachineValue? PositiveElevationGain => data.GetValue(FtmsUuids.PositiveElevationGain); + public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); + public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); + public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); + public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); + public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); + public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); + public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); + + public IObservable> Connect() => data.Connect(); + public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); + public void Dispose() => data.Dispose(); +} \ No newline at end of file diff --git a/FTMS.NET/Data/ThreadmillData.cs b/FTMS.NET/Data/ThreadmillData.cs index a07364e..ea9ba74 100644 --- a/FTMS.NET/Data/ThreadmillData.cs +++ b/FTMS.NET/Data/ThreadmillData.cs @@ -1,30 +1,30 @@ -namespace FTMS.NET.Data; - -using DynamicData; -using System; - -public sealed class ThreadmillData(IFitnessMachineData data) : IThreadmillData -{ - public IFitnessMachineValue? InstantaneousSpeed => data.GetValue(FtmsUuids.InstantaneousSpeed); - public IFitnessMachineValue? AverageSpeed => data.GetValue(FtmsUuids.AverageSpeed); - public IFitnessMachineValue? TotalDistance => data.GetValue(FtmsUuids.TotalDistance); - public IFitnessMachineValue? Inclination => data.GetValue(FtmsUuids.Inclination); - public IFitnessMachineValue? RampAngleSetting => data.GetValue(FtmsUuids.RampAngleSetting); - public IFitnessMachineValue? PositiveElevationGain => data.GetValue(FtmsUuids.PositiveElevationGain); - public IFitnessMachineValue? NegativeElevationGain => data.GetValue(FtmsUuids.NegativeElevationGain); - public IFitnessMachineValue? InstantaneousPace => data.GetValue(FtmsUuids.InstantaneousPace); - public IFitnessMachineValue? AveragePace => data.GetValue(FtmsUuids.AveragePace); - public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); - public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); - public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); - public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); - public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); - public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); - public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); - public IFitnessMachineValue? ForceOnBelt => data.GetValue(FtmsUuids.ForceOnBelt); - public IFitnessMachineValue? PowerOutput => data.GetValue(FtmsUuids.PowerOutput); - - public IObservable> Connect() => data.Connect(); - public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); - public void Dispose() => data.Dispose(); -} +namespace FTMS.NET.Data; + +using DynamicData; +using System; + +public sealed class ThreadmillData(IFitnessMachineData data) : IThreadmillData +{ + public IFitnessMachineValue? InstantaneousSpeed => data.GetValue(FtmsUuids.InstantaneousSpeed); + public IFitnessMachineValue? AverageSpeed => data.GetValue(FtmsUuids.AverageSpeed); + public IFitnessMachineValue? TotalDistance => data.GetValue(FtmsUuids.TotalDistance); + public IFitnessMachineValue? Inclination => data.GetValue(FtmsUuids.Inclination); + public IFitnessMachineValue? RampAngleSetting => data.GetValue(FtmsUuids.RampAngleSetting); + public IFitnessMachineValue? PositiveElevationGain => data.GetValue(FtmsUuids.PositiveElevationGain); + public IFitnessMachineValue? NegativeElevationGain => data.GetValue(FtmsUuids.NegativeElevationGain); + public IFitnessMachineValue? InstantaneousPace => data.GetValue(FtmsUuids.InstantaneousPace); + public IFitnessMachineValue? AveragePace => data.GetValue(FtmsUuids.AveragePace); + public IFitnessMachineValue? TotalEnergy => data.GetValue(FtmsUuids.TotalEnergy); + public IFitnessMachineValue? EnergyPerHour => data.GetValue(FtmsUuids.EnergyPerHour); + public IFitnessMachineValue? EnergyPerMinute => data.GetValue(FtmsUuids.EnergyPerMinute); + public IFitnessMachineValue? HeartRate => data.GetValue(FtmsUuids.HeartRate); + public IFitnessMachineValue? MetabolicEquivalent => data.GetValue(FtmsUuids.MetabolicEquivalent); + public IFitnessMachineValue? ElapsedTime => data.GetValue(FtmsUuids.ElapsedTime); + public IFitnessMachineValue? RemainingTime => data.GetValue(FtmsUuids.RemainingTime); + public IFitnessMachineValue? ForceOnBelt => data.GetValue(FtmsUuids.ForceOnBelt); + public IFitnessMachineValue? PowerOutput => data.GetValue(FtmsUuids.PowerOutput); + + public IObservable> Connect() => data.Connect(); + public IFitnessMachineValue? GetValue(Guid uuid) => data.GetValue(uuid); + public void Dispose() => data.Dispose(); +} \ No newline at end of file diff --git a/FTMS.NET/EFitnessMachineType.cs b/FTMS.NET/EFitnessMachineType.cs index 1970770..8b4cc5d 100644 --- a/FTMS.NET/EFitnessMachineType.cs +++ b/FTMS.NET/EFitnessMachineType.cs @@ -1,36 +1,36 @@ -namespace FTMS.NET; - -using FTMS.NET.Exceptions; - -public enum EFitnessMachineType -{ - Threadmill, - CrossTrainer, - StepClimber, - StairClimber, - Rower, - IndoorBike -} - -public static class EFitnessMachineTypeExtensions -{ - public static EFitnessMachineType EnsureType(this EFitnessMachineType fitnessMachineType) - { - if (Enum.IsDefined(fitnessMachineType) == false) - throw new FitnessMachineTypeNotDefinedException(fitnessMachineType); - - return fitnessMachineType; - } - - public static Guid GetDataCharacteristicId(this EFitnessMachineType fitnessMachineType) - => fitnessMachineType switch - { - EFitnessMachineType.Threadmill => FtmsUuids.TreadmillData, - EFitnessMachineType.CrossTrainer => FtmsUuids.CrossTrainerData, - EFitnessMachineType.StepClimber => FtmsUuids.StepClimberData, - EFitnessMachineType.StairClimber => FtmsUuids.StairClimberData, - EFitnessMachineType.Rower => FtmsUuids.RowerData, - EFitnessMachineType.IndoorBike => FtmsUuids.IndoorBikeData, - _ => throw new InvalidOperationException() - }; -} +namespace FTMS.NET; + +using FTMS.NET.Exceptions; + +public enum EFitnessMachineType +{ + Threadmill, + CrossTrainer, + StepClimber, + StairClimber, + Rower, + IndoorBike +} + +public static class EFitnessMachineTypeExtensions +{ + public static EFitnessMachineType EnsureType(this EFitnessMachineType fitnessMachineType) + { + if (Enum.IsDefined(fitnessMachineType) == false) + throw new FitnessMachineTypeNotDefinedException(fitnessMachineType); + + return fitnessMachineType; + } + + public static Guid GetDataCharacteristicId(this EFitnessMachineType fitnessMachineType) + => fitnessMachineType switch + { + EFitnessMachineType.Threadmill => FtmsUuids.TreadmillData, + EFitnessMachineType.CrossTrainer => FtmsUuids.CrossTrainerData, + EFitnessMachineType.StepClimber => FtmsUuids.StepClimberData, + EFitnessMachineType.StairClimber => FtmsUuids.StairClimberData, + EFitnessMachineType.Rower => FtmsUuids.RowerData, + EFitnessMachineType.IndoorBike => FtmsUuids.IndoorBikeData, + _ => throw new InvalidOperationException() + }; +} \ No newline at end of file diff --git a/FTMS.NET/Exceptions/ControlRequestException.cs b/FTMS.NET/Exceptions/ControlRequestException.cs index 7a529c0..ce9d2eb 100644 --- a/FTMS.NET/Exceptions/ControlRequestException.cs +++ b/FTMS.NET/Exceptions/ControlRequestException.cs @@ -1,19 +1,19 @@ -namespace FTMS.NET.Exceptions; - -using FTMS.NET.Control; -using System; - -public sealed class ControlRequestException( - EControlOpCode opCode, - Exception? innerException = null) - : Exception("Control Request could not be executed.", innerException) -{ - public EControlOpCode OpCode { get; } = opCode; - public EControlResultCode? ResultCode { get; } - - public ControlRequestException(EControlOpCode opCode, EControlResultCode resultCode) - : this(opCode) - { - this.ResultCode = resultCode; - } -} +namespace FTMS.NET.Exceptions; + +using FTMS.NET.Control; +using System; + +public sealed class ControlRequestException( + EControlOpCode opCode, + Exception? innerException = null) + : Exception("Control Request could not be executed.", innerException) +{ + public EControlOpCode OpCode { get; } = opCode; + public EControlResultCode? ResultCode { get; } + + public ControlRequestException(EControlOpCode opCode, EControlResultCode resultCode) + : this(opCode) + { + this.ResultCode = resultCode; + } +} \ No newline at end of file diff --git a/FTMS.NET/Exceptions/FitnessMachineNotAvailableException.cs b/FTMS.NET/Exceptions/FitnessMachineNotAvailableException.cs index 94a7f8d..c3b943c 100644 --- a/FTMS.NET/Exceptions/FitnessMachineNotAvailableException.cs +++ b/FTMS.NET/Exceptions/FitnessMachineNotAvailableException.cs @@ -1,5 +1,6 @@ -namespace FTMS.NET.Exceptions; -using System; - -public sealed class FitnessMachineNotAvailableException() +namespace FTMS.NET.Exceptions; + +using System; + +public sealed class FitnessMachineNotAvailableException() : Exception("The service data indicate, that the fitness machine service is not available."); \ No newline at end of file diff --git a/FTMS.NET/Exceptions/FitnessMachineTypeNotDefinedException.cs b/FTMS.NET/Exceptions/FitnessMachineTypeNotDefinedException.cs index a7c03c2..a84eed4 100644 --- a/FTMS.NET/Exceptions/FitnessMachineTypeNotDefinedException.cs +++ b/FTMS.NET/Exceptions/FitnessMachineTypeNotDefinedException.cs @@ -1,9 +1,9 @@ -namespace FTMS.NET.Exceptions; - -using System; - -public sealed class FitnessMachineTypeNotDefinedException(EFitnessMachineType machineType) - : Exception("The fitness machine type sent from the server is not defined.") -{ - public EFitnessMachineType MachineType { get; } = machineType; -} +namespace FTMS.NET.Exceptions; + +using System; + +public sealed class FitnessMachineTypeNotDefinedException(EFitnessMachineType machineType) + : Exception("The fitness machine type sent from the server is not defined.") +{ + public EFitnessMachineType MachineType { get; } = machineType; +} \ No newline at end of file diff --git a/FTMS.NET/Exceptions/NeededCharacteristicNotAvailableException.cs b/FTMS.NET/Exceptions/NeededCharacteristicNotAvailableException.cs index 91b107e..a6011ce 100644 --- a/FTMS.NET/Exceptions/NeededCharacteristicNotAvailableException.cs +++ b/FTMS.NET/Exceptions/NeededCharacteristicNotAvailableException.cs @@ -1,11 +1,11 @@ -namespace FTMS.NET.Exceptions; - -using System; - -public sealed class NeededCharacteristicNotAvailableException( - Guid characteristicUuid) - : Exception("The needed characteristic is not available.") -{ - public string CharacteristicName { get; } = FtmsUuids.GetName(characteristicUuid); - public Guid CharacteristicUuid { get; } = characteristicUuid; -} +namespace FTMS.NET.Exceptions; + +using System; + +public sealed class NeededCharacteristicNotAvailableException( + Guid characteristicUuid) + : Exception("The needed characteristic is not available.") +{ + public string CharacteristicName { get; } = FtmsUuids.GetName(characteristicUuid); + public Guid CharacteristicUuid { get; } = characteristicUuid; +} \ No newline at end of file diff --git a/FTMS.NET/Features/FitnessMachineFeatures.cs b/FTMS.NET/Features/FitnessMachineFeatures.cs index 5c292a3..ddbd56f 100644 --- a/FTMS.NET/Features/FitnessMachineFeatures.cs +++ b/FTMS.NET/Features/FitnessMachineFeatures.cs @@ -1,56 +1,56 @@ -namespace FTMS.NET.Features; - -using FTMS.NET.Utils; -using System; - -internal sealed class FitnessMachineFeatures( - ReadOnlySpan featuresField, - ReadOnlySpan targetSettingsField) - : IFitnessMachineFeatures -{ - public bool AverageSpeedSupported { get; } = featuresField.IsBitSet(0); - public bool CadenceSupported { get; } = featuresField.IsBitSet(1); - public bool TotalDistanceSupported { get; } = featuresField.IsBitSet(2); - public bool InclinationSupported { get; } = featuresField.IsBitSet(3); - public bool ElevationGainSupported { get; } = featuresField.IsBitSet(4); - public bool PaceSupported { get; } = featuresField.IsBitSet(5); - public bool StepCountSupported { get; } = featuresField.IsBitSet(6); - public bool ResistanceLevelSupported { get; } = featuresField.IsBitSet(7); - public bool StrideCountSupported { get; } = featuresField.IsBitSet(8); - public bool ExpendedEnergySupported { get; } = featuresField.IsBitSet(9); - public bool HeartRateMeasurementSupported { get; } = featuresField.IsBitSet(10); - public bool MetabolicEquivalentSupported { get; } = featuresField.IsBitSet(11); - public bool ElapsedTimeSupported { get; } = featuresField.IsBitSet(12); - public bool RemainingTimeSupported { get; } = featuresField.IsBitSet(13); - public bool PowerMeasurementSupported { get; } = featuresField.IsBitSet(14); - public bool ForceOnBeltAndPowerOutputSupported { get; } = featuresField.IsBitSet(15); - public bool UserDataRetentionSupported { get; } = featuresField.IsBitSet(16); - - public bool SpeedTargetSettingSupported { get; } = targetSettingsField.IsBitSet(0); - public bool InclinationTargetSettingSupported { get; } = targetSettingsField.IsBitSet(1); - public bool ResistanceTargetSettingSupported { get; } = targetSettingsField.IsBitSet(2); - public bool PowerTargetSettingSupported { get; } = targetSettingsField.IsBitSet(3); - public bool HeartRateTargetSettingSupported { get; } = targetSettingsField.IsBitSet(4); - public bool TargetedExpendedEnergyConfigurationSupported { get; } = targetSettingsField.IsBitSet(5); - public bool TargetedStepNumberConfigurationSupported { get; } = targetSettingsField.IsBitSet(6); - public bool TargetedStrideNumberConfigurationSupported { get; } = targetSettingsField.IsBitSet(7); - public bool TargetedDistanceConfigurationSupported { get; } = targetSettingsField.IsBitSet(8); - public bool TargetedTrainingTimeConfigurationSupported { get; } = targetSettingsField.IsBitSet(9); - public bool TargetedTimeInTwoHeartRateZonesConfigurationSupported { get; } = targetSettingsField.IsBitSet(10); - public bool TargetedTimeInThreeHeartRateZonesConfigurationSupported { get; } = targetSettingsField.IsBitSet(11); - public bool TargetedTimeInFiveHeartRateZonesConfigurationSupported { get; } = targetSettingsField.IsBitSet(12); - public bool IndoorBikeSimulationParametersSupported { get; } = targetSettingsField.IsBitSet(13); - public bool WheelCircumferenceConfigurationSupported { get; } = targetSettingsField.IsBitSet(14); - public bool SpinDownControlSupported { get; } = targetSettingsField.IsBitSet(15); - public bool TargetedCadenceConfigurationSupported { get; } = targetSettingsField.IsBitSet(16); - - public ISupportedRange? SpeedRange { get; init; } - - public ISupportedRange? InclinationRange { get; init; } - - public ISupportedRange? ResistanceLevelRange { get; init; } - - public ISupportedRange? PowerRange { get; init; } - - public ISupportedRange? HeartRateRange { get; init; } -} +namespace FTMS.NET.Features; + +using FTMS.NET.Utils; +using System; + +internal sealed class FitnessMachineFeatures( + ReadOnlySpan featuresField, + ReadOnlySpan targetSettingsField) + : IFitnessMachineFeatures +{ + public bool AverageSpeedSupported { get; } = featuresField.IsBitSet(0); + public bool CadenceSupported { get; } = featuresField.IsBitSet(1); + public bool TotalDistanceSupported { get; } = featuresField.IsBitSet(2); + public bool InclinationSupported { get; } = featuresField.IsBitSet(3); + public bool ElevationGainSupported { get; } = featuresField.IsBitSet(4); + public bool PaceSupported { get; } = featuresField.IsBitSet(5); + public bool StepCountSupported { get; } = featuresField.IsBitSet(6); + public bool ResistanceLevelSupported { get; } = featuresField.IsBitSet(7); + public bool StrideCountSupported { get; } = featuresField.IsBitSet(8); + public bool ExpendedEnergySupported { get; } = featuresField.IsBitSet(9); + public bool HeartRateMeasurementSupported { get; } = featuresField.IsBitSet(10); + public bool MetabolicEquivalentSupported { get; } = featuresField.IsBitSet(11); + public bool ElapsedTimeSupported { get; } = featuresField.IsBitSet(12); + public bool RemainingTimeSupported { get; } = featuresField.IsBitSet(13); + public bool PowerMeasurementSupported { get; } = featuresField.IsBitSet(14); + public bool ForceOnBeltAndPowerOutputSupported { get; } = featuresField.IsBitSet(15); + public bool UserDataRetentionSupported { get; } = featuresField.IsBitSet(16); + + public bool SpeedTargetSettingSupported { get; } = targetSettingsField.IsBitSet(0); + public bool InclinationTargetSettingSupported { get; } = targetSettingsField.IsBitSet(1); + public bool ResistanceTargetSettingSupported { get; } = targetSettingsField.IsBitSet(2); + public bool PowerTargetSettingSupported { get; } = targetSettingsField.IsBitSet(3); + public bool HeartRateTargetSettingSupported { get; } = targetSettingsField.IsBitSet(4); + public bool TargetedExpendedEnergyConfigurationSupported { get; } = targetSettingsField.IsBitSet(5); + public bool TargetedStepNumberConfigurationSupported { get; } = targetSettingsField.IsBitSet(6); + public bool TargetedStrideNumberConfigurationSupported { get; } = targetSettingsField.IsBitSet(7); + public bool TargetedDistanceConfigurationSupported { get; } = targetSettingsField.IsBitSet(8); + public bool TargetedTrainingTimeConfigurationSupported { get; } = targetSettingsField.IsBitSet(9); + public bool TargetedTimeInTwoHeartRateZonesConfigurationSupported { get; } = targetSettingsField.IsBitSet(10); + public bool TargetedTimeInThreeHeartRateZonesConfigurationSupported { get; } = targetSettingsField.IsBitSet(11); + public bool TargetedTimeInFiveHeartRateZonesConfigurationSupported { get; } = targetSettingsField.IsBitSet(12); + public bool IndoorBikeSimulationParametersSupported { get; } = targetSettingsField.IsBitSet(13); + public bool WheelCircumferenceConfigurationSupported { get; } = targetSettingsField.IsBitSet(14); + public bool SpinDownControlSupported { get; } = targetSettingsField.IsBitSet(15); + public bool TargetedCadenceConfigurationSupported { get; } = targetSettingsField.IsBitSet(16); + + public ISupportedRange? SpeedRange { get; init; } + + public ISupportedRange? InclinationRange { get; init; } + + public ISupportedRange? ResistanceLevelRange { get; init; } + + public ISupportedRange? PowerRange { get; init; } + + public ISupportedRange? HeartRateRange { get; init; } +} \ No newline at end of file diff --git a/FTMS.NET/Features/IFitnessMachineFeatures.cs b/FTMS.NET/Features/IFitnessMachineFeatures.cs index e47da3d..65889c1 100644 --- a/FTMS.NET/Features/IFitnessMachineFeatures.cs +++ b/FTMS.NET/Features/IFitnessMachineFeatures.cs @@ -1,45 +1,46 @@ -namespace FTMS.NET.Features; -public interface IFitnessMachineFeatures -{ - bool AverageSpeedSupported { get; } - bool CadenceSupported { get; } - bool TotalDistanceSupported { get; } - bool InclinationSupported { get; } - bool ElevationGainSupported { get; } - bool PaceSupported { get; } - bool StepCountSupported { get; } - bool ResistanceLevelSupported { get; } - bool StrideCountSupported { get; } - bool ExpendedEnergySupported { get; } - bool HeartRateMeasurementSupported { get; } - bool MetabolicEquivalentSupported { get; } - bool ElapsedTimeSupported { get; } - bool RemainingTimeSupported { get; } - bool PowerMeasurementSupported { get; } - bool ForceOnBeltAndPowerOutputSupported { get; } - bool UserDataRetentionSupported { get; } - - bool SpeedTargetSettingSupported { get; } - bool InclinationTargetSettingSupported { get; } - bool ResistanceTargetSettingSupported { get; } - bool PowerTargetSettingSupported { get; } - bool HeartRateTargetSettingSupported { get; } - bool TargetedExpendedEnergyConfigurationSupported { get; } - bool TargetedStepNumberConfigurationSupported { get; } - bool TargetedStrideNumberConfigurationSupported { get; } - bool TargetedDistanceConfigurationSupported { get; } - bool TargetedTrainingTimeConfigurationSupported { get; } - bool TargetedTimeInTwoHeartRateZonesConfigurationSupported { get; } - bool TargetedTimeInThreeHeartRateZonesConfigurationSupported { get; } - bool TargetedTimeInFiveHeartRateZonesConfigurationSupported { get; } - bool IndoorBikeSimulationParametersSupported { get; } - bool WheelCircumferenceConfigurationSupported { get; } - bool SpinDownControlSupported { get; } - bool TargetedCadenceConfigurationSupported { get; } - - ISupportedRange? SpeedRange { get; } - ISupportedRange? InclinationRange { get; } - ISupportedRange? ResistanceLevelRange { get; } - ISupportedRange? PowerRange { get; } - ISupportedRange? HeartRateRange { get; } -} +namespace FTMS.NET.Features; + +public interface IFitnessMachineFeatures +{ + bool AverageSpeedSupported { get; } + bool CadenceSupported { get; } + bool TotalDistanceSupported { get; } + bool InclinationSupported { get; } + bool ElevationGainSupported { get; } + bool PaceSupported { get; } + bool StepCountSupported { get; } + bool ResistanceLevelSupported { get; } + bool StrideCountSupported { get; } + bool ExpendedEnergySupported { get; } + bool HeartRateMeasurementSupported { get; } + bool MetabolicEquivalentSupported { get; } + bool ElapsedTimeSupported { get; } + bool RemainingTimeSupported { get; } + bool PowerMeasurementSupported { get; } + bool ForceOnBeltAndPowerOutputSupported { get; } + bool UserDataRetentionSupported { get; } + + bool SpeedTargetSettingSupported { get; } + bool InclinationTargetSettingSupported { get; } + bool ResistanceTargetSettingSupported { get; } + bool PowerTargetSettingSupported { get; } + bool HeartRateTargetSettingSupported { get; } + bool TargetedExpendedEnergyConfigurationSupported { get; } + bool TargetedStepNumberConfigurationSupported { get; } + bool TargetedStrideNumberConfigurationSupported { get; } + bool TargetedDistanceConfigurationSupported { get; } + bool TargetedTrainingTimeConfigurationSupported { get; } + bool TargetedTimeInTwoHeartRateZonesConfigurationSupported { get; } + bool TargetedTimeInThreeHeartRateZonesConfigurationSupported { get; } + bool TargetedTimeInFiveHeartRateZonesConfigurationSupported { get; } + bool IndoorBikeSimulationParametersSupported { get; } + bool WheelCircumferenceConfigurationSupported { get; } + bool SpinDownControlSupported { get; } + bool TargetedCadenceConfigurationSupported { get; } + + ISupportedRange? SpeedRange { get; } + ISupportedRange? InclinationRange { get; } + ISupportedRange? ResistanceLevelRange { get; } + ISupportedRange? PowerRange { get; } + ISupportedRange? HeartRateRange { get; } +} \ No newline at end of file diff --git a/FTMS.NET/Features/ISupportedRange.cs b/FTMS.NET/Features/ISupportedRange.cs index 149604f..2fad88d 100644 --- a/FTMS.NET/Features/ISupportedRange.cs +++ b/FTMS.NET/Features/ISupportedRange.cs @@ -1,8 +1,9 @@ -namespace FTMS.NET.Features; -public interface ISupportedRange -{ - double MinimumValue { get; } - double MaximumValue { get; } - double MinimumIncrement { get; } - FitnessMachineUnit Unit { get; } -} +namespace FTMS.NET.Features; + +public interface ISupportedRange +{ + double MinimumValue { get; } + double MaximumValue { get; } + double MinimumIncrement { get; } + FitnessMachineUnit Unit { get; } +} \ No newline at end of file diff --git a/FTMS.NET/Features/ISupportedRangeExtensions.cs b/FTMS.NET/Features/ISupportedRangeExtensions.cs index 504aab2..f9ae5c1 100644 --- a/FTMS.NET/Features/ISupportedRangeExtensions.cs +++ b/FTMS.NET/Features/ISupportedRangeExtensions.cs @@ -1,15 +1,15 @@ -namespace FTMS.NET.Features; - -using FTMS.NET.Utils; -using System.Numerics; - -public static class ISupportedRangeExtensions -{ - public static bool IsInRange(this ISupportedRange range, TValue value) - where TValue : struct, INumber - => GenericMath.IsInRange(value, range.MinimumValue, range.MaximumValue); - - public static TValue Clamp(this ISupportedRange range, TValue value) - where TValue : struct, INumber - => GenericMath.Clamp(value, range.MinimumValue, range.MaximumValue); -} +namespace FTMS.NET.Features; + +using FTMS.NET.Utils; +using System.Numerics; + +public static class ISupportedRangeExtensions +{ + public static bool IsInRange(this ISupportedRange range, TValue value) + where TValue : struct, INumber + => GenericMath.IsInRange(value, range.MinimumValue, range.MaximumValue); + + public static TValue Clamp(this ISupportedRange range, TValue value) + where TValue : struct, INumber + => GenericMath.Clamp(value, range.MinimumValue, range.MaximumValue); +} \ No newline at end of file diff --git a/FTMS.NET/Features/RangeCalculations.cs b/FTMS.NET/Features/RangeCalculations.cs index 393f78e..25162b8 100644 --- a/FTMS.NET/Features/RangeCalculations.cs +++ b/FTMS.NET/Features/RangeCalculations.cs @@ -1,12 +1,12 @@ -namespace FTMS.NET.Features; - -using FTMS.NET.Utils; - -internal static class RangeCalculations -{ - public static ValueCalculation Speed => new(1, -2, 0); - public static ValueCalculation Inclination => new(1, -1, 0); - public static ValueCalculation ResistanceLevel => new(1, 1, 0); - public static ValueCalculation Power => new(1, 0, 0); - public static ValueCalculation HeartRange => new(1, 0, 0); -} +namespace FTMS.NET.Features; + +using FTMS.NET.Utils; + +internal static class RangeCalculations +{ + public static ValueCalculation Speed => new(1, -2, 0); + public static ValueCalculation Inclination => new(1, -1, 0); + public static ValueCalculation ResistanceLevel => new(1, 1, 0); + public static ValueCalculation Power => new(1, 0, 0); + public static ValueCalculation HeartRange => new(1, 0, 0); +} \ No newline at end of file diff --git a/FTMS.NET/Features/SupportedRange.cs b/FTMS.NET/Features/SupportedRange.cs index 02ac9bf..a117656 100644 --- a/FTMS.NET/Features/SupportedRange.cs +++ b/FTMS.NET/Features/SupportedRange.cs @@ -1,46 +1,46 @@ -namespace FTMS.NET.Features; - -using FTMS.NET; -using FTMS.NET.Utils; - -internal sealed record SupportedRange( - double MinimumValue, - double MaximumValue, - double MinimumIncrement, - FitnessMachineUnit Unit) - : ISupportedRange -{ - public static ISupportedRange ReadSpeed(byte[] data) => - Read(data, RangeCalculations.Speed, FitnessMachineUnit.KilometersPerHour, r => r.ReadUInt16()); - - public static ISupportedRange ReadInclination(byte[] data) => - Read(data, RangeCalculations.Inclination, FitnessMachineUnit.Percent, r => r.ReadInt16(), r => r.ReadInt16(), r => r.ReadUInt16()); - - public static ISupportedRange ReadResistanceLevel(byte[] data) => - Read(data, RangeCalculations.ResistanceLevel, FitnessMachineUnit.None, r => r.ReadByte()); - - public static ISupportedRange ReadPower(byte[] data) => - Read(data, RangeCalculations.Power, FitnessMachineUnit.Watt, r => r.ReadInt16(), r => r.ReadInt16(), r => r.ReadUInt16()); - - public static ISupportedRange ReadHeartRate(byte[] data) => - Read(data, RangeCalculations.HeartRange, FitnessMachineUnit.BeatsPerMinute, r => r.ReadByte()); - - private static SupportedRange Read( - byte[] data, - ValueCalculation calculation, - FitnessMachineUnit unit, - Func readMinimum, - Func? readMaximum = null, - Func? readIncrement = null) - { - readMaximum ??= readMinimum; - readIncrement ??= readMinimum; - - using MemoryStream stream = new(data); - using BinaryReader reader = new(stream); - var minimumValue = calculation.Calculate(readMinimum(reader)); - var maximumValue = calculation.Calculate(readMaximum(reader)); - var minimumIncrement = calculation.Calculate(readIncrement(reader)); - return new(minimumValue, maximumValue, minimumIncrement, unit); - } -} +namespace FTMS.NET.Features; + +using FTMS.NET; +using FTMS.NET.Utils; + +internal sealed record SupportedRange( + double MinimumValue, + double MaximumValue, + double MinimumIncrement, + FitnessMachineUnit Unit) + : ISupportedRange +{ + public static ISupportedRange ReadSpeed(byte[] data) => + Read(data, RangeCalculations.Speed, FitnessMachineUnit.KilometersPerHour, r => r.ReadUInt16()); + + public static ISupportedRange ReadInclination(byte[] data) => + Read(data, RangeCalculations.Inclination, FitnessMachineUnit.Percent, r => r.ReadInt16(), r => r.ReadInt16(), r => r.ReadUInt16()); + + public static ISupportedRange ReadResistanceLevel(byte[] data) => + Read(data, RangeCalculations.ResistanceLevel, FitnessMachineUnit.None, r => r.ReadByte()); + + public static ISupportedRange ReadPower(byte[] data) => + Read(data, RangeCalculations.Power, FitnessMachineUnit.Watt, r => r.ReadInt16(), r => r.ReadInt16(), r => r.ReadUInt16()); + + public static ISupportedRange ReadHeartRate(byte[] data) => + Read(data, RangeCalculations.HeartRange, FitnessMachineUnit.BeatsPerMinute, r => r.ReadByte()); + + private static SupportedRange Read( + byte[] data, + ValueCalculation calculation, + FitnessMachineUnit unit, + Func readMinimum, + Func? readMaximum = null, + Func? readIncrement = null) + { + readMaximum ??= readMinimum; + readIncrement ??= readMinimum; + + using MemoryStream stream = new(data); + using BinaryReader reader = new(stream); + var minimumValue = calculation.Calculate(readMinimum(reader)); + var maximumValue = calculation.Calculate(readMaximum(reader)); + var minimumIncrement = calculation.Calculate(readIncrement(reader)); + return new(minimumValue, maximumValue, minimumIncrement, unit); + } +} \ No newline at end of file diff --git a/FTMS.NET/FitnessMachineService.cs b/FTMS.NET/FitnessMachineService.cs index 34c0078..d1e1bca 100644 --- a/FTMS.NET/FitnessMachineService.cs +++ b/FTMS.NET/FitnessMachineService.cs @@ -1,45 +1,45 @@ -namespace FTMS.NET; - -using DynamicData; -using FTMS.NET.Control; -using FTMS.NET.Data; -using FTMS.NET.Features; -using FTMS.NET.State; -using System.Threading.Tasks; - -internal sealed class FitnessMachineService( - EFitnessMachineType type, - IFitnessMachineData data, - IFitnessMachineControl control, - IFitnessMachineStateProvider state, - IFitnessMachineFeatures features) - : IFitnessMachineService -{ - public EFitnessMachineType Type { get; } = type; - public IFitnessMachineData Data { get; } = data; - public IFitnessMachineControl Control { get; } = control; - public IFitnessMachineStateProvider State { get; } = state; - public IFitnessMachineFeatures Features { get; } = features; - - public Task Execute(ControlRequest request) - => this.Control.Execute(request); - - public IObservable ObserveMachineState() - => this.State.ObserveMachineState(); - - public IObservable ObserveTrainingState() - => this.State.ObserveTrainingState(); - - public IObservable> Connect() - => this.Data.Connect(); - - public IFitnessMachineValue? GetValue(Guid uuid) - => this.Data.GetValue(uuid); - - public void Dispose() - { - this.Data.Dispose(); - this.Control.Dispose(); - this.State.Dispose(); - } -} +namespace FTMS.NET; + +using DynamicData; +using FTMS.NET.Control; +using FTMS.NET.Data; +using FTMS.NET.Features; +using FTMS.NET.State; +using System.Threading.Tasks; + +internal sealed class FitnessMachineService( + EFitnessMachineType type, + IFitnessMachineData data, + IFitnessMachineControl control, + IFitnessMachineStateProvider state, + IFitnessMachineFeatures features) + : IFitnessMachineService +{ + public EFitnessMachineType Type { get; } = type; + public IFitnessMachineData Data { get; } = data; + public IFitnessMachineControl Control { get; } = control; + public IFitnessMachineStateProvider State { get; } = state; + public IFitnessMachineFeatures Features { get; } = features; + + public Task Execute(ControlRequest request) + => this.Control.Execute(request); + + public IObservable ObserveMachineState() + => this.State.ObserveMachineState(); + + public IObservable ObserveTrainingState() + => this.State.ObserveTrainingState(); + + public IObservable> Connect() + => this.Data.Connect(); + + public IFitnessMachineValue? GetValue(Guid uuid) + => this.Data.GetValue(uuid); + + public void Dispose() + { + this.Data.Dispose(); + this.Control.Dispose(); + this.State.Dispose(); + } +} \ No newline at end of file diff --git a/FTMS.NET/FitnessMachineServiceFactory.cs b/FTMS.NET/FitnessMachineServiceFactory.cs index f9eb188..2a5a1f5 100644 --- a/FTMS.NET/FitnessMachineServiceFactory.cs +++ b/FTMS.NET/FitnessMachineServiceFactory.cs @@ -1,123 +1,130 @@ -namespace FTMS.NET; - -using FTMS.NET.Control; -using FTMS.NET.Data; -using FTMS.NET.Exceptions; -using FTMS.NET.Features; -using FTMS.NET.State; -using FTMS.NET.Utils; -using System; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; - -public static class FitnessMachineServiceFactory -{ - public static async Task CreateFitnessMachineServiceAsync( - this IFitnessMachineServiceConnection connection) - { - connection.EnsureAvailability(); - var fitnessMachineType = connection.ReadType(); - - IFitnessMachineFeatures features = await connection.ReadFitnessMachineFeaturesAsync(); - IFitnessMachineData data = await connection.CreateFitnessMachineDataAsync(); - IFitnessMachineControl control = await connection.CreateFitnessMachineControlAsync(); - IFitnessMachineStateProvider stateProvider = await connection.CreateFitnessMachineStateProviderAsync(); - - return new FitnessMachineService(fitnessMachineType, data, control, stateProvider, features); - } - - public static void EnsureAvailability(this IFitnessMachineServiceConnection connection) - { - var available = connection.ServiceData[2].IsBitSet(0); - if (available is false) - throw new FitnessMachineNotAvailableException(); - } - - public static async Task CreateFitnessMachineDataAsync( - this IFitnessMachineServiceConnection connection) - { - var fitnessMachineType = connection.ReadType(); - var fitnessMachineDataReader = fitnessMachineType.GetDataReader(); - - var dataCharacteristicId = fitnessMachineType.GetDataCharacteristicId(); - var dataCharacteristic = await connection.GetCharacteristicAsync(dataCharacteristicId); - - dataCharacteristic.EnsureAvailabieCharacteristic(dataCharacteristicId); - return new FitnessMachineData(dataCharacteristic.ObserveValue(), fitnessMachineDataReader); - } - - public static EFitnessMachineType ReadType(this IFitnessMachineServiceConnection connection) - => ((EFitnessMachineType)BitOperations.TrailingZeroCount( - BitConverter.ToUInt16( - connection.ServiceData.AsSpan()[3..]))) - .EnsureType(); - - private static FitnessMachineDataReader GetDataReader(this EFitnessMachineType fitnessMachineType) - => new(SingleFrameStrategies.GetFor(fitnessMachineType)); - - public static async Task CreateFitnessMachineControlAsync( - this IFitnessMachineServiceConnection connection) - { - var controlPointCharacteristic = await connection.GetCharacteristicAsync(FtmsUuids.ControlPoint); - controlPointCharacteristic ??= new ThrowingCharacteristic(FtmsUuids.ControlPoint); - - return new FitnessMachineControl( - controlPointCharacteristic.ObserveValue(), - controlPointCharacteristic.WriteValueAsync); - } - - public static async Task CreateFitnessMachineStateProviderAsync( - this IFitnessMachineServiceConnection connection) - { - var machineStateCharacteristic = await connection.GetCharacteristicAsync(FtmsUuids.MachineState); - var trainingStateCharacteristic = await connection.GetCharacteristicAsync(FtmsUuids.TrainingState); - - machineStateCharacteristic ??= new ThrowingCharacteristic(FtmsUuids.MachineState); - trainingStateCharacteristic ??= new ThrowingCharacteristic(FtmsUuids.TrainingState); - - return new FitnessMachineStateProvider( - machineStateCharacteristic.ObserveValue(), - trainingStateCharacteristic.ObserveValue(), - trainingStateCharacteristic.ReadValueAsync); - } - - public static async Task ReadFitnessMachineFeaturesAsync( - this IFitnessMachineServiceConnection connection) - { - var featureCharacteristic = await connection.GetCharacteristicAsync(FtmsUuids.Feature); - featureCharacteristic.EnsureAvailabieCharacteristic(FtmsUuids.Feature); - - var featureData = await featureCharacteristic.ReadValueAsync(); - var featureDataSpan = featureData.AsSpan(); - - return new FitnessMachineFeatures(featureDataSpan[..4], featureDataSpan[5..]) - { - SpeedRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedSpeedRange, SupportedRange.ReadSpeed), - InclinationRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedInclinationRange, SupportedRange.ReadInclination), - ResistanceLevelRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedResistanceLevelRange, SupportedRange.ReadResistanceLevel), - PowerRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedPowerRange, SupportedRange.ReadPower), - HeartRateRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedHeartRateRange, SupportedRange.ReadHeartRate) - }; - - async Task ReadSupportedRangeAsync(Guid characteristicId, Func createRange) - { - var characteristic = await connection.GetCharacteristicAsync(characteristicId); - if (characteristic is null) - return null; - - var data = await characteristic.ReadValueAsync(); - if (data is null) - return null; - - return createRange(data); - } - } - - public static void EnsureAvailabieCharacteristic( - [NotNull] this IFitnessMachineCharacteristic? characteristic, - Guid characteristicUuid) - { - if (characteristic is null) - throw new NeededCharacteristicNotAvailableException(characteristicUuid); - } -} +namespace FTMS.NET; + +using FTMS.NET.Control; +using FTMS.NET.Data; +using FTMS.NET.Exceptions; +using FTMS.NET.Features; +using FTMS.NET.State; +using FTMS.NET.Utils; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; + +public static class FitnessMachineServiceFactory +{ + public static async Task CreateFitnessMachineServiceAsync( + this IFitnessMachineServiceConnection connection) + { + connection.EnsureAvailability(); + var fitnessMachineType = connection.ReadType(); + + IFitnessMachineFeatures features = await connection.ReadFitnessMachineFeaturesAsync(); + IFitnessMachineData data = await connection.CreateFitnessMachineDataAsync(); + IFitnessMachineControl control = await connection.CreateFitnessMachineControlAsync(); + IFitnessMachineStateProvider stateProvider = await connection.CreateFitnessMachineStateProviderAsync(); + + return new FitnessMachineService(fitnessMachineType, data, control, stateProvider, features); + } + + public static void EnsureAvailability(this IFitnessMachineServiceConnection connection) + { + var available = connection.ServiceData[2].IsBitSet(0); + if (available is false) + throw new FitnessMachineNotAvailableException(); + } + + public static async Task CreateFitnessMachineDataAsync( + this IFitnessMachineServiceConnection connection) + { + var fitnessMachineType = connection.ReadType(); + var fitnessMachineDataReader = fitnessMachineType.GetDataReader(); + + var dataCharacteristicId = fitnessMachineType.GetDataCharacteristicId(); + var dataCharacteristic = await connection.GetRequiredCharacteristic(dataCharacteristicId); + + return new FitnessMachineData(dataCharacteristic.ObserveValue(), fitnessMachineDataReader); + } + + public static EFitnessMachineType ReadType(this IFitnessMachineServiceConnection connection) + => ((EFitnessMachineType)BitOperations.TrailingZeroCount( + BitConverter.ToUInt16( + connection.ServiceData.AsSpan()[3..]))) + .EnsureType(); + + private static FitnessMachineDataReader GetDataReader(this EFitnessMachineType fitnessMachineType) + => new(SingleFrameStrategies.GetFor(fitnessMachineType)); + + public static async Task CreateFitnessMachineControlAsync( + this IFitnessMachineServiceConnection connection) + { + var controlPointCharacteristic = await connection.GetCharacteristicAsync(FtmsUuids.ControlPoint); + controlPointCharacteristic ??= new ThrowingCharacteristic(FtmsUuids.ControlPoint); + + return new FitnessMachineControl( + controlPointCharacteristic.ObserveValue(), + controlPointCharacteristic.WriteValueAsync); + } + + public static async Task CreateFitnessMachineStateProviderAsync( + this IFitnessMachineServiceConnection connection) + { + var machineStateCharacteristic = await connection.GetCharacteristicAsync(FtmsUuids.MachineState); + var trainingStateCharacteristic = await connection.GetCharacteristicAsync(FtmsUuids.TrainingState); + + machineStateCharacteristic ??= new ThrowingCharacteristic(FtmsUuids.MachineState); + trainingStateCharacteristic ??= new ThrowingCharacteristic(FtmsUuids.TrainingState); + + return new FitnessMachineStateProvider( + machineStateCharacteristic.ObserveValue(), + trainingStateCharacteristic.ObserveValue(), + trainingStateCharacteristic.ReadValueAsync); + } + + public static async Task ReadFitnessMachineFeaturesAsync( + this IFitnessMachineServiceConnection connection) + { + var featureCharacteristic = await connection.GetRequiredCharacteristic(FtmsUuids.Feature); + + var featureData = await featureCharacteristic.ReadValueAsync(); + var featureDataSpan = featureData.AsSpan(); + + return new FitnessMachineFeatures(featureDataSpan[..4], featureDataSpan[4..]) + { + SpeedRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedSpeedRange, SupportedRange.ReadSpeed), + InclinationRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedInclinationRange, SupportedRange.ReadInclination), + ResistanceLevelRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedResistanceLevelRange, SupportedRange.ReadResistanceLevel), + PowerRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedPowerRange, SupportedRange.ReadPower), + HeartRateRange = await ReadSupportedRangeAsync(FtmsUuids.SupportedHeartRateRange, SupportedRange.ReadHeartRate) + }; + + async Task ReadSupportedRangeAsync(Guid characteristicId, Func createRange) + { + var characteristic = await connection.GetCharacteristicAsync(characteristicId); + if (characteristic is null) + return null; + + var data = await characteristic.ReadValueAsync(); + if (data is null) + return null; + + return createRange(data); + } + } + + private static async Task GetRequiredCharacteristic( + this IFitnessMachineServiceConnection connection, + Guid characteristicUuid) + { + var characteristic = await connection.GetCharacteristicAsync(characteristicUuid); + characteristic.EnsureAvailableCharacteristic(characteristicUuid); + return characteristic; + } + + private static void EnsureAvailableCharacteristic( + [NotNull] this IFitnessMachineCharacteristic? characteristic, + Guid characteristicUuid) + { + if (characteristic is null) + throw new NeededCharacteristicNotAvailableException(characteristicUuid); + } +} \ No newline at end of file diff --git a/FTMS.NET/FitnessMachineUnit.cs b/FTMS.NET/FitnessMachineUnit.cs index 51a23c4..73e0fa7 100644 --- a/FTMS.NET/FitnessMachineUnit.cs +++ b/FTMS.NET/FitnessMachineUnit.cs @@ -1,18 +1,19 @@ -namespace FTMS.NET; -public enum FitnessMachineUnit -{ - None, - KilometersPerHour, - Percent, - Watt, - BeatsPerMinute, - Calories, - Steps, - Stride, - Millimeters, - Meters, - Seconds, - PerMinute, - MetersPerSecond, - KilogramPerMeter, -} +namespace FTMS.NET; + +public enum FitnessMachineUnit +{ + None, + KilometersPerHour, + Percent, + Watt, + BeatsPerMinute, + Calories, + Steps, + Stride, + Millimeters, + Meters, + Seconds, + PerMinute, + MetersPerSecond, + KilogramPerMeter, +} \ No newline at end of file diff --git a/FTMS.NET/FtmsUuids.cs b/FTMS.NET/FtmsUuids.cs index 64bad69..2800ad0 100644 --- a/FTMS.NET/FtmsUuids.cs +++ b/FTMS.NET/FtmsUuids.cs @@ -1,79 +1,79 @@ -namespace FTMS.NET; - -using SourceGeneration.Reflection; -using System; -using System.Collections.Frozen; - -[SourceReflection] -public static class FtmsUuids -{ - private static readonly FrozenDictionary allFieldsByUuid; - - public readonly static Guid Service = Guid.ParseExact("00001826-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid Feature = Guid.ParseExact("00002acc-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid MachineState = Guid.ParseExact("00002ada-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid TrainingState = Guid.ParseExact("00002ad3-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid ControlPoint = Guid.ParseExact("00002ad9-0000-1000-8000-00805f9b34fb", "d"); - - public readonly static Guid SupportedSpeedRange = Guid.ParseExact("00002ad4-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid SupportedInclinationRange = Guid.ParseExact("00002ad5-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid SupportedResistanceLevelRange = Guid.ParseExact("00002ad6-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid SupportedPowerRange = Guid.ParseExact("00002ad8-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid SupportedHeartRateRange = Guid.ParseExact("00002ad7-0000-1000-8000-00805f9b34fb", "d"); - - public readonly static Guid TreadmillData = Guid.ParseExact("00002acd-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid CrossTrainerData = Guid.ParseExact("00002ace-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid StepClimberData = Guid.ParseExact("00002acf-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid StairClimberData = Guid.ParseExact("00002ad0-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid RowerData = Guid.ParseExact("00002ad1-0000-1000-8000-00805f9b34fb", "d"); - public readonly static Guid IndoorBikeData = Guid.ParseExact("00002ad2-0000-1000-8000-00805f9b34fb", "d"); - - public static readonly Guid InstantaneousSpeed = new("765050cd-a033-4972-a71b-29c013c0845b"); - public static readonly Guid AverageSpeed = new("fe4f0fda-caa8-4a91-b4f6-049a7c74a68f"); - public static readonly Guid InstantaneousCadence = new("d5f34315-184e-4d9d-a3dd-b70858475f1c"); - public static readonly Guid AverageCadence = new("549c3fa5-caea-4db5-8b08-8d621db5177c"); - public static readonly Guid TotalDistance = new("20cd1373-cb1c-40ac-90f2-9b2ff0c6e74b"); - public static readonly Guid ResistantLevel = new("acba72d3-4e64-4b36-8675-202bcf43e761"); - public static readonly Guid InstantaneousPower = new("a9b3e3f6-fe62-45e0-9f82-f7a26a33378a"); - public static readonly Guid AveragePower = new("183da544-af04-42b8-861d-07ce96e261ef"); - public static readonly Guid TotalEnergy = new("ed9a5962-6b12-4679-894a-379131e12122"); - public static readonly Guid EnergyPerHour = new("8d799ac5-9c50-4119-96c9-da99ae22f91f"); - public static readonly Guid EnergyPerMinute = new("73d9c26e-356b-4ebc-83da-0f2719fca421"); - public static readonly Guid HeartRate = new("636a7fef-dd35-49ec-83df-35d52a34d4cc"); - public static readonly Guid MetabolicEquivalent = new("166f2f4c-9cff-4050-985b-2fea8b1a95bc"); - public static readonly Guid ElapsedTime = new("1eb082c4-e3ae-46f9-b4cb-d55a2f81e27b"); - public static readonly Guid RemainingTime = new("02185a25-968e-40e0-a55c-b2acf71e50fb"); - public static readonly Guid Inclination = new("d31143f6-3808-4921-aa24-abc3e714e291"); - public static readonly Guid RampAngleSetting = new("fa0e1933-d7a3-4537-a496-0a7211da6ca0"); - public static readonly Guid PositiveElevationGain = new("ef8234ce-19bd-4596-860e-30e3e632dcc6"); - public static readonly Guid NegativeElevationGain = new("bcb222de-896f-4582-8308-c44fa2c05a0c"); - public static readonly Guid InstantaneousPace = new("cb813306-52ef-463b-bde6-be92e58706ab"); - public static readonly Guid AveragePace = new("4ced1b35-7d75-4911-9afd-9a8ba9ca9d4e"); - public static readonly Guid ForceOnBelt = new("9fa8ab4b-8382-4240-9f71-2a04c221aa7f"); - public static readonly Guid PowerOutput = new("7128776e-0991-44b9-8511-ef969a8ea418"); - public static readonly Guid StepsPerMinute = new("814c417b-95f2-4cea-b86f-30625b4c2777"); - public static readonly Guid AverageStepRate = new("654ecaf1-be99-4256-a764-ff8f556d4392"); - public static readonly Guid StrideCount = new("ecdfab6f-40f4-45ad-9be9-030982a563ff"); - public static readonly Guid Floors = new("ace47522-5ea0-459b-afe7-8b70020f9e15"); - public static readonly Guid StepCount = new("97984a03-ee3f-4fd0-857f-d17500ac9038"); - public static readonly Guid StrokeRate = new("c663b25a-abef-44fe-a852-2b7ccbdb0149"); - public static readonly Guid StrokeCount = new("0119eab6-298b-4c2f-a7c4-26a4933c8365"); - public static readonly Guid AverageStrokeRate = new("07d4df62-397b-4040-ac9a-8fef6ad8a44d"); - - static FtmsUuids() - { - var thisType = typeof(FtmsUuids); - var guidType = typeof(Guid); - allFieldsByUuid = SourceReflector.GetType(thisType)? - .GetFieldsAndProperties() - .Where(fop => fop.IsStatic && fop.Accessibility == SourceAccessibility.Public) - .Where(fop => fop.MemberType == guidType) - .ToFrozenDictionary(fop => (Guid)fop.GetValue(null)!, fop => fop.Name) - ?? throw new InvalidOperationException($"{nameof(FtmsUuids)} class reflection was not generated!"); - } - - public static string GetName(Guid uuid) - { - return allFieldsByUuid.GetValueOrDefault(uuid) ?? string.Empty; - } -} +namespace FTMS.NET; + +using SourceGeneration.Reflection; +using System; +using System.Collections.Frozen; + +[SourceReflection] +public static class FtmsUuids +{ + private static readonly FrozenDictionary allFieldsByUuid; + + public readonly static Guid Service = Guid.ParseExact("00001826-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid Feature = Guid.ParseExact("00002acc-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid MachineState = Guid.ParseExact("00002ada-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid TrainingState = Guid.ParseExact("00002ad3-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid ControlPoint = Guid.ParseExact("00002ad9-0000-1000-8000-00805f9b34fb", "d"); + + public readonly static Guid SupportedSpeedRange = Guid.ParseExact("00002ad4-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid SupportedInclinationRange = Guid.ParseExact("00002ad5-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid SupportedResistanceLevelRange = Guid.ParseExact("00002ad6-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid SupportedPowerRange = Guid.ParseExact("00002ad8-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid SupportedHeartRateRange = Guid.ParseExact("00002ad7-0000-1000-8000-00805f9b34fb", "d"); + + public readonly static Guid TreadmillData = Guid.ParseExact("00002acd-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid CrossTrainerData = Guid.ParseExact("00002ace-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid StepClimberData = Guid.ParseExact("00002acf-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid StairClimberData = Guid.ParseExact("00002ad0-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid RowerData = Guid.ParseExact("00002ad1-0000-1000-8000-00805f9b34fb", "d"); + public readonly static Guid IndoorBikeData = Guid.ParseExact("00002ad2-0000-1000-8000-00805f9b34fb", "d"); + + public static readonly Guid InstantaneousSpeed = new("765050cd-a033-4972-a71b-29c013c0845b"); + public static readonly Guid AverageSpeed = new("fe4f0fda-caa8-4a91-b4f6-049a7c74a68f"); + public static readonly Guid InstantaneousCadence = new("d5f34315-184e-4d9d-a3dd-b70858475f1c"); + public static readonly Guid AverageCadence = new("549c3fa5-caea-4db5-8b08-8d621db5177c"); + public static readonly Guid TotalDistance = new("20cd1373-cb1c-40ac-90f2-9b2ff0c6e74b"); + public static readonly Guid ResistantLevel = new("acba72d3-4e64-4b36-8675-202bcf43e761"); + public static readonly Guid InstantaneousPower = new("a9b3e3f6-fe62-45e0-9f82-f7a26a33378a"); + public static readonly Guid AveragePower = new("183da544-af04-42b8-861d-07ce96e261ef"); + public static readonly Guid TotalEnergy = new("ed9a5962-6b12-4679-894a-379131e12122"); + public static readonly Guid EnergyPerHour = new("8d799ac5-9c50-4119-96c9-da99ae22f91f"); + public static readonly Guid EnergyPerMinute = new("73d9c26e-356b-4ebc-83da-0f2719fca421"); + public static readonly Guid HeartRate = new("636a7fef-dd35-49ec-83df-35d52a34d4cc"); + public static readonly Guid MetabolicEquivalent = new("166f2f4c-9cff-4050-985b-2fea8b1a95bc"); + public static readonly Guid ElapsedTime = new("1eb082c4-e3ae-46f9-b4cb-d55a2f81e27b"); + public static readonly Guid RemainingTime = new("02185a25-968e-40e0-a55c-b2acf71e50fb"); + public static readonly Guid Inclination = new("d31143f6-3808-4921-aa24-abc3e714e291"); + public static readonly Guid RampAngleSetting = new("fa0e1933-d7a3-4537-a496-0a7211da6ca0"); + public static readonly Guid PositiveElevationGain = new("ef8234ce-19bd-4596-860e-30e3e632dcc6"); + public static readonly Guid NegativeElevationGain = new("bcb222de-896f-4582-8308-c44fa2c05a0c"); + public static readonly Guid InstantaneousPace = new("cb813306-52ef-463b-bde6-be92e58706ab"); + public static readonly Guid AveragePace = new("4ced1b35-7d75-4911-9afd-9a8ba9ca9d4e"); + public static readonly Guid ForceOnBelt = new("9fa8ab4b-8382-4240-9f71-2a04c221aa7f"); + public static readonly Guid PowerOutput = new("7128776e-0991-44b9-8511-ef969a8ea418"); + public static readonly Guid StepsPerMinute = new("814c417b-95f2-4cea-b86f-30625b4c2777"); + public static readonly Guid AverageStepRate = new("654ecaf1-be99-4256-a764-ff8f556d4392"); + public static readonly Guid StrideCount = new("ecdfab6f-40f4-45ad-9be9-030982a563ff"); + public static readonly Guid Floors = new("ace47522-5ea0-459b-afe7-8b70020f9e15"); + public static readonly Guid StepCount = new("97984a03-ee3f-4fd0-857f-d17500ac9038"); + public static readonly Guid StrokeRate = new("c663b25a-abef-44fe-a852-2b7ccbdb0149"); + public static readonly Guid StrokeCount = new("0119eab6-298b-4c2f-a7c4-26a4933c8365"); + public static readonly Guid AverageStrokeRate = new("07d4df62-397b-4040-ac9a-8fef6ad8a44d"); + + static FtmsUuids() + { + var thisType = typeof(FtmsUuids); + var guidType = typeof(Guid); + allFieldsByUuid = SourceReflector.GetType(thisType)? + .GetFieldsAndProperties() + .Where(fop => fop.IsStatic && fop.Accessibility == SourceAccessibility.Public) + .Where(fop => fop.MemberType == guidType) + .ToFrozenDictionary(fop => (Guid)fop.GetValue(null)!, fop => fop.Name) + ?? throw new InvalidOperationException($"{nameof(FtmsUuids)} class reflection was not generated!"); + } + + public static string GetName(Guid uuid) + { + return allFieldsByUuid.GetValueOrDefault(uuid) ?? string.Empty; + } +} \ No newline at end of file diff --git a/FTMS.NET/IFitnessMachineCharacteristic.cs b/FTMS.NET/IFitnessMachineCharacteristic.cs index 9576ec9..5a05fec 100644 --- a/FTMS.NET/IFitnessMachineCharacteristic.cs +++ b/FTMS.NET/IFitnessMachineCharacteristic.cs @@ -1,12 +1,12 @@ -namespace FTMS.NET; - -using System; -using System.Threading.Tasks; - -public interface IFitnessMachineCharacteristic -{ - public Guid Id { get; } - Task ReadValueAsync(); - Task WriteValueAsync(byte[] value); - IObservable ObserveValue(); -} +namespace FTMS.NET; + +using System; +using System.Threading.Tasks; + +public interface IFitnessMachineCharacteristic +{ + public Guid Id { get; } + Task ReadValueAsync(); + Task WriteValueAsync(byte[] value); + IObservable ObserveValue(); +} \ No newline at end of file diff --git a/FTMS.NET/IFitnessMachineService.cs b/FTMS.NET/IFitnessMachineService.cs index c5a9d45..04cd9b7 100644 --- a/FTMS.NET/IFitnessMachineService.cs +++ b/FTMS.NET/IFitnessMachineService.cs @@ -1,16 +1,16 @@ -namespace FTMS.NET; - -using FTMS.NET.Control; -using FTMS.NET.Data; -using FTMS.NET.Features; -using FTMS.NET.State; - -public interface IFitnessMachineService - : IFitnessMachineControl, IFitnessMachineStateProvider, IFitnessMachineData, IDisposable -{ - EFitnessMachineType Type { get; } - IFitnessMachineData Data { get; } - IFitnessMachineStateProvider State { get; } - IFitnessMachineControl Control { get; } - IFitnessMachineFeatures Features { get; } -} +namespace FTMS.NET; + +using FTMS.NET.Control; +using FTMS.NET.Data; +using FTMS.NET.Features; +using FTMS.NET.State; + +public interface IFitnessMachineService + : IFitnessMachineControl, IFitnessMachineStateProvider, IFitnessMachineData, IDisposable +{ + EFitnessMachineType Type { get; } + IFitnessMachineData Data { get; } + IFitnessMachineStateProvider State { get; } + IFitnessMachineControl Control { get; } + IFitnessMachineFeatures Features { get; } +} \ No newline at end of file diff --git a/FTMS.NET/IFitnessMachineServiceConnection.cs b/FTMS.NET/IFitnessMachineServiceConnection.cs index 64d69d9..abaf435 100644 --- a/FTMS.NET/IFitnessMachineServiceConnection.cs +++ b/FTMS.NET/IFitnessMachineServiceConnection.cs @@ -1,10 +1,10 @@ -namespace FTMS.NET; - -using System; -using System.Threading.Tasks; - -public interface IFitnessMachineServiceConnection -{ - byte[] ServiceData { get; } - Task GetCharacteristicAsync(Guid id); -} +namespace FTMS.NET; + +using System; +using System.Threading.Tasks; + +public interface IFitnessMachineServiceConnection +{ + byte[] ServiceData { get; } + Task GetCharacteristicAsync(Guid id); +} \ No newline at end of file diff --git a/FTMS.NET/State/ESpinDownState.cs b/FTMS.NET/State/ESpinDownState.cs index 992d61a..8385321 100644 --- a/FTMS.NET/State/ESpinDownState.cs +++ b/FTMS.NET/State/ESpinDownState.cs @@ -1,8 +1,9 @@ -namespace FTMS.NET.State; -public enum ESpinDownState : byte -{ - SpinDownRequested = 0x01, - Success = 0x02, - Error = 0x03, - StopPedaling = 0x04 -} +namespace FTMS.NET.State; + +public enum ESpinDownState : byte +{ + SpinDownRequested = 0x01, + Success = 0x02, + Error = 0x03, + StopPedaling = 0x04 +} \ No newline at end of file diff --git a/FTMS.NET/State/EStateOpCode.cs b/FTMS.NET/State/EStateOpCode.cs index 06496c9..05ef310 100644 --- a/FTMS.NET/State/EStateOpCode.cs +++ b/FTMS.NET/State/EStateOpCode.cs @@ -1,27 +1,28 @@ -namespace FTMS.NET.State; -public enum EStateOpCode : byte -{ - Reset = 0x01, - FitnessMachineStoppedOrPausedByTheUser = 0x02, - FitnessMachineStoppedBySafetyKey = 0x03, - FitnessMachineStartedOrResumedByTheUser = 0x04, - TargetSpeedChanged = 0x05, - TargetInclineChanged = 0x06, - TargetResistanceLevelChanged = 0x07, - TargetPowerChanged = 0x08, - TargetHeartRateChanged = 0x09, - TargetedExpendedEnergyChanged = 0x0A, - TargetedNumberOfStepsChanged = 0x0B, - TargetedNumberOfStridesChanged = 0x0C, - TargetedDistanceChanged = 0x0D, - TargetedTrainingTimeChanged = 0x0E, - TargetedTimeInTwoHeartRateZonesChanged = 0x0F, - TargetedTimeInThreeHeartRateZonesChanged = 0x10, - TargetedTimeInFiveHeartRateZonesChanged = 0x11, - IndoorBikeSimulationParametersChanged = 0x12, - WheelCircumferenceChanged = 0x13, - SpinDownState = 0x14, - TargetedCadenceChanged = 0x15, - - ControlPermissionLost = 0xFF -} +namespace FTMS.NET.State; + +public enum EStateOpCode : byte +{ + Reset = 0x01, + FitnessMachineStoppedOrPausedByTheUser = 0x02, + FitnessMachineStoppedBySafetyKey = 0x03, + FitnessMachineStartedOrResumedByTheUser = 0x04, + TargetSpeedChanged = 0x05, + TargetInclineChanged = 0x06, + TargetResistanceLevelChanged = 0x07, + TargetPowerChanged = 0x08, + TargetHeartRateChanged = 0x09, + TargetedExpendedEnergyChanged = 0x0A, + TargetedNumberOfStepsChanged = 0x0B, + TargetedNumberOfStridesChanged = 0x0C, + TargetedDistanceChanged = 0x0D, + TargetedTrainingTimeChanged = 0x0E, + TargetedTimeInTwoHeartRateZonesChanged = 0x0F, + TargetedTimeInThreeHeartRateZonesChanged = 0x10, + TargetedTimeInFiveHeartRateZonesChanged = 0x11, + IndoorBikeSimulationParametersChanged = 0x12, + WheelCircumferenceChanged = 0x13, + SpinDownState = 0x14, + TargetedCadenceChanged = 0x15, + + ControlPermissionLost = 0xFF +} \ No newline at end of file diff --git a/FTMS.NET/State/ETrainingState.cs b/FTMS.NET/State/ETrainingState.cs index 80da977..c4d0fd6 100644 --- a/FTMS.NET/State/ETrainingState.cs +++ b/FTMS.NET/State/ETrainingState.cs @@ -1,20 +1,21 @@ -namespace FTMS.NET.State; -public enum ETrainingState : byte -{ - Other = 0x00, - Idle = 0x01, - WarmingUp = 0x02, - LowIntensityInterval = 0x03, - HighIntensityInterval = 0x04, - RecoveryInterval = 0x05, - Isometric = 0x06, - HeartRateControl = 0x07, - FitnessTest = 0x08, - SpeedLowerOfControlRegion = 0x09, - SpeedHigherOfControlRegion = 0x0A, - CoolDown = 0x0B, - WattControl = 0x0C, - ManualMode = 0x0D, - PreWorkout = 0x0E, - PostWorkout = 0x0F, -} +namespace FTMS.NET.State; + +public enum ETrainingState : byte +{ + Other = 0x00, + Idle = 0x01, + WarmingUp = 0x02, + LowIntensityInterval = 0x03, + HighIntensityInterval = 0x04, + RecoveryInterval = 0x05, + Isometric = 0x06, + HeartRateControl = 0x07, + FitnessTest = 0x08, + SpeedLowerOfControlRegion = 0x09, + SpeedHigherOfControlRegion = 0x0A, + CoolDown = 0x0B, + WattControl = 0x0C, + ManualMode = 0x0D, + PreWorkout = 0x0E, + PostWorkout = 0x0F, +} \ No newline at end of file diff --git a/FTMS.NET/State/FitnessMachineState.cs b/FTMS.NET/State/FitnessMachineState.cs index 264ddbc..a18ecc3 100644 --- a/FTMS.NET/State/FitnessMachineState.cs +++ b/FTMS.NET/State/FitnessMachineState.cs @@ -1,14 +1,14 @@ -namespace FTMS.NET.State; - -using System.Collections.Immutable; - -internal sealed record FitnessMachineState( - EStateOpCode OpCode, - byte[] RawData) - : IFitnessMachineState -{ - private IImmutableList? parameters; - - public IImmutableList ReadParameters() - => this.parameters ??= FitnessMachineStateParameterFactory.ReadParameters(OpCode, RawData).ToImmutableList(); -} +namespace FTMS.NET.State; + +using System.Collections.Immutable; + +internal sealed record FitnessMachineState( + EStateOpCode OpCode, + byte[] RawData) + : IFitnessMachineState +{ + private IImmutableList? parameters; + + public IImmutableList ReadParameters() + => this.parameters ??= FitnessMachineStateParameterFactory.ReadParameters(OpCode, RawData).ToImmutableList(); +} \ No newline at end of file diff --git a/FTMS.NET/State/FitnessMachineStateParameter.cs b/FTMS.NET/State/FitnessMachineStateParameter.cs index 0fb6c49..462a1d1 100644 --- a/FTMS.NET/State/FitnessMachineStateParameter.cs +++ b/FTMS.NET/State/FitnessMachineStateParameter.cs @@ -1,6 +1,6 @@ -namespace FTMS.NET.State; - -public sealed record FitnessMachineStateParameter( - string Name, - FitnessMachineUnit Unit, +namespace FTMS.NET.State; + +public sealed record FitnessMachineStateParameter( + string Name, + FitnessMachineUnit Unit, double Value); \ No newline at end of file diff --git a/FTMS.NET/State/FitnessMachineStateParameterFactory.cs b/FTMS.NET/State/FitnessMachineStateParameterFactory.cs index 309373c..73c6785 100644 --- a/FTMS.NET/State/FitnessMachineStateParameterFactory.cs +++ b/FTMS.NET/State/FitnessMachineStateParameterFactory.cs @@ -1,128 +1,128 @@ -namespace FTMS.NET.State; - -using FTMS.NET.Control; -using FTMS.NET.Utils; -using System; - -public static class FitnessMachineStateParameterFactory -{ - private static readonly Dictionary> parameterCalculations = new() - { - { EStateOpCode.Reset, ReadEmpty }, - { EStateOpCode.FitnessMachineStoppedOrPausedByTheUser, ReadStopOrPauseCode }, - { EStateOpCode.FitnessMachineStoppedBySafetyKey, ReadEmpty }, - { EStateOpCode.FitnessMachineStartedOrResumedByTheUser, ReadEmpty }, - { EStateOpCode.TargetSpeedChanged, ReadTargetSpeed }, - { EStateOpCode.TargetInclineChanged, ReadTargetIncline }, - { EStateOpCode.TargetResistanceLevelChanged, ReadTargetResistanceLevel }, - { EStateOpCode.TargetPowerChanged, ReadTargetPower }, - { EStateOpCode.TargetHeartRateChanged, ReadTargetHeartRate }, - { EStateOpCode.TargetedExpendedEnergyChanged, ReadTargetedExpendedEnergy }, - { EStateOpCode.TargetedNumberOfStepsChanged, ReadTargetedNumberOfSteps }, - { EStateOpCode.TargetedNumberOfStridesChanged, ReadTargetedNumberOfStrides }, - { EStateOpCode.TargetedDistanceChanged, ReadTargetedDistance }, - { EStateOpCode.TargetedTrainingTimeChanged, ReadTargetedTrainingTime }, - { EStateOpCode.TargetedTimeInTwoHeartRateZonesChanged, ReadTargetedTimeInTwoHeartRateZones }, - { EStateOpCode.TargetedTimeInThreeHeartRateZonesChanged, ReadTargetedTimeInThreeHeartRateZones }, - { EStateOpCode.TargetedTimeInFiveHeartRateZonesChanged, ReadTargetedTimeInFiveHeartRateZones }, - { EStateOpCode.IndoorBikeSimulationParametersChanged, ReadIndoorBikeSimulationParameters }, - { EStateOpCode.WheelCircumferenceChanged, ReadWheelCircumference }, - { EStateOpCode.SpinDownState, ReadSpinDownState }, - { EStateOpCode.TargetedCadenceChanged, ReadTargetedCadence }, - { EStateOpCode.ControlPermissionLost, ReadEmpty }, - }; - - public static IEnumerable ReadParameters(EStateOpCode opCode, byte[] rawData) - { - var parameter = parameterCalculations[opCode].Invoke(rawData); - if (parameter is IEnumerable multipleParameters) - return multipleParameters; - return [parameter]; - } - - private static IEnumerable ReadEmpty(byte[] rawData) => []; - - private static object ReadStopOrPauseCode(byte[] rawData) - => (EStopOrPauseCode)rawData[0]; - - private static FitnessMachineStateParameter ReadTargetSpeed(byte[] rawData) - => new("Target Speed", FitnessMachineUnit.KilometersPerHour, BitConverter.ToUInt16(rawData) * 0.01); - - private static FitnessMachineStateParameter ReadTargetIncline(byte[] rawData) - => new("Target Incline", FitnessMachineUnit.Percent, BitConverter.ToUInt16(rawData) * 0.1); - - private static FitnessMachineStateParameter ReadTargetResistanceLevel(byte[] rawData) - => new("Target Resistance Level", FitnessMachineUnit.None, rawData[0]); - - private static FitnessMachineStateParameter ReadTargetPower(byte[] rawData) - => new("Target Power", FitnessMachineUnit.Watt, BitConverter.ToInt16(rawData)); - - private static FitnessMachineStateParameter ReadTargetHeartRate(byte[] rawData) - => new("Target Heart Rate", FitnessMachineUnit.BeatsPerMinute, rawData[0]); - - private static FitnessMachineStateParameter ReadTargetedExpendedEnergy(byte[] rawData) - => new("Targeted Expended Energy", FitnessMachineUnit.Calories, BitConverter.ToUInt16(rawData)); - - private static FitnessMachineStateParameter ReadTargetedNumberOfSteps(byte[] rawData) - => new("Targeted Number Of Steps", FitnessMachineUnit.Steps, BitConverter.ToUInt16(rawData)); - - private static FitnessMachineStateParameter ReadTargetedNumberOfStrides(byte[] rawData) - => new("Targeted Number Of Strides", FitnessMachineUnit.Stride, BitConverter.ToUInt16(rawData)); - - private static FitnessMachineStateParameter ReadTargetedDistance(byte[] rawData) - => new("Targeted Distance", FitnessMachineUnit.Meters, new UInt24(BitConverter.ToUInt16(rawData))); - - private static FitnessMachineStateParameter ReadTargetedTrainingTime(byte[] rawData) - => new("Targeted Training Time", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(rawData)); - - private static IEnumerable ReadTargetedTimeInTwoHeartRateZones(byte[] rawData) - { - var dataSpan = rawData.AsSpan(); - return [ - new("Targeted Time in Fat Burn Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[..2])), - new("Targeted Time in Fitness Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[2..])) - ]; - } - - private static IEnumerable ReadTargetedTimeInThreeHeartRateZones(byte[] rawData) - { - var dataSpan = rawData.AsSpan(); - return [ - new("Targeted Time in Light Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[..2])), - new("Targeted Time in Moderate Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[2..4])), - new("Targeted Time in Hard Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[4..])) - ]; - } - - private static IEnumerable ReadTargetedTimeInFiveHeartRateZones(byte[] rawData) - { - var dataSpan = rawData.AsSpan(); - return [ - new("Targeted Time in Very Light Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[..2])), - new("Targeted Time in Light Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[2..4])), - new("Targeted Time in Moderate Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[4..6])), - new("Targeted Time in Hard Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[6..8])), - new("Targeted Time in Maximum Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[8..])) - ]; - } - - private static IEnumerable ReadIndoorBikeSimulationParameters(byte[] rawData) - { - var dataSpan = rawData.AsSpan(); - return [ - new("Wind Speed", FitnessMachineUnit.MetersPerSecond, BitConverter.ToInt16(dataSpan[..2]) * 0.001), - new("Grade", FitnessMachineUnit.Percent, BitConverter.ToInt16(dataSpan[2..4]) * 0.01), - new("Coefficient of Rolling Resistance", FitnessMachineUnit.None, dataSpan[4] * 0.0001), - new("Wind Resistance Coefficient", FitnessMachineUnit.KilogramPerMeter, dataSpan[5] * 0.01) - ]; - } - - private static FitnessMachineStateParameter ReadWheelCircumference(byte[] rawData) - => new("Wheel Circumference", FitnessMachineUnit.Millimeters, BitConverter.ToUInt16(rawData) * 0.1); - - private static object ReadSpinDownState(byte[] rawData) - => (ESpinDownState)rawData[0]; - - private static FitnessMachineStateParameter ReadTargetedCadence(byte[] rawData) - => new("Targeted Cadence", FitnessMachineUnit.PerMinute, BitConverter.ToUInt16(rawData) * 0.5); -} +namespace FTMS.NET.State; + +using FTMS.NET.Control; +using FTMS.NET.Utils; +using System; + +public static class FitnessMachineStateParameterFactory +{ + private static readonly Dictionary> parameterCalculations = new() + { + { EStateOpCode.Reset, ReadEmpty }, + { EStateOpCode.FitnessMachineStoppedOrPausedByTheUser, ReadStopOrPauseCode }, + { EStateOpCode.FitnessMachineStoppedBySafetyKey, ReadEmpty }, + { EStateOpCode.FitnessMachineStartedOrResumedByTheUser, ReadEmpty }, + { EStateOpCode.TargetSpeedChanged, ReadTargetSpeed }, + { EStateOpCode.TargetInclineChanged, ReadTargetIncline }, + { EStateOpCode.TargetResistanceLevelChanged, ReadTargetResistanceLevel }, + { EStateOpCode.TargetPowerChanged, ReadTargetPower }, + { EStateOpCode.TargetHeartRateChanged, ReadTargetHeartRate }, + { EStateOpCode.TargetedExpendedEnergyChanged, ReadTargetedExpendedEnergy }, + { EStateOpCode.TargetedNumberOfStepsChanged, ReadTargetedNumberOfSteps }, + { EStateOpCode.TargetedNumberOfStridesChanged, ReadTargetedNumberOfStrides }, + { EStateOpCode.TargetedDistanceChanged, ReadTargetedDistance }, + { EStateOpCode.TargetedTrainingTimeChanged, ReadTargetedTrainingTime }, + { EStateOpCode.TargetedTimeInTwoHeartRateZonesChanged, ReadTargetedTimeInTwoHeartRateZones }, + { EStateOpCode.TargetedTimeInThreeHeartRateZonesChanged, ReadTargetedTimeInThreeHeartRateZones }, + { EStateOpCode.TargetedTimeInFiveHeartRateZonesChanged, ReadTargetedTimeInFiveHeartRateZones }, + { EStateOpCode.IndoorBikeSimulationParametersChanged, ReadIndoorBikeSimulationParameters }, + { EStateOpCode.WheelCircumferenceChanged, ReadWheelCircumference }, + { EStateOpCode.SpinDownState, ReadSpinDownState }, + { EStateOpCode.TargetedCadenceChanged, ReadTargetedCadence }, + { EStateOpCode.ControlPermissionLost, ReadEmpty }, + }; + + public static IEnumerable ReadParameters(EStateOpCode opCode, byte[] rawData) + { + var parameter = parameterCalculations[opCode].Invoke(rawData); + if (parameter is IEnumerable multipleParameters) + return multipleParameters; + return [parameter]; + } + + private static IEnumerable ReadEmpty(byte[] rawData) => []; + + private static object ReadStopOrPauseCode(byte[] rawData) + => (EStopOrPauseCode)rawData[0]; + + private static FitnessMachineStateParameter ReadTargetSpeed(byte[] rawData) + => new("Target Speed", FitnessMachineUnit.KilometersPerHour, BitConverter.ToUInt16(rawData) * 0.01); + + private static FitnessMachineStateParameter ReadTargetIncline(byte[] rawData) + => new("Target Incline", FitnessMachineUnit.Percent, BitConverter.ToInt16(rawData) * 0.1); + + private static FitnessMachineStateParameter ReadTargetResistanceLevel(byte[] rawData) + => new("Target Resistance Level", FitnessMachineUnit.None, rawData[0]); + + private static FitnessMachineStateParameter ReadTargetPower(byte[] rawData) + => new("Target Power", FitnessMachineUnit.Watt, BitConverter.ToInt16(rawData)); + + private static FitnessMachineStateParameter ReadTargetHeartRate(byte[] rawData) + => new("Target Heart Rate", FitnessMachineUnit.BeatsPerMinute, rawData[0]); + + private static FitnessMachineStateParameter ReadTargetedExpendedEnergy(byte[] rawData) + => new("Targeted Expended Energy", FitnessMachineUnit.Calories, BitConverter.ToUInt16(rawData)); + + private static FitnessMachineStateParameter ReadTargetedNumberOfSteps(byte[] rawData) + => new("Targeted Number Of Steps", FitnessMachineUnit.Steps, BitConverter.ToUInt16(rawData)); + + private static FitnessMachineStateParameter ReadTargetedNumberOfStrides(byte[] rawData) + => new("Targeted Number Of Strides", FitnessMachineUnit.Stride, BitConverter.ToUInt16(rawData)); + + private static FitnessMachineStateParameter ReadTargetedDistance(byte[] rawData) + => new("Targeted Distance", FitnessMachineUnit.Meters, new UInt24(rawData[0], rawData[1], rawData[2])); + + private static FitnessMachineStateParameter ReadTargetedTrainingTime(byte[] rawData) + => new("Targeted Training Time", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(rawData)); + + private static IEnumerable ReadTargetedTimeInTwoHeartRateZones(byte[] rawData) + { + var dataSpan = rawData.AsSpan(); + return [ + new("Targeted Time in Fat Burn Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[..2])), + new("Targeted Time in Fitness Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[2..])) + ]; + } + + private static IEnumerable ReadTargetedTimeInThreeHeartRateZones(byte[] rawData) + { + var dataSpan = rawData.AsSpan(); + return [ + new("Targeted Time in Light Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[..2])), + new("Targeted Time in Moderate Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[2..4])), + new("Targeted Time in Hard Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[4..])) + ]; + } + + private static IEnumerable ReadTargetedTimeInFiveHeartRateZones(byte[] rawData) + { + var dataSpan = rawData.AsSpan(); + return [ + new("Targeted Time in Very Light Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[..2])), + new("Targeted Time in Light Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[2..4])), + new("Targeted Time in Moderate Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[4..6])), + new("Targeted Time in Hard Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[6..8])), + new("Targeted Time in Maximum Zone", FitnessMachineUnit.Seconds, BitConverter.ToUInt16(dataSpan[8..])) + ]; + } + + private static IEnumerable ReadIndoorBikeSimulationParameters(byte[] rawData) + { + var dataSpan = rawData.AsSpan(); + return [ + new("Wind Speed", FitnessMachineUnit.MetersPerSecond, BitConverter.ToInt16(dataSpan[..2]) * 0.001), + new("Grade", FitnessMachineUnit.Percent, BitConverter.ToInt16(dataSpan[2..4]) * 0.01), + new("Coefficient of Rolling Resistance", FitnessMachineUnit.None, dataSpan[4] * 0.0001), + new("Wind Resistance Coefficient", FitnessMachineUnit.KilogramPerMeter, dataSpan[5] * 0.01) + ]; + } + + private static FitnessMachineStateParameter ReadWheelCircumference(byte[] rawData) + => new("Wheel Circumference", FitnessMachineUnit.Millimeters, BitConverter.ToUInt16(rawData) * 0.1); + + private static object ReadSpinDownState(byte[] rawData) + => (ESpinDownState)rawData[0]; + + private static FitnessMachineStateParameter ReadTargetedCadence(byte[] rawData) + => new("Targeted Cadence", FitnessMachineUnit.PerMinute, BitConverter.ToUInt16(rawData) * 0.5); +} \ No newline at end of file diff --git a/FTMS.NET/State/FitnessMachineStateProvider.cs b/FTMS.NET/State/FitnessMachineStateProvider.cs index 6ab5408..7cc9674 100644 --- a/FTMS.NET/State/FitnessMachineStateProvider.cs +++ b/FTMS.NET/State/FitnessMachineStateProvider.cs @@ -1,73 +1,73 @@ -namespace FTMS.NET.State; - -using FTMS.NET.Utils; -using System; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Text; - -internal sealed class FitnessMachineStateProvider : IFitnessMachineStateProvider -{ - private readonly IObservable machineStateObservable; - private readonly IObservable trainingStateObservable; - private readonly CancellationDisposable cancellationDisposable = new(); - private readonly Func> readTrainingStateAsync; - - public FitnessMachineStateProvider( - IObservable observeMachineState, - IObservable observeTrainingState, - Func> readTrainingStateAsync) - { - this.machineStateObservable = observeMachineState - .TakeUntil(this.cancellationDisposable.Token) - .Select(this.ReadMachineStateData) - .Publish() - .RefCount(); - - this.trainingStateObservable = observeTrainingState - .TakeUntil(this.cancellationDisposable.Token) - .SelectMany(this.ReadTrainingStateDataAsync) - .Publish() - .RefCount(); - - this.readTrainingStateAsync = readTrainingStateAsync; - } - - public IObservable ObserveMachineState() - => this.machineStateObservable.AsObservable(); - - public IObservable ObserveTrainingState() - => this.trainingStateObservable.AsObservable(); - - private IFitnessMachineState ReadMachineStateData(byte[] data) - { - var opCode = (EStateOpCode)data.First(); - byte[] parameter = [.. data.Skip(1)]; - - return new FitnessMachineState(opCode, parameter); - } - - private async Task ReadTrainingStateDataAsync(byte[] data) - { - var flags = data[0]; - var trainingStateValue = (ETrainingState)data[1]; - string? detailString = await ReadDetailString(); - - return new TrainingState(trainingStateValue, detailString); - - async Task ReadDetailString() - { - if (flags.IsBitSet(0)) - return Encoding.UTF8.GetString(data.AsSpan()[2..]); - else if (flags.IsBitSet(1)) - { - var detailBytes = await this.readTrainingStateAsync(); - return Encoding.UTF8.GetString(detailBytes.AsSpan()); - } - return null; - } - } - - public void Dispose() => this.cancellationDisposable.Dispose(); -} +namespace FTMS.NET.State; + +using FTMS.NET.Utils; +using System; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text; + +internal sealed class FitnessMachineStateProvider : IFitnessMachineStateProvider +{ + private readonly IObservable machineStateObservable; + private readonly IObservable trainingStateObservable; + private readonly CancellationDisposable cancellationDisposable = new(); + private readonly Func> readTrainingStateAsync; + + public FitnessMachineStateProvider( + IObservable observeMachineState, + IObservable observeTrainingState, + Func> readTrainingStateAsync) + { + this.machineStateObservable = observeMachineState + .TakeUntil(this.cancellationDisposable.Token) + .Select(this.ReadMachineStateData) + .Publish() + .RefCount(); + + this.trainingStateObservable = observeTrainingState + .TakeUntil(this.cancellationDisposable.Token) + .SelectMany(this.ReadTrainingStateDataAsync) + .Publish() + .RefCount(); + + this.readTrainingStateAsync = readTrainingStateAsync; + } + + public IObservable ObserveMachineState() + => this.machineStateObservable.AsObservable(); + + public IObservable ObserveTrainingState() + => this.trainingStateObservable.AsObservable(); + + private IFitnessMachineState ReadMachineStateData(byte[] data) + { + var opCode = (EStateOpCode)data.First(); + byte[] parameter = [.. data.Skip(1)]; + + return new FitnessMachineState(opCode, parameter); + } + + private async Task ReadTrainingStateDataAsync(byte[] data) + { + var flags = data[0]; + var trainingStateValue = (ETrainingState)data[1]; + string? detailString = await ReadDetailString(); + + return new TrainingState(trainingStateValue, detailString); + + async Task ReadDetailString() + { + if (flags.IsBitSet(0)) + return Encoding.UTF8.GetString(data.AsSpan()[2..]); + else if (flags.IsBitSet(1)) + { + var detailBytes = await this.readTrainingStateAsync(); + return Encoding.UTF8.GetString(detailBytes.AsSpan()); + } + return null; + } + } + + public void Dispose() => this.cancellationDisposable.Dispose(); +} \ No newline at end of file diff --git a/FTMS.NET/State/IFitnessMachineState.cs b/FTMS.NET/State/IFitnessMachineState.cs index 234b262..651abd2 100644 --- a/FTMS.NET/State/IFitnessMachineState.cs +++ b/FTMS.NET/State/IFitnessMachineState.cs @@ -1,10 +1,10 @@ -namespace FTMS.NET.State; - -using System.Collections.Immutable; - -public interface IFitnessMachineState -{ - EStateOpCode OpCode { get; } - byte[] RawData { get; } - IImmutableList ReadParameters(); -} +namespace FTMS.NET.State; + +using System.Collections.Immutable; + +public interface IFitnessMachineState +{ + EStateOpCode OpCode { get; } + byte[] RawData { get; } + IImmutableList ReadParameters(); +} \ No newline at end of file diff --git a/FTMS.NET/State/IFitnessMachineStateProvider.cs b/FTMS.NET/State/IFitnessMachineStateProvider.cs index e8728e0..aedb7df 100644 --- a/FTMS.NET/State/IFitnessMachineStateProvider.cs +++ b/FTMS.NET/State/IFitnessMachineStateProvider.cs @@ -1,9 +1,9 @@ -namespace FTMS.NET.State; - -using System; - -public interface IFitnessMachineStateProvider : IDisposable -{ - IObservable ObserveMachineState(); - IObservable ObserveTrainingState(); -} +namespace FTMS.NET.State; + +using System; + +public interface IFitnessMachineStateProvider : IDisposable +{ + IObservable ObserveMachineState(); + IObservable ObserveTrainingState(); +} \ No newline at end of file diff --git a/FTMS.NET/State/ITrainingState.cs b/FTMS.NET/State/ITrainingState.cs index bde8785..2de5101 100644 --- a/FTMS.NET/State/ITrainingState.cs +++ b/FTMS.NET/State/ITrainingState.cs @@ -1,6 +1,7 @@ -namespace FTMS.NET.State; -public interface ITrainingState -{ - ETrainingState State { get; } - string? Details { get; } -} +namespace FTMS.NET.State; + +public interface ITrainingState +{ + ETrainingState State { get; } + string? Details { get; } +} \ No newline at end of file diff --git a/FTMS.NET/State/TrainingState.cs b/FTMS.NET/State/TrainingState.cs index 79f0aff..2097594 100644 --- a/FTMS.NET/State/TrainingState.cs +++ b/FTMS.NET/State/TrainingState.cs @@ -1,3 +1,3 @@ -namespace FTMS.NET.State; - +namespace FTMS.NET.State; + internal sealed record TrainingState(ETrainingState State, string? Details) : ITrainingState; \ No newline at end of file diff --git a/FTMS.NET/ThrowingCharacteristic.cs b/FTMS.NET/ThrowingCharacteristic.cs index b541716..eb9dd99 100644 --- a/FTMS.NET/ThrowingCharacteristic.cs +++ b/FTMS.NET/ThrowingCharacteristic.cs @@ -1,19 +1,19 @@ -namespace FTMS.NET; - -using FTMS.NET.Exceptions; -using System; -using System.Reactive.Linq; -using System.Threading.Tasks; - -internal sealed class ThrowingCharacteristic(Guid uuid) : IFitnessMachineCharacteristic -{ - private readonly NeededCharacteristicNotAvailableException exception = new(uuid); - - public Guid Id => throw this.exception; - - public IObservable ObserveValue() => Observable.Throw(this.exception); - - public Task ReadValueAsync() => throw this.exception; - - public Task WriteValueAsync(byte[] value) => throw this.exception; -} +namespace FTMS.NET; + +using FTMS.NET.Exceptions; +using System; +using System.Reactive.Linq; +using System.Threading.Tasks; + +internal sealed class ThrowingCharacteristic(Guid uuid) : IFitnessMachineCharacteristic +{ + private readonly NeededCharacteristicNotAvailableException exception = new(uuid); + + public Guid Id => throw this.exception; + + public IObservable ObserveValue() => Observable.Throw(this.exception); + + public Task ReadValueAsync() => throw this.exception; + + public Task WriteValueAsync(byte[] value) => throw this.exception; +} \ No newline at end of file diff --git a/FTMS.NET/Utils/ByteExtensions.cs b/FTMS.NET/Utils/ByteExtensions.cs index 399723e..f36dc19 100644 --- a/FTMS.NET/Utils/ByteExtensions.cs +++ b/FTMS.NET/Utils/ByteExtensions.cs @@ -1,40 +1,40 @@ -namespace FTMS.NET.Utils; - -internal static class ByteExtensions -{ - public static bool IsBitSet(this byte[] data, int pos) - { - if (pos < 0) - throw new IndexOutOfRangeException(); - - int byteIndex = pos / 7; - int bitIndex = pos % 7; - - if (byteIndex >= data.Length) - throw new IndexOutOfRangeException(); - - return data[byteIndex].IsBitSet(bitIndex); - } - - public static bool IsBitSet(this ReadOnlySpan data, int pos) - { - if (pos < 0) - throw new IndexOutOfRangeException(); - - int byteIndex = pos / 7; - int bitIndex = pos % 7; - - if (byteIndex >= data.Length) - throw new IndexOutOfRangeException(); - - return data[byteIndex].IsBitSet(bitIndex); - } - - public static bool IsBitSet(this byte b, int pos) - { - if ((uint)pos > 7) - throw new IndexOutOfRangeException(); - - return ((b >> pos) & 1) != 0; - } +namespace FTMS.NET.Utils; + +internal static class ByteExtensions +{ + public static bool IsBitSet(this byte[] data, int pos) + { + if (pos < 0) + throw new IndexOutOfRangeException(); + + int byteIndex = pos / 8; + int bitIndex = pos % 8; + + if (byteIndex >= data.Length) + throw new IndexOutOfRangeException(); + + return data[byteIndex].IsBitSet(bitIndex); + } + + public static bool IsBitSet(this ReadOnlySpan data, int pos) + { + if (pos < 0) + throw new IndexOutOfRangeException(); + + int byteIndex = pos / 8; + int bitIndex = pos % 8; + + if (byteIndex >= data.Length) + throw new IndexOutOfRangeException(); + + return data[byteIndex].IsBitSet(bitIndex); + } + + public static bool IsBitSet(this byte b, int pos) + { + if ((uint)pos > 7) + throw new IndexOutOfRangeException(); + + return ((b >> pos) & 1) != 0; + } } \ No newline at end of file diff --git a/FTMS.NET/Utils/GenericMath.cs b/FTMS.NET/Utils/GenericMath.cs index de685e9..5e431f4 100644 --- a/FTMS.NET/Utils/GenericMath.cs +++ b/FTMS.NET/Utils/GenericMath.cs @@ -1,28 +1,28 @@ -namespace FTMS.NET.Utils; - -using System.Numerics; - -internal static class GenericMath -{ - public static TValue Clamp(TValue value, TMin min, TMax max) - where TValue : struct, INumber - where TMin : struct, INumber - where TMax : struct, INumber - { - var tMin = TValue.CreateSaturating(min); - if (tMin > value) - return tMin; - - var tMax = TValue.CreateSaturating(max); - if (tMax < value) - return tMax; - - return value; - } - - public static bool IsInRange(TValue value, TMin min, TMax max) - where TValue : struct, INumber - where TMin : struct, INumber - where TMax : struct, INumber - => value >= TValue.CreateSaturating(min) && value <= TValue.CreateSaturating(max); -} +namespace FTMS.NET.Utils; + +using System.Numerics; + +internal static class GenericMath +{ + public static TValue Clamp(TValue value, TMin min, TMax max) + where TValue : struct, INumber + where TMin : struct, INumber + where TMax : struct, INumber + { + var tMin = TValue.CreateSaturating(min); + if (tMin > value) + return tMin; + + var tMax = TValue.CreateSaturating(max); + if (tMax < value) + return tMax; + + return value; + } + + public static bool IsInRange(TValue value, TMin min, TMax max) + where TValue : struct, INumber + where TMin : struct, INumber + where TMax : struct, INumber + => value >= TValue.CreateSaturating(min) && value <= TValue.CreateSaturating(max); +} \ No newline at end of file diff --git a/FTMS.NET/Utils/UInt24.cs b/FTMS.NET/Utils/UInt24.cs index 1463d12..3b5f7bb 100644 --- a/FTMS.NET/Utils/UInt24.cs +++ b/FTMS.NET/Utils/UInt24.cs @@ -1,80 +1,82 @@ -namespace FTMS.NET.Utils; - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -/// Based on https://stackoverflow.com/a/12549260 -[StructLayout(LayoutKind.Sequential)] -public readonly struct UInt24 : IEquatable, IComparable/*, INumber*/ -{ - public static readonly UInt24 MaxValue = new(0xFF, 0xFF, 0xFF); - public static readonly UInt24 MinValue = new(0x00, 0x00, 0x00); - - private const uint _maxValue = 0x00FFFFFF; // 16,777,215 // This is the *inclusive* upper-bound (i.e. max-value). - - // - - public static implicit operator uint(UInt24 self) => self.Value; - public static implicit operator UInt24(ushort u16) => new(value: u16); - - // - - private readonly byte b0; - private readonly byte b1; - private readonly byte b2; - - public UInt24(byte b0, byte b1, byte b2) - { - this.b0 = b0; - this.b1 = b1; - this.b2 = b2; - } - - public UInt24(uint value) - { - if (value > _maxValue) - throw new ArgumentOutOfRangeException( - paramName: nameof(value), - actualValue: value, - message: $"Value {value:N0} must be between 0 and {_maxValue:N0} (inclusive)."); - - // - - this.b0 = (byte)(value & 0xFF); - this.b1 = (byte)(value >> 8 & 0xFF); - this.b2 = (byte)(value >> 16 & 0xFF); - } - -#if UNSAFE - public unsafe Byte* Byte0 => &_b0; -#endif - - private int SignedValue => this.b0 | this.b1 << 8 | this.b2 << 16; - public uint Value => (uint)this.SignedValue; - - // - - #region Struct Tedium + IEquatable + IComparable - - public override string ToString() => this.Value.ToString(); - public override int GetHashCode() => this.SignedValue; - public override bool Equals([NotNullWhen(true)] object? obj) => obj is UInt24 other && this.Equals(other: other); - - public bool Equals(UInt24 other) => this.Value.Equals(other.Value); - public int CompareTo(UInt24 other) => this.Value.CompareTo(other.Value); - - public static int Compare(UInt24 left, UInt24 right) => left.Value.CompareTo(right.Value); - - // - - public static bool operator ==(UInt24 left, UInt24 right) => left.Equals(right); - public static bool operator !=(UInt24 left, UInt24 right) => !left.Equals(right); - - public static bool operator >(UInt24 left, UInt24 right) => Compare(left, right) > 0; - public static bool operator >=(UInt24 left, UInt24 right) => Compare(left, right) >= 0; - - public static bool operator <(UInt24 left, UInt24 right) => Compare(left, right) < 0; - public static bool operator <=(UInt24 left, UInt24 right) => Compare(left, right) <= 0; - - #endregion +namespace FTMS.NET.Utils; + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +/// Based on https://stackoverflow.com/a/12549260 +[StructLayout(LayoutKind.Sequential)] +public readonly struct UInt24 : IEquatable, IComparable/*, INumber*/ +{ + public static readonly UInt24 MaxValue = new(0xFF, 0xFF, 0xFF); + public static readonly UInt24 MinValue = new(0x00, 0x00, 0x00); + + private const uint _maxValue = 0x00FFFFFF; // 16,777,215 // This is the *inclusive* upper-bound (i.e. max-value). + + // + + public static implicit operator uint(UInt24 self) => self.Value; + public static implicit operator UInt24(ushort u16) => new(value: u16); + + // + + private readonly byte b0; + private readonly byte b1; + private readonly byte b2; + + public UInt24(byte b0, byte b1, byte b2) + { + this.b0 = b0; + this.b1 = b1; + this.b2 = b2; + } + + public UInt24(uint value) + { + if (value > _maxValue) + throw new ArgumentOutOfRangeException( + paramName: nameof(value), + actualValue: value, + message: $"Value {value:N0} must be between 0 and {_maxValue:N0} (inclusive)."); + + // + + this.b0 = (byte)(value & 0xFF); + this.b1 = (byte)(value >> 8 & 0xFF); + this.b2 = (byte)(value >> 16 & 0xFF); + } + +#if UNSAFE + public unsafe Byte* Byte0 => &_b0; +#endif + + private int SignedValue => this.b0 | this.b1 << 8 | this.b2 << 16; + public uint Value => (uint)this.SignedValue; + + public byte[] GetBytes() => [this.b0, this.b1, this.b2]; + + // + + #region Struct Tedium + IEquatable + IComparable + + public override string ToString() => this.Value.ToString(); + public override int GetHashCode() => this.SignedValue; + public override bool Equals([NotNullWhen(true)] object? obj) => obj is UInt24 other && this.Equals(other: other); + + public bool Equals(UInt24 other) => this.Value.Equals(other.Value); + public int CompareTo(UInt24 other) => this.Value.CompareTo(other.Value); + + public static int Compare(UInt24 left, UInt24 right) => left.Value.CompareTo(right.Value); + + // + + public static bool operator ==(UInt24 left, UInt24 right) => left.Equals(right); + public static bool operator !=(UInt24 left, UInt24 right) => !left.Equals(right); + + public static bool operator >(UInt24 left, UInt24 right) => Compare(left, right) > 0; + public static bool operator >=(UInt24 left, UInt24 right) => Compare(left, right) >= 0; + + public static bool operator <(UInt24 left, UInt24 right) => Compare(left, right) < 0; + public static bool operator <=(UInt24 left, UInt24 right) => Compare(left, right) <= 0; + + #endregion } \ No newline at end of file diff --git a/FTMS.NET/Utils/ValueCalculation.cs b/FTMS.NET/Utils/ValueCalculation.cs index bf54ee4..550df57 100644 --- a/FTMS.NET/Utils/ValueCalculation.cs +++ b/FTMS.NET/Utils/ValueCalculation.cs @@ -1,9 +1,10 @@ -namespace FTMS.NET.Utils; -using System; - -internal sealed record ValueCalculation(int Multiplier = 1, int DecimalExponent = 0, int BinaryExponent = 0) -{ - private readonly double constantMultiplier = Multiplier * Math.Pow(10, DecimalExponent) * Math.Pow(2, BinaryExponent); - - public double Calculate(long rawValue) => rawValue * this.constantMultiplier; -} +namespace FTMS.NET.Utils; + +using System; + +internal sealed record ValueCalculation(int Multiplier = 1, int DecimalExponent = 0, int BinaryExponent = 0) +{ + private readonly double constantMultiplier = Multiplier * Math.Pow(10, DecimalExponent) * Math.Pow(2, BinaryExponent); + + public double Calculate(long rawValue) => rawValue * this.constantMultiplier; +} \ No newline at end of file