Skip to content

Commit f8d124f

Browse files
authored
Pressure to altitude conversion fixed (#2483)
1 parent 3982d38 commit f8d124f

14 files changed

Lines changed: 108 additions & 62 deletions

File tree

src/Iot.Device.Bindings/CompatibilitySuppressions.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
33
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
4+
<Suppression>
5+
<DiagnosticId>CP0002</DiagnosticId>
6+
<Target>F:Iot.Device.Common.WeatherHelper.MeanSeaLevel</Target>
7+
<Left>lib/net8.0/Iot.Device.Bindings.dll</Left>
8+
<Right>lib/net8.0/Iot.Device.Bindings.dll</Right>
9+
<IsBaselineSuppression>true</IsBaselineSuppression>
10+
</Suppression>
411
<Suppression>
512
<DiagnosticId>CP0002</DiagnosticId>
613
<Target>F:Iot.Device.Tca955x.Tca955x.DefaultI2cAdress</Target>
@@ -15,6 +22,13 @@
1522
<Right>lib/net8.0/Iot.Device.Bindings.dll</Right>
1623
<IsBaselineSuppression>true</IsBaselineSuppression>
1724
</Suppression>
25+
<Suppression>
26+
<DiagnosticId>CP0002</DiagnosticId>
27+
<Target>M:Iot.Device.Common.WeatherHelper.CalculateAltitude(UnitsNet.Pressure,UnitsNet.Temperature)</Target>
28+
<Left>lib/net8.0/Iot.Device.Bindings.dll</Left>
29+
<Right>lib/net8.0/Iot.Device.Bindings.dll</Right>
30+
<IsBaselineSuppression>true</IsBaselineSuppression>
31+
</Suppression>
1832
<Suppression>
1933
<DiagnosticId>CP0002</DiagnosticId>
2034
<Target>M:Iot.Device.Nmea0183.Ais.TrackEstimationParameters.get_WarningDistance</Target>

src/devices/Bmp180/Bmp180.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public Pressure ReadPressure()
9898
/// <returns>
9999
/// Height in meters above sea level
100100
/// </returns>
101-
public Length ReadAltitude() => ReadAltitude(WeatherHelper.MeanSeaLevel);
101+
public Length ReadAltitude() => ReadAltitude(WeatherHelper.MeanSeaLevelPressure);
102102

103103
/// <summary>
104104
/// Calculates the pressure at sea level when given a known altitude

src/devices/Bmp180/samples/Program.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
Console.WriteLine($"Pressure: {preValue.Hectopascals:0.##}hPa");
2828

2929
// Note that if you already have the pressure value and the temperature, you could also calculate altitude by
30-
// calling WeatherHelper.CalculateAltitude(preValue, Pressure.MeanSeaLevel, tempValue) which would be more performant.
31-
Length altValue = i2cBmp280.ReadAltitude(WeatherHelper.MeanSeaLevel);
30+
// calling WeatherHelper.CalculateAltitude(preValue, WeatherHelper.MeanSeaLevelPressure, tempValue) which would be more performant.
31+
Length altValue = i2cBmp280.ReadAltitude(WeatherHelper.MeanSeaLevelPressure);
3232

3333
Console.WriteLine($"Altitude: {altValue:0.##}m");
3434
Thread.Sleep(1000);
@@ -43,6 +43,6 @@
4343
Console.WriteLine($"Pressure: {preValue.Hectopascals:0.##}hPa");
4444

4545
// Note that if you already have the pressure value and the temperature, you could also calculate altitude by
46-
// calling WeatherHelper.CalculateAltitude(preValue, Pressure.MeanSeaLevel, tempValue) which would be more performant.
47-
altValue = i2cBmp280.ReadAltitude(WeatherHelper.MeanSeaLevel);
46+
// calling WeatherHelper.CalculateAltitude(preValue, WeatherHelper.MeanSeaLevelPressure, tempValue) which would be more performant.
47+
altValue = i2cBmp280.ReadAltitude(WeatherHelper.MeanSeaLevelPressure);
4848
Console.WriteLine($"Altitude: {altValue:0.##}m");

src/devices/Bmxx80/Bmx280Base.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public bool TryReadAltitude(Pressure seaLevelPressure, out Length altitude)
176176
/// Contains <see cref="double.NaN"/> otherwise.
177177
/// </param>
178178
/// <returns><code>true</code> if pressure measurement was not skipped, otherwise <code>false</code>.</returns>
179-
public bool TryReadAltitude(out Length altitude) => TryReadAltitude(WeatherHelper.MeanSeaLevel, out altitude);
179+
public bool TryReadAltitude(out Length altitude) => TryReadAltitude(WeatherHelper.MeanSeaLevelPressure, out altitude);
180180

181181
/// <summary>
182182
/// Get the current status of the device.

src/devices/Bmxx80/samples/Bme280/Bme280.sample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// bus id on the raspberry pi 3
1616
const int busId = 1;
1717
// set this to the current sea level pressure in the area for correct altitude readings
18-
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevel;
18+
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevelPressure;
1919

2020
I2cConnectionSettings i2cSettings = new(busId, Bme280.DefaultI2cAddress);
2121
using I2cDevice i2cDevice = I2cDevice.Create(i2cSettings);

src/devices/Bmxx80/samples/Bme680/Bme680.sample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// The I2C bus ID on the Raspberry Pi 3.
1414
const int busId = 1;
1515
// set this to the current sea level pressure in the area for correct altitude readings
16-
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevel;
16+
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevelPressure;
1717

1818
I2cConnectionSettings i2cSettings = new(busId, Bme680.DefaultI2cAddress);
1919
I2cDevice i2cDevice = I2cDevice.Create(i2cSettings);

src/devices/Bmxx80/samples/Bmp280/Bmp280.sample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// bus id on the raspberry pi 3 and 4
1717
const int busId = 1;
1818
// set this to the current sea level pressure in the area for correct altitude readings
19-
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevel;
19+
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevelPressure;
2020

2121
I2cConnectionSettings i2cSettings = new(busId, Bmp280.DefaultI2cAddress);
2222
I2cDevice i2cDevice = I2cDevice.Create(i2cSettings);

src/devices/Common/Iot/Device/Common/WeatherHelper.cs

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,30 @@ public static class WeatherHelper
1515
/// <summary>
1616
/// Gas constant of dry Air, J / (kg * K)
1717
/// </summary>
18-
internal const double SpecificGasConstantOfAir = 287.058;
18+
public const double SpecificGasConstantOfAir = 287.058;
1919

2020
/// <summary>
2121
/// Gas constant of vapor, J / (kg * K)
2222
/// </summary>
23-
internal const double SpecificGasConstantOfVapor = 461.523;
23+
public const double SpecificGasConstantOfVapor = 461.523;
2424

2525
/// <summary>
2626
/// Default atmospheric temperature gradient = 0.0065K/m (or 0.65K per 100m)
2727
/// </summary>
28-
internal const double DefaultTemperatureGradient = 0.0065;
28+
public const double DefaultTemperatureGradient = 0.0065;
29+
30+
/// <summary>
31+
/// The default assumed temperature at sea level.
32+
/// This is used for <see cref="CalculateAltitude(Pressure, Pressure)"/> since it is not possible to calculate the temperature at sea level
33+
/// for this formula, as we do not know the altitude (it is the result of that computation, after all). The QNH value does consider the
34+
/// temperature, though.
35+
/// </summary>
36+
public static Temperature DefaultSeaLevelTemperature => Temperature.FromDegreesCelsius(15);
2937

3038
/// <summary>
3139
/// The mean sea-level pressure (MSLP) is the average atmospheric pressure at mean sea level
3240
/// </summary>
33-
public static readonly Pressure MeanSeaLevel = Pressure.FromPascals(101325);
41+
public static Pressure MeanSeaLevelPressure => Pressure.FromPascals(101325);
3442

3543
#region TemperatureAndRelativeHumidity
3644

@@ -191,47 +199,46 @@ public static RelativeHumidity GetRelativeHumidityFromActualAirTemperature(Tempe
191199
#endregion TemperatureAndRelativeHumidity
192200

193201
#region Pressure
194-
// Formula from https://de.wikipedia.org/wiki/Barometrische_Höhenformel#Internationale_Höhenformel, solved
195-
// for different parameters
196202

197203
/// <summary>
198-
/// Calculates the altitude in meters from the given pressure, sea-level pressure and air temperature.
204+
/// Calculates the altitude in meters from the given pressure and sea-level pressure.
199205
/// </summary>
200206
/// <param name="pressure">The pressure at the point for which altitude is being calculated</param>
201-
/// <param name="seaLevelPressure">The sea-level pressure</param>
202-
/// <param name="airTemperature">The dry air temperature at the point for which altitude is being calculated</param>
203-
/// <returns>The altitude</returns>
204-
public static Length CalculateAltitude(Pressure pressure, Pressure seaLevelPressure, Temperature airTemperature)
207+
/// <param name="seaLevelPressure">The sea-level pressure. In aviation, this is called the QNH value and provided by weather reports or ATC</param>
208+
/// <remarks>
209+
/// Formula from https://de.wikipedia.org/wiki/Barometrische_Höhenformel#Internationale_Höhenformel, solved
210+
/// </remarks>
211+
/// <returns>The altitude over mean sea level</returns>
212+
public static Length CalculateAltitude(Pressure pressure, Pressure seaLevelPressure)
205213
{
206-
double meters = ((Math.Pow(seaLevelPressure.Pascals / pressure.Pascals, 1 / 5.255) - 1) * airTemperature.Kelvins) / DefaultTemperatureGradient;
214+
double meters = (Math.Pow(seaLevelPressure.Pascals / pressure.Pascals, -1.0 / 5.255) - 1) * (DefaultSeaLevelTemperature.Kelvins / -DefaultTemperatureGradient);
207215
return Length.FromMeters(meters);
208216
}
209217

210218
/// <summary>
211-
/// Calculates the altitude in meters from the given pressure and air temperature. Assumes mean sea-level pressure.
212-
/// </summary>
213-
/// <param name="pressure">The pressure at the point for which altitude is being calculated</param>
214-
/// <param name="airTemperature">The dry air temperature at the point for which altitude is being calculated</param>
215-
/// <returns>The altitude</returns>
216-
public static Length CalculateAltitude(Pressure pressure, Temperature airTemperature) =>
217-
CalculateAltitude(pressure, MeanSeaLevel, airTemperature);
218-
219-
/// <summary>
220-
/// Calculates the altitude in meters from the given pressure and sea-level pressure. Assumes temperature of 15C.
219+
/// Calculates the altitude in meters from the given pressure, sea-level pressure and air temperature.
221220
/// </summary>
222221
/// <param name="pressure">The pressure at the point for which altitude is being calculated</param>
223-
/// <param name="seaLevelPressure">The sea-level pressure</param>
224-
/// <returns>The altitude</returns>
225-
public static Length CalculateAltitude(Pressure pressure, Pressure seaLevelPressure) =>
226-
CalculateAltitude(pressure, seaLevelPressure, Temperature.FromDegreesCelsius(15));
222+
/// <param name="seaLevelPressure">The sea-level pressure. In aviation, this is called the QNH value and provided by weather reports or ATC</param>
223+
/// <param name="temperatureAtObservation">Temperature at observation point. Since this depends on the altitude (which we don't know), we use an iterative approach here</param>
224+
/// <returns>The altitude over mean sea level</returns>
225+
public static Length CalculateAltitude(Pressure pressure, Pressure seaLevelPressure, Temperature temperatureAtObservation)
226+
{
227+
// The first iteration is to get an approximation of the current height, so we can do the temperature compensation
228+
double meters = (Math.Pow(seaLevelPressure.Pascals / pressure.Pascals, -1.0 / 5.255) - 1) * (DefaultSeaLevelTemperature.Kelvins / -DefaultTemperatureGradient);
229+
// It gets hotter when going down (when in the Troposphere)
230+
Temperature temperatureAtSeaLevel = temperatureAtObservation + TemperatureDelta.FromDegreesCelsius(DefaultTemperatureGradient * meters);
231+
double meters2 = (Math.Pow(seaLevelPressure.Pascals / pressure.Pascals, -1.0 / 5.255) - 1) * (temperatureAtSeaLevel.Kelvins / -DefaultTemperatureGradient);
232+
return Length.FromMeters(meters2);
233+
}
227234

228235
/// <summary>
229-
/// Calculates the altitude in meters from the given pressure. Assumes mean sea-level pressure and temperature of 15C.
236+
/// Calculates the altitude in meters from the given pressure. Assumes mean sea-level pressure.
230237
/// </summary>
231238
/// <param name="pressure">The pressure at the point for which altitude is being calculated</param>
232239
/// <returns>The altitude</returns>
233240
public static Length CalculateAltitude(Pressure pressure) =>
234-
CalculateAltitude(pressure, MeanSeaLevel, Temperature.FromDegreesCelsius(15));
241+
CalculateAltitude(pressure, MeanSeaLevelPressure);
235242

236243
/// <summary>
237244
/// Calculates the approximate sea-level pressure from given absolute pressure, altitude and air temperature.

src/devices/Common/tests/WeatherTests.cs

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,33 +81,29 @@ public void AbsoluteHumidityIsCalculatedCorrectly(double expected, double fahren
8181
}
8282

8383
[Theory]
84-
[InlineData(1011.22, 900)]
85-
[InlineData(111.18, 1000)]
86-
[InlineData(547.1, 950)]
84+
[InlineData(988.50, 900)]
85+
[InlineData(110.88, 1000)]
8786
public void AltitudeIsCalculatedCorrectlyAtMslpAndDefaultTemp(double expected, double hpa)
8887
{
8988
Length altitude = WeatherHelper.CalculateAltitude(Pressure.FromHectopascals(hpa));
90-
Assert.Equal(expected, Math.Round(altitude.Meters, 2));
89+
Assert.Equal(altitude.Meters, expected, 0.5);
9190
}
9291

9392
[Theory]
94-
[InlineData(1011.22, 900, 1013.25)]
95-
[InlineData(111.18, 1000, 1013.25)]
96-
[InlineData(547.1, 950, 1013.25)]
97-
public void AltitudeIsCalculatedCorrectlyAtDefaultTemp(double expected, double hpa, double seaLevelHpa)
93+
[InlineData(0, 1013.25, 1013.25, 15)]
94+
[InlineData(1000.13, 898.75, 1013.25, 15)]
95+
[InlineData(3000.43, 701.09, 1013.25, 15)]
96+
[InlineData(540.43, 950, 1013.25, 15)]
97+
public void AltitudeIsCalculatedCorrectly(double expected, double hpa, double seaLevelHpa, double celsius)
9898
{
9999
Length altitude = WeatherHelper.CalculateAltitude(Pressure.FromHectopascals(hpa), Pressure.FromHectopascals(seaLevelHpa));
100-
Assert.Equal(expected, Math.Round(altitude.Meters, 2));
101-
}
102100

103-
[Theory]
104-
[InlineData(1011.22, 900, 1013.25, 15)]
105-
[InlineData(111.18, 1000, 1013.25, 15)]
106-
[InlineData(547.1, 950, 1013.25, 15)]
107-
public void AltitudeIsCalculatedCorrectly(double expected, double hpa, double seaLevelHpa, double celsius)
108-
{
109-
Length altitude = WeatherHelper.CalculateAltitude(Pressure.FromHectopascals(hpa), Pressure.FromHectopascals(seaLevelHpa), Temperature.FromDegreesCelsius(celsius));
101+
// These two formulas should be equivalent.
102+
double meters1 = (Math.Pow(seaLevelHpa / hpa, -1 / 5.255) - 1) * ((celsius + 273.15) / -WeatherHelper.DefaultTemperatureGradient);
103+
double meters2 = ((celsius + 273.15) / WeatherHelper.DefaultTemperatureGradient) * (1 - Math.Pow(hpa / seaLevelHpa, 1.0 / 5.255));
110104
Assert.Equal(expected, Math.Round(altitude.Meters, 2));
105+
Assert.Equal(meters1, expected, 0.01);
106+
Assert.Equal(meters2, expected, 0.01);
111107
}
112108

113109
[Theory]
@@ -159,6 +155,17 @@ public void CalculateBarometricPressure(double measuredValue, double temperature
159155
Assert.Equal(expected, result.Hectopascals, 2);
160156
}
161157

158+
[Theory]
159+
[InlineData(1113.25, -801.147)]
160+
[InlineData(1013.25, 0.0)]
161+
[InlineData(913.25, 867.955)]
162+
[InlineData(813.25, 1816.617)]
163+
public void Pressure2Msl(double pressureHPa, double expectedHeightMsl)
164+
{
165+
var heightMsl = WeatherHelper.CalculateAltitude(Pressure.FromHectopascals(pressureHPa));
166+
Assert.Equal(heightMsl.Meters, expectedHeightMsl, 0.1);
167+
}
168+
162169
[Theory]
163170
[InlineData(950, 15, 546.89, 10, 1013.19)]
164171
[InlineData(950, 15, 546.89, 50, 1013.01)]
@@ -235,5 +242,23 @@ public void CalculateWindForce(double temperature, double windSpeed, double pres
235242
var density = WeatherHelper.CalculateWindForce(airDensity, Speed.FromKilometersPerHour(windSpeed), 1.0);
236243
Assert.Equal(expected, density.NewtonsPerSquareMeter, 1);
237244
}
245+
246+
[Theory]
247+
[InlineData(0, 1013.25, 15, 0)]
248+
[InlineData(0, 1020, 15, 0)]
249+
[InlineData(0, 1020, -20, 0)]
250+
[InlineData(400, 970, 15, 397.989)]
251+
[InlineData(400, 970, -5, 399.047)]
252+
[InlineData(2000, 940, -5, 1995.953)]
253+
public void RoundTripAltitudeAndPressure(double originalAltitude, double originalPressure, double originalTemperature, double expectedResult)
254+
{
255+
// The expectedResult and the originalAltitude should ideally be equal.
256+
Pressure qnh = WeatherHelper.CalculateBarometricPressure(Pressure.FromHectopascals(originalPressure),
257+
Temperature.FromDegreesCelsius(originalTemperature), Length.FromMeters(originalAltitude));
258+
259+
Length altitudeResult = WeatherHelper.CalculateAltitude(Pressure.FromHectopascals(originalPressure), qnh, Temperature.FromDegreesCelsius(originalTemperature));
260+
261+
Assert.Equal(expectedResult, altitudeResult.Meters, 1E-3);
262+
}
238263
}
239264
}

src/devices/Ft232H/samples/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ void I2cScan(Ftx232HDevice ft232h)
115115
void TestI2c(Ftx232HDevice ft232h)
116116
{
117117
// set this to the current sea level pressure in the area for correct altitude readings
118-
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevel;
118+
Pressure defaultSeaLevelPressure = WeatherHelper.MeanSeaLevelPressure;
119119
Length stationHeight = Length.FromMeters(640); // Elevation of the sensor
120120
var ftI2cBus = ft232h.CreateOrGetI2cBus(ft232h.GetDefaultI2cBusNumber());
121121
var i2cDevice = ftI2cBus.CreateDevice(Bmp280.DefaultI2cAddress);

0 commit comments

Comments
 (0)