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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions src/Daqifi.Core.Tests/Channel/AnalogChannelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,196 @@ public void Properties_CanBeModified()
Assert.Equal(5.0, channel.PortRange);
}

// ---------------------------------------------------------------------
// Bounds validation (daqifi-core#300)
// ---------------------------------------------------------------------

[Theory]
[InlineData(254u)] // just below the 8-bit max-count floor
[InlineData(16_777_217u)] // just above the 24-bit max-count ceiling
public void Constructor_WithOutOfRangeResolution_ThrowsException(uint resolution)
{
Assert.Throws<ArgumentOutOfRangeException>(() => new AnalogChannel(channelNumber: 0, resolution: resolution));
}

[Theory]
[InlineData(255u)] // 8-bit max-count floor
[InlineData(16_777_216u)] // 24-bit ceiling
public void Constructor_WithBoundaryResolution_IsAccepted(uint resolution)
{
var channel = new AnalogChannel(channelNumber: 0, resolution: resolution);
Assert.Equal(resolution, channel.Resolution);
}

[Theory]
[InlineData(0.0)] // zero range
[InlineData(-5.0)] // negative range
[InlineData(AnalogChannel.MaxPortRangeVolts + 0.1)] // beyond max
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
public void PortRange_WithInvalidValue_ThrowsException(double value)
{
var channel = new AnalogChannel(0);
Assert.Throws<ArgumentOutOfRangeException>(() => channel.PortRange = value);
}

[Theory]
[InlineData(0.0)] // zero scale factor discards the measurement
[InlineData(double.NaN)]
[InlineData(double.NegativeInfinity)]
[InlineData(AnalogChannel.MaxCalibrationMagnitude * 2)]
public void CalibrationM_WithInvalidValue_ThrowsException(double value)
{
var channel = new AnalogChannel(0);
Assert.Throws<ArgumentOutOfRangeException>(() => channel.CalibrationM = value);
}

[Fact]
public void CalibrationM_WithNegativeValue_IsAccepted()
{
// A negative slope legitimately inverts the signal (e.g. reversed wiring).
var channel = new AnalogChannel(0) { CalibrationM = -2.5 };
Assert.Equal(-2.5, channel.CalibrationM);
}

[Theory]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
[InlineData(AnalogChannel.MaxCalibrationMagnitude * 2)]
public void CalibrationB_WithInvalidValue_ThrowsException(double value)
{
var channel = new AnalogChannel(0);
Assert.Throws<ArgumentOutOfRangeException>(() => channel.CalibrationB = value);
}

[Fact]
public void CalibrationB_WithZero_IsAccepted()
{
// Zero is a valid offset (it's the default).
var channel = new AnalogChannel(0) { CalibrationB = 0.0 };
Assert.Equal(0.0, channel.CalibrationB);
}

[Theory]
[InlineData(0.0)]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
public void InternalScaleM_WithInvalidValue_ThrowsException(double value)
{
var channel = new AnalogChannel(0);
Assert.Throws<ArgumentOutOfRangeException>(() => channel.InternalScaleM = value);
}

[Theory]
[InlineData(double.NaN)]
[InlineData(double.NegativeInfinity)]
public void MinValue_WithNonFinite_ThrowsException(double value)
{
var channel = new AnalogChannel(0);
Assert.Throws<ArgumentOutOfRangeException>(() => channel.MinValue = value);
}

[Theory]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
public void MaxValue_WithNonFinite_ThrowsException(double value)
{
var channel = new AnalogChannel(0);
Assert.Throws<ArgumentOutOfRangeException>(() => channel.MaxValue = value);
}

[Fact]
public void PortRange_AtMaxBoundary_IsAccepted()
{
var channel = new AnalogChannel(0) { PortRange = AnalogChannel.MaxPortRangeVolts };
Assert.Equal(AnalogChannel.MaxPortRangeVolts, channel.PortRange);
}

// ---------------------------------------------------------------------
// Bipolar / signed scaling (daqifi-core#297)
// ---------------------------------------------------------------------

[Fact]
public void GetScaledValue_WithNegativeRawValue_ProducesNegativeVoltage()
{
// ±10V bipolar range: signed two's-complement raw counts should map straight through
// to signed voltages with no unipolar-only assumption in the formula.
var channel = new AnalogChannel(0, 262143)
{
PortRange = 10.0,
CalibrationM = 1.0,
CalibrationB = 0.0,
InternalScaleM = 1.0,
MinValue = -10.0,
MaxValue = 10.0
};

// -full scale -> -PortRange
Assert.Equal(-10.0, channel.GetScaledValue(-262143), precision: 6);
// -half scale -> -PortRange/2
Assert.Equal(-5.0, channel.GetScaledValue(-131072), precision: 2);
// zero raw -> 0 V (no offset)
Assert.Equal(0.0, channel.GetScaledValue(0), precision: 6);
}

[Fact]
public void GetScaledValue_IsSymmetricAboutZeroForBipolarRange()
{
var channel = new AnalogChannel(0, 65535)
{
PortRange = 5.0,
CalibrationM = 1.0,
CalibrationB = 0.0,
InternalScaleM = 1.0
};

var positive = channel.GetScaledValue(20000);
var negative = channel.GetScaledValue(-20000);

Assert.Equal(-positive, negative, precision: 9);
}

[Fact]
public void GetScaledValue_WithNegativeRawAndOffset_AppliesOffsetAfterSignedGain()
{
// Formula: (raw/Res * PortRange * M + B) * InternalScaleM.
// At -full scale with M=1, B=1: (-1 * 10 * 1 + 1) = -9.
var channel = new AnalogChannel(0, 262143)
{
PortRange = 10.0,
CalibrationM = 1.0,
CalibrationB = 1.0,
InternalScaleM = 1.0
};

Assert.Equal(-9.0, channel.GetScaledValue(-262143), precision: 6);
}

[Fact]
public void GetScaledValue_WithNegativeCalibrationM_InvertsSign()
{
var channel = new AnalogChannel(0, 65535)
{
PortRange = 10.0,
CalibrationM = -1.0,
CalibrationB = 0.0,
InternalScaleM = 1.0
};

// A negative raw with an inverting slope yields a positive voltage.
Assert.Equal(10.0, channel.GetScaledValue(-65535), precision: 6);
}

[Fact]
public void IsBipolar_ReflectsConfiguredMinValue()
{
var bipolar = new AnalogChannel(0) { MinValue = -10.0, MaxValue = 10.0 };
Assert.True(bipolar.IsBipolar);

var unipolar = new AnalogChannel(0) { MinValue = 0.0, MaxValue = 10.0 };
Assert.False(unipolar.IsBipolar);
}

[Fact]
public void ToString_ReturnsChannelName()
{
Expand Down
75 changes: 75 additions & 0 deletions src/Daqifi.Core.Tests/Device/ChannelPopulationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,81 @@ public void PopulateChannelsFromStatus_AnalogChannelsHaveCorrectCalibrationParam
Assert.Equal(5.0, analogChannels[1].PortRange, 3);
}

[Fact]
public void PopulateChannelsFromStatus_WithCorruptScalingValues_SubstitutesSafeDefaultsWithoutThrowing()
{
// A corrupted device response can carry NaN/Infinity or nonsensical coefficients. Population
// must not throw (which would abort channel population mid-stream) and must fall back to safe
// defaults rather than propagating garbage into every scaled sample (daqifi-core#300).
var device = new DaqifiDevice("TestDevice");
var message = new DaqifiOutMessage
{
AnalogInPortNum = 2,
AnalogInRes = 65535
};
message.AnalogInCalM.Add(float.NaN); // ch0: invalid -> default 1.0
message.AnalogInCalM.Add(2.0f); // ch1: valid
message.AnalogInCalB.Add(float.PositiveInfinity); // ch0: invalid -> default 0.0
message.AnalogInCalB.Add(0.2f); // ch1: valid
message.AnalogInIntScaleM.Add(0.0f); // ch0: zero scale -> default 1.0
message.AnalogInIntScaleM.Add(1.2f); // ch1: valid
message.AnalogInPortRange.Add(-10.0f); // ch0: negative range -> default 1.0
message.AnalogInPortRange.Add(5.0f); // ch1: valid

device.PopulateChannelsFromStatus(message);

var analogChannels = device.Channels
.Where(c => c.Type == ChannelType.Analog)
.Cast<IAnalogChannel>()
.ToList();

// ch0 fell back to defaults across the board
Assert.Equal(1.0, analogChannels[0].CalibrationM, 3);
Assert.Equal(0.0, analogChannels[0].CalibrationB, 3);
Assert.Equal(1.0, analogChannels[0].InternalScaleM, 3);
Assert.Equal(1.0, analogChannels[0].PortRange, 3);

// ch1's valid values were preserved
Assert.Equal(2.0, analogChannels[1].CalibrationM, 3);
Assert.Equal(0.2, analogChannels[1].CalibrationB, 3);
Assert.Equal(1.2, analogChannels[1].InternalScaleM, 3);
Assert.Equal(5.0, analogChannels[1].PortRange, 3);
}

[Theory]
[InlineData(0u)] // missing resolution
[InlineData(1u)] // non-zero but below MinResolution
[InlineData(uint.MaxValue)] // above MaxResolution
public void PopulateChannelsFromStatus_WithUnusableResolution_FallsBackWithoutThrowing(uint reportedResolution)
{
// A non-zero but out-of-range AnalogInRes must not reach the AnalogChannel constructor
// (which now rejects it) and abort channel population — it should fall back to the assumed
// default and flag ResolutionIsAssumed, on both the new-channel and reuse paths.
var device = new DaqifiDevice("TestDevice");
var message = new DaqifiOutMessage
{
AnalogInPortNum = 2,
AnalogInRes = reportedResolution
};

// First population creates the channels; second re-populates (exercises the reuse path via
// UpdateScalingFromStatus) — neither should throw.
device.PopulateChannelsFromStatus(message);
device.PopulateChannelsFromStatus(message);

var analogChannels = device.Channels
.Where(c => c.Type == ChannelType.Analog)
.Cast<IAnalogChannel>()
.ToList();

Assert.Equal(2, analogChannels.Count);
foreach (var ch in analogChannels)
{
Assert.Equal(65535u, ch.Resolution);
Assert.True(ch.ResolutionIsAssumed);
}
}

[Fact]
public void PopulateChannelsFromStatus_AnalogChannelsHaveCorrectResolution()
{
Expand Down
Loading