From 6611042925d1c69b8eb6a736ef13075debfc9975 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Tue, 31 Dec 2024 10:17:37 +0100 Subject: [PATCH 01/10] Solution layout fixed --- src/devices/Bmm150/Bmm150.sln | 47 +++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/devices/Bmm150/Bmm150.sln b/src/devices/Bmm150/Bmm150.sln index 50b162f6f3..f72aa433e4 100644 --- a/src/devices/Bmm150/Bmm150.sln +++ b/src/devices/Bmm150/Bmm150.sln @@ -1,13 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35017.193 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{A8C3EC1C-FDA1-48C1-B18A-D1007F260D2A}") = "samples", "samples", "{034CA6D3-0F53-455B-B62A-05469793013A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bmm150", "Bmm150.csproj", "{00D33DB1-453A-4C28-9C58-D7BD9803C25B}" EndProject -Project("{D584ECB5-3871-441B-B5FB-5A2637B39161}") = "Bmm150.Sample", "samples\Bmm150.Sample.csproj", "{47740F69-06C5-46C6-A335-D9A50B13F42F}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{A82B9026-3C3E-4FDD-9AF9-BE012C7A3CB7}" EndProject -Project("{D584ECB5-3871-441B-B5FB-5A2637B39161}") = "Bmm150", "Bmm150.csproj", "{00D33DB1-453A-4C28-9C58-D7BD9803C25B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bmm150.Sample", "samples\Bmm150.Sample.csproj", "{10438E1B-E89D-446C-A276-241A6FD4B9DC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -18,22 +18,7 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Debug|x64.ActiveCfg = Debug|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Debug|x64.Build.0 = Debug|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Debug|x86.ActiveCfg = Debug|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Debug|x86.Build.0 = Debug|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Release|Any CPU.Build.0 = Release|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Release|x64.ActiveCfg = Release|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Release|x64.Build.0 = Release|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Release|x86.ActiveCfg = Release|Any CPU - {47740F69-06C5-46C6-A335-D9A50B13F42F}.Release|x86.Build.0 = Release|Any CPU {00D33DB1-453A-4C28-9C58-D7BD9803C25B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {00D33DB1-453A-4C28-9C58-D7BD9803C25B}.Debug|Any CPU.Build.0 = Debug|Any CPU {00D33DB1-453A-4C28-9C58-D7BD9803C25B}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -46,8 +31,26 @@ Global {00D33DB1-453A-4C28-9C58-D7BD9803C25B}.Release|x64.Build.0 = Release|Any CPU {00D33DB1-453A-4C28-9C58-D7BD9803C25B}.Release|x86.ActiveCfg = Release|Any CPU {00D33DB1-453A-4C28-9C58-D7BD9803C25B}.Release|x86.Build.0 = Release|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Debug|x64.Build.0 = Debug|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Debug|x86.Build.0 = Debug|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Release|Any CPU.Build.0 = Release|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Release|x64.ActiveCfg = Release|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Release|x64.Build.0 = Release|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Release|x86.ActiveCfg = Release|Any CPU + {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {47740F69-06C5-46C6-A335-D9A50B13F42F} = {034CA6D3-0F53-455B-B62A-05469793013A} + {10438E1B-E89D-446C-A276-241A6FD4B9DC} = {A82B9026-3C3E-4FDD-9AF9-BE012C7A3CB7} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F8CEC844-3F4C-445A-9534-C72F6CD76663} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal From 8be0e89724a874a59b4dd4b8c6d2675a211a2528 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Tue, 31 Dec 2024 20:54:11 +0100 Subject: [PATCH 02/10] Fix various incorrect sign usages The reported sensor output was completely wrong --- src/devices/Bmm150/Bmm150.cs | 82 +++++++++++++------ src/devices/Bmm150/Bmm150.sln | 14 ++++ src/devices/Bmm150/Bmm150TrimRegister.cs | 47 ++++++----- src/devices/Bmm150/Register.cs | 5 -- .../Bmm150/samples/Bmm150.Sample.csproj | 1 + src/devices/Bmm150/samples/Program.cs | 32 ++++++-- 6 files changed, 125 insertions(+), 56 deletions(-) diff --git a/src/devices/Bmm150/Bmm150.cs b/src/devices/Bmm150/Bmm150.cs index 5d04b7ee2f..af9c2013ac 100644 --- a/src/devices/Bmm150/Bmm150.cs +++ b/src/devices/Bmm150/Bmm150.cs @@ -133,23 +133,35 @@ private void Initialize() } /// - /// Get the device information + /// Calibrate the magnetometer. + /// Please make sure you are not close to any magnetic field like magnet or phone + /// Please make sure you are moving the magnetometer all over space, rotating it. /// - /// The device information - public byte GetDeviceInfo() => ReadByte(Bmp180Register.INFO); + /// Number of measurement for the calibration, default is 100 + // https://platformio.org/lib/show/12697/M5_BMM150 + [Obsolete("Prefer another overload")] + public void CalibrateMagnetometer(int numberOfMeasurements = 100) + { + CalibrateMagnetometer(null, numberOfMeasurements); + } /// /// Calibrate the magnetometer. /// Please make sure you are not close to any magnetic field like magnet or phone /// Please make sure you are moving the magnetometer all over space, rotating it. /// + /// A progress provider (returns a value in percent) /// Number of measurement for the calibration, default is 100 // https://platformio.org/lib/show/12697/M5_BMM150 - public void CalibrateMagnetometer(int numberOfMeasurements = 100) + public void CalibrateMagnetometer(IProgress? progress, int numberOfMeasurements) { Vector3 mag_min = new Vector3() { X = 9000, Y = 9000, Z = 30000 }; Vector3 mag_max = new Vector3() { X = -9000, Y = -9000, Z = -30000 }; Vector3 rawMagnetometerData; + if (numberOfMeasurements <= 0) + { + throw new ArgumentOutOfRangeException(nameof(numberOfMeasurements), "The number of measurements must be > 0"); + } for (int i = 0; i < numberOfMeasurements; i++) { @@ -182,6 +194,17 @@ public void CalibrateMagnetometer(int numberOfMeasurements = 100) { // skip this reading } + + if (progress != null) + { + double percentDone = ((double)i / numberOfMeasurements) * 100.0; + progress.Report(percentDone); + } + } + + if (progress != null) + { + progress.Report(100.0); } // Refresh CalibrationCompensation vector @@ -239,28 +262,41 @@ public Vector3 ReadMagnetometerWithoutCorrection(bool waitForData, TimeSpan time Vector3 magnetoRaw = new Vector3(); - // Shift the MSB data to left by 5 bits - // Multiply by 32 to get the shift left by 5 value - magnetoRaw.X = (rawData[1] & 0x7F) << 5 | rawData[0] >> 3; - if ((rawData[1] & 0x80) == 0x80) + // Because we mix and match signed and unsigned below + unchecked { - magnetoRaw.X = -magnetoRaw.X; - } + int temp; + // Shift the MSB data to left by 5 bits + // Multiply by 32 to get the shift left by 5 value + // X and Y have 13 significant bits each + temp = (rawData[1]) << 5 | rawData[0] >> 3; + if ((rawData[1] & 0x80) == 0x80) + { + temp = temp | (int)0xFFFFE000; + } - // Shift the MSB data to left by 5 bits - // Multiply by 32 to get the shift left by 5 value - magnetoRaw.Y = (rawData[3] & 0x07F) << 5 | rawData[2] >> 3; - if ((rawData[3] & 0x80) == 0x80) - { - magnetoRaw.Y = -magnetoRaw.Y; - } + magnetoRaw.X = temp; - // Shift the MSB data to left by 7 bits - // Multiply by 128 to get the shift left by 7 value - magnetoRaw.Z = (rawData[5] & 0x07F) << 7 | rawData[4] >> 1; - if ((rawData[5] & 0x80) == 0x80) - { - magnetoRaw.Z = -magnetoRaw.Z; + // Shift the MSB data to left by 5 bits + // Multiply by 32 to get the shift left by 5 value + temp = (rawData[3]) << 5 | rawData[2] >> 3; + if ((rawData[3] & 0x80) == 0x80) + { + temp = temp | (int)0xFFFFE000; + } + + magnetoRaw.Y = temp; + + // Shift the MSB data to left by 7 bits + // Multiply by 128 to get the shift left by 7 value + // The Z value has 15 significant bits + temp = (rawData[5]) << 7 | rawData[4] >> 1; + if ((rawData[5] & 0x80) == 0x80) + { + temp = temp | (int)0xFFFF8000; + } + + magnetoRaw.Z = temp; } _rHall = (uint)(rawData[7] << 6 | rawData[6] >> 2); diff --git a/src/devices/Bmm150/Bmm150.sln b/src/devices/Bmm150/Bmm150.sln index f72aa433e4..84d9ecebdd 100644 --- a/src/devices/Bmm150/Bmm150.sln +++ b/src/devices/Bmm150/Bmm150.sln @@ -9,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{A82B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bmm150.Sample", "samples\Bmm150.Sample.csproj", "{10438E1B-E89D-446C-A276-241A6FD4B9DC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arduino", "..\Arduino\Arduino.csproj", "{244B116E-A464-4293-9AC2-0E73BE1B85A4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,18 @@ Global {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Release|x64.Build.0 = Release|Any CPU {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Release|x86.ActiveCfg = Release|Any CPU {10438E1B-E89D-446C-A276-241A6FD4B9DC}.Release|x86.Build.0 = Release|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Debug|x64.Build.0 = Debug|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Debug|x86.Build.0 = Debug|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Release|Any CPU.Build.0 = Release|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Release|x64.ActiveCfg = Release|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Release|x64.Build.0 = Release|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Release|x86.ActiveCfg = Release|Any CPU + {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/devices/Bmm150/Bmm150TrimRegister.cs b/src/devices/Bmm150/Bmm150TrimRegister.cs index 89eb9e6336..e443c3cfe5 100644 --- a/src/devices/Bmm150/Bmm150TrimRegister.cs +++ b/src/devices/Bmm150/Bmm150TrimRegister.cs @@ -13,57 +13,57 @@ public class Bmm150TrimRegisterData /// /// trim DigX1 data /// - public byte DigX1 { get; set; } + public sbyte DigX1 { get; set; } /// /// trim DigY1 data /// - public byte DigY1 { get; set; } + public sbyte DigY1 { get; set; } /// /// trim DigX2 data /// - public byte DigX2 { get; set; } + public sbyte DigX2 { get; set; } /// /// trim DigY2 data /// - public byte DigY2 { get; set; } + public sbyte DigY2 { get; set; } /// /// trim DigZ1 data /// - public int DigZ1 { get; set; } + public ushort DigZ1 { get; set; } /// /// trim DigZ2 data /// - public int DigZ2 { get; set; } + public short DigZ2 { get; set; } /// /// trim DigZ3 data /// - public int DigZ3 { get; set; } + public short DigZ3 { get; set; } /// /// trim DigZ4 data /// - public int DigZ4 { get; set; } + public short DigZ4 { get; set; } /// /// trim DigXy1 data /// - public int DigXy1 { get; set; } + public byte DigXy1 { get; set; } /// /// trim DigXy2 data /// - public int DigXy2 { get; set; } + public sbyte DigXy2 { get; set; } /// /// trim DigXyz1 data /// - public int DigXyz1 { get; set; } + public ushort DigXyz1 { get; set; } /// /// Creates a new instace @@ -80,17 +80,20 @@ public Bmm150TrimRegisterData() /// trimXy1Xy2Data bytes public Bmm150TrimRegisterData(Span trimX1y1Data, Span trimXyzData, Span trimXy1Xy2Data) { - DigX1 = (byte)trimX1y1Data[0]; - DigY1 = (byte)trimX1y1Data[1]; - DigX2 = (byte)trimXyzData[2]; - DigY2 = (byte)trimXyzData[3]; - DigZ1 = trimXy1Xy2Data[3] << 8 | trimXy1Xy2Data[2]; - DigZ2 = (short)(trimXy1Xy2Data[1] << 8 | trimXy1Xy2Data[0]); - DigZ3 = (short)(trimXy1Xy2Data[7] << 8 | trimXy1Xy2Data[6]); - DigZ4 = (short)(trimXyzData[1] << 8 | trimXyzData[0]); - DigXy1 = trimXy1Xy2Data[9]; - DigXy2 = (sbyte)trimXy1Xy2Data[8]; - DigXyz1 = ((trimXy1Xy2Data[5] & 0x7F) << 8) | trimXy1Xy2Data[4]; + unchecked + { + DigX1 = (sbyte)trimX1y1Data[0]; + DigY1 = (sbyte)trimX1y1Data[1]; + DigX2 = (sbyte)trimXyzData[2]; + DigY2 = (sbyte)trimXyzData[3]; + DigZ1 = (ushort)(trimXy1Xy2Data[3] << 8 | trimXy1Xy2Data[2]); + DigZ2 = (short)(trimXy1Xy2Data[1] << 8 | trimXy1Xy2Data[0]); + DigZ3 = (short)(trimXy1Xy2Data[7] << 8 | trimXy1Xy2Data[6]); + DigZ4 = (short)(trimXyzData[1] << 8 | trimXyzData[0]); + DigXy1 = trimXy1Xy2Data[9]; + DigXy2 = (sbyte)trimXy1Xy2Data[8]; + DigXyz1 = (ushort)(((trimXy1Xy2Data[5] & 0x7F) << 8) | trimXy1Xy2Data[4]); + } } } } diff --git a/src/devices/Bmm150/Register.cs b/src/devices/Bmm150/Register.cs index 358fce82a9..01093917e5 100644 --- a/src/devices/Bmm150/Register.cs +++ b/src/devices/Bmm150/Register.cs @@ -28,11 +28,6 @@ internal enum Bmp180Register /// WIA = 0x40, - /// - /// INFO: Information - /// - INFO = 0x01, - /// /// DATA_READY_STATUS: Page 25, data ready status /// diff --git a/src/devices/Bmm150/samples/Bmm150.Sample.csproj b/src/devices/Bmm150/samples/Bmm150.Sample.csproj index 8f3430ac25..33bd6c12b6 100644 --- a/src/devices/Bmm150/samples/Bmm150.Sample.csproj +++ b/src/devices/Bmm150/samples/Bmm150.Sample.csproj @@ -4,6 +4,7 @@ $(DefaultSampleTfms) + \ No newline at end of file diff --git a/src/devices/Bmm150/samples/Program.cs b/src/devices/Bmm150/samples/Program.cs index af9792e76a..fa591f8f6c 100644 --- a/src/devices/Bmm150/samples/Program.cs +++ b/src/devices/Bmm150/samples/Program.cs @@ -4,25 +4,37 @@ using System; using System.Device.I2c; using System.Diagnostics; +using System.IO; using System.Threading; using System.Numerics; +using Iot.Device.Arduino; using Iot.Device.Bmp180; -// The I2C pins 21 and 22 in the sample below are ESP32 specific and may differ from other platforms. -// Please double check your device datasheet. -I2cConnectionSettings mpui2CConnectionSettingmpus = new(1, Bmm150.SecondaryI2cAddress); +using ArduinoBoard board = new ArduinoBoard("COM5", 115200); +I2cConnectionSettings settings = new(0, Bmm150.PrimaryI2cAddress); -using Bmm150 bmm150 = new Bmm150(I2cDevice.Create(mpui2CConnectionSettingmpus)); +using Bmm150 bmm150 = new Bmm150(board.CreateI2cDevice(settings)); Console.WriteLine($"Please move your device in all directions..."); -bmm150.CalibrateMagnetometer(); +////bmm150.CalibrateMagnetometer(new Feedback(), 100); +Console.WriteLine(); Console.WriteLine($"Calibration completed."); while (!Console.KeyAvailable) { - Vector3 magne = bmm150.ReadMagnetometer(true, TimeSpan.FromMilliseconds(11)); + Vector3 magne; + try + { + magne = bmm150.ReadMagnetometerWithoutCorrection(true, TimeSpan.FromMilliseconds(11)); + } + catch (Exception x) when (x is TimeoutException || x is IOException) + { + Console.WriteLine(x.Message); + Thread.Sleep(100); + continue; + } var head_dir = Math.Atan2(magne.X, magne.Y) * 180.0 / Math.PI; @@ -30,3 +42,11 @@ Thread.Sleep(100); } + +internal class Feedback : IProgress +{ + public void Report(double value) + { + Console.Write($"\r{value:F1}% done"); + } +} From 0f599725e903c5afeacdceececf909b6c8d50a3b Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Wed, 1 Jan 2025 12:58:04 +0100 Subject: [PATCH 03/10] Compensation calculation fixed --- src/devices/Bmm150/Bmm150.cs | 14 +- src/devices/Bmm150/Bmm150.sln | 17 ++ src/devices/Bmm150/Bmm150Compensation.cs | 209 +++++++++++++------ src/devices/Bmm150/Bmm150TrimRegister.cs | 22 +- src/devices/Bmm150/tests/Bmm150.Tests.cs | 86 ++++++++ src/devices/Bmm150/tests/Bmm150.Tests.csproj | 11 + 6 files changed, 279 insertions(+), 80 deletions(-) create mode 100644 src/devices/Bmm150/tests/Bmm150.Tests.cs create mode 100644 src/devices/Bmm150/tests/Bmm150.Tests.csproj diff --git a/src/devices/Bmm150/Bmm150.cs b/src/devices/Bmm150/Bmm150.cs index af9c2013ac..f333d870cb 100644 --- a/src/devices/Bmm150/Bmm150.cs +++ b/src/devices/Bmm150/Bmm150.cs @@ -9,6 +9,7 @@ using System.IO; using System.Numerics; using System.Threading; +using UnitsNet; namespace Iot.Device.Bmp180 { @@ -310,7 +311,7 @@ public Vector3 ReadMagnetometerWithoutCorrection(bool waitForData, TimeSpan time /// true to wait for new data /// The data from the magnetometer [Telemetry("Magnetometer")] - public Vector3 ReadMagnetometer(bool waitForData = true) => ReadMagnetometer(waitForData, DefaultTimeout); + public MagneticField[] ReadMagnetometer(bool waitForData = true) => ReadMagnetometer(waitForData, DefaultTimeout); /// /// Read the magnetometer with compensation calculation and can wait for new data to be present @@ -318,15 +319,16 @@ public Vector3 ReadMagnetometerWithoutCorrection(bool waitForData, TimeSpan time /// true to wait for new data /// timeout for waiting the data, ignored if waitForData is false /// The data from the magnetometer - public Vector3 ReadMagnetometer(bool waitForData, TimeSpan timeout) + public MagneticField[] ReadMagnetometer(bool waitForData, TimeSpan timeout) { var magn = ReadMagnetometerWithoutCorrection(waitForData, timeout); - magn.X = (float)Bmm150Compensation.CompensateX(magn.X - CalibrationCompensation.X, _rHall, _trimData); - magn.Y = (float)Bmm150Compensation.CompensateY(magn.Y - CalibrationCompensation.Y, _rHall, _trimData); - magn.Z = (float)Bmm150Compensation.CompensateZ(magn.Z - CalibrationCompensation.Z, _rHall, _trimData); + MagneticField[] ret = new MagneticField[3]; + ret[0] = MagneticField.FromMilliteslas(Bmm150Compensation.CompensateX((int)magn.X, _rHall, _trimData) - CalibrationCompensation.X); + ret[1] = MagneticField.FromMilliteslas(Bmm150Compensation.CompensateY((int)magn.Y, _rHall, _trimData) - CalibrationCompensation.Y); + ret[0] = MagneticField.FromMilliteslas(Bmm150Compensation.CompensateZ((int)magn.Z, _rHall, _trimData) - CalibrationCompensation.Z); - return magn; + return ret; } private void WriteRegister(Bmp180Register reg, byte data) => _bmm150Interface.WriteRegister(_i2cDevice, (byte)reg, data); diff --git a/src/devices/Bmm150/Bmm150.sln b/src/devices/Bmm150/Bmm150.sln index 84d9ecebdd..f157ac8bf7 100644 --- a/src/devices/Bmm150/Bmm150.sln +++ b/src/devices/Bmm150/Bmm150.sln @@ -11,6 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bmm150.Sample", "samples\Bm EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arduino", "..\Arduino\Arduino.csproj", "{244B116E-A464-4293-9AC2-0E73BE1B85A4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B61A74C6-AF72-4FD1-BEDA-C6278922C9CB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bmm150.Tests", "tests\Bmm150.Tests.csproj", "{633BBD73-03F3-49F0-99E4-24411B760CB2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,12 +61,25 @@ Global {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Release|x64.Build.0 = Release|Any CPU {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Release|x86.ActiveCfg = Release|Any CPU {244B116E-A464-4293-9AC2-0E73BE1B85A4}.Release|x86.Build.0 = Release|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Debug|x64.ActiveCfg = Debug|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Debug|x64.Build.0 = Debug|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Debug|x86.ActiveCfg = Debug|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Debug|x86.Build.0 = Debug|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Release|Any CPU.Build.0 = Release|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Release|x64.ActiveCfg = Release|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Release|x64.Build.0 = Release|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Release|x86.ActiveCfg = Release|Any CPU + {633BBD73-03F3-49F0-99E4-24411B760CB2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {10438E1B-E89D-446C-A276-241A6FD4B9DC} = {A82B9026-3C3E-4FDD-9AF9-BE012C7A3CB7} + {633BBD73-03F3-49F0-99E4-24411B760CB2} = {B61A74C6-AF72-4FD1-BEDA-C6278922C9CB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F8CEC844-3F4C-445A-9534-C72F6CD76663} diff --git a/src/devices/Bmm150/Bmm150Compensation.cs b/src/devices/Bmm150/Bmm150Compensation.cs index c391e64000..623cbc8683 100644 --- a/src/devices/Bmm150/Bmm150Compensation.cs +++ b/src/devices/Bmm150/Bmm150Compensation.cs @@ -13,6 +13,11 @@ namespace Iot.Device.Bmp180 /// public class Bmm150Compensation { + private const int Bmm150OverflowAdcvalXYaxesFlip = -4096; + private const int Bmm150OverflowAdcvalZaxisHall = -16384; + private const int Bmm150NegativeSaturationZ = -32767; + private const int Bmm150PositiveSaturationZ = 32767; + /// /// Returns the compensated magnetometer x axis data(micro-tesla) in float. /// More details, permalink: https://github.com/BoschSensortec/BMM150-Sensor-API/blob/a20641f216057f0c54de115fe81b57368e119c01/bmm150.c#L1614 @@ -21,35 +26,66 @@ public class Bmm150Compensation /// temperature compensation value (RHALL) /// trim registers values /// compensated magnetometer x axis data(micro-tesla) in float - public static double CompensateX(double x, uint rhall, Bmm150TrimRegisterData trimData) + public static double CompensateX(int x, uint rhall, Bmm150TrimRegisterData trimData) { - float retval = 0; - float processCompX0; - float processCompX1; - float processCompX2; - float processCompX3; - float processCompX4; - int bmm150_overflow_adcval_xyaxes_flip = -4096; + int retval = 0; + int process_comp_x0 = 0; + int process_comp_x1; + int process_comp_x2; + int process_comp_x3; + int process_comp_x4; + int process_comp_x5; + int process_comp_x6; + int process_comp_x7; + int process_comp_x8; + int process_comp_x9; + int process_comp_x10; // Overflow condition check - if ((x != bmm150_overflow_adcval_xyaxes_flip) && (rhall != 0) && (trimData.DigXyz1 != 0)) + if (x != Bmm150OverflowAdcvalXYaxesFlip) { - // Processing compensation equations - processCompX0 = (((float)trimData.DigXyz1) * 16384.0f / rhall); - retval = (processCompX0 - 16384.0f); - processCompX1 = ((float)trimData.DigXy2) * (retval * retval / 268435456.0f); - processCompX2 = processCompX1 + retval * ((float)trimData.DigXy1) / 16384.0f; - processCompX3 = ((float)trimData.DigX2) + 160.0f; - processCompX4 = (float)(x * ((processCompX2 + 256.0f) * processCompX3)); - retval = ((processCompX4 / 8192.0f) + (((float)trimData.DigX1) * 8.0f)) / 16.0f; + if (rhall != 0) + { + /* Availability of valid data*/ + // rhall is always > 0 and at most 16 bits. In fact, it should be in the order of DigXyz1 (around 6000) + process_comp_x0 = (int)rhall; + } + else if (trimData.DigXyz1 != 0) + { + process_comp_x0 = trimData.DigXyz1; + } + else + { + process_comp_x0 = 0; + } + + if (process_comp_x0 != 0) + { + /* Processing compensation equations*/ + process_comp_x1 = (trimData.DigXyz1) * 16384; // ~ 100'000'000 + process_comp_x2 = ((process_comp_x1 / process_comp_x0)) - (0x4000); // ~0 + retval = (process_comp_x2); + process_comp_x3 = ((retval) * (retval)); + process_comp_x4 = ((trimData.DigXy2) * (process_comp_x3 / 128)); + process_comp_x5 = ((trimData.DigXy1) * 128); + process_comp_x6 = retval * process_comp_x5; + process_comp_x7 = (((process_comp_x4 + process_comp_x6) / 512) + (0x100000)); + process_comp_x8 = (((trimData.DigX2) + (0xA0))); + process_comp_x9 = ((process_comp_x7 * process_comp_x8) / 4096); + process_comp_x10 = (x) * process_comp_x9; + retval = ((process_comp_x10 / 8192)); + return (retval + ((trimData.DigX1) * 8)) / 16.0; + } + else + { + return Double.NaN; + } } else { // Overflow, set output to 0.0f - retval = 0.0f; + return Double.NaN; } - - return retval; } /// @@ -60,35 +96,63 @@ public static double CompensateX(double x, uint rhall, Bmm150TrimRegisterData tr /// temperature compensation value (RHALL) /// trim registers values /// compensated magnetometer y axis data(micro-tesla) in float - public static double CompensateY(double y, uint rhall, Bmm150TrimRegisterData trimData) + public static double CompensateY(int y, uint rhall, Bmm150TrimRegisterData trimData) { - float retval = 0; - float processCompY0; - float processCompY1; - float processCompY2; - float processCompY3; - float processCompY4; - int bmm150_overflow_adcval_xyaxes_flip = -4096; + int retval; + int process_comp_y0 = 0; + int process_comp_y1; + int process_comp_y2; + int process_comp_y3; + int process_comp_y4; + int process_comp_y5; + int process_comp_y6; + int process_comp_y7; + int process_comp_y8; + int process_comp_y9; // Overflow condition check - if ((y != bmm150_overflow_adcval_xyaxes_flip) && (rhall != 0) && (trimData.DigXyz1 != 0)) + if (y != Bmm150OverflowAdcvalXYaxesFlip) { - // Processing compensation equations - processCompY0 = ((float)trimData.DigXyz1) * 16384.0f / rhall; - retval = processCompY0 - 16384.0f; - processCompY1 = ((float)trimData.DigXy2) * (retval * retval / 268435456.0f); - processCompY2 = processCompY1 + retval * ((float)trimData.DigXy1) / 16384.0f; - processCompY3 = ((float)trimData.DigY2) + 160.0f; - processCompY4 = (float)(y * (((processCompY2) + 256.0f) * processCompY3)); - retval = ((processCompY4 / 8192.0f) + (((float)trimData.DigY1) * 8.0f)) / 16.0f; + if (rhall != 0) + { + /* Availability of valid data*/ + process_comp_y0 = (int)rhall; + } + else if (trimData.DigXyz1 != 0) + { + process_comp_y0 = trimData.DigXyz1; + } + else + { + process_comp_y0 = 0; + } + + if (process_comp_y0 != 0) + { + /*Processing compensation equations*/ + process_comp_y1 = ((trimData.DigXyz1) * 16384) / process_comp_y0; + process_comp_y2 = (process_comp_y1) - (0x4000); + retval = (process_comp_y2); + process_comp_y3 = retval * retval; + process_comp_y4 = (trimData.DigXy2) * (process_comp_y3 / 128); + process_comp_y5 = (((trimData.DigXy1) * 128)); + process_comp_y6 = ((process_comp_y4 + (retval * process_comp_y5)) / 512); + process_comp_y7 = trimData.DigY2 + 0xA0; + process_comp_y8 = (((process_comp_y6 + 0x100000) * process_comp_y7) / 4096); + process_comp_y9 = (y * process_comp_y8); + retval = (process_comp_y9 / 8192); + return (retval + ((trimData.DigY1) * 8)) / 16.0; + } + else + { + return double.NaN; + } } else { // Overflow, set output to 0.0f - retval = 0.0f; + return double.NaN; } - - return retval; } /// @@ -99,37 +163,56 @@ public static double CompensateY(double y, uint rhall, Bmm150TrimRegisterData tr /// temperature compensation value (RHALL) /// trim registers values /// compensated magnetometer z axis data(micro-tesla) in float - public static double CompensateZ(double z, uint rhall, Bmm150TrimRegisterData trimData) + public static double CompensateZ(int z, uint rhall, Bmm150TrimRegisterData trimData) { - float retval = 0; - float processCompX0; - float processCompX1; - float processCompZ2; - float processCompZ3; - float processCompZ4; - float processCompZ5; - int bmm150_overflow_adcval_zaxis_hall = -16384; + int retval; + int process_comp_z0; + int process_comp_z1; + int process_comp_z2; + int process_comp_z3; + int process_comp_z4; // Overflow condition check - if ((z != bmm150_overflow_adcval_zaxis_hall) && (trimData.DigZ2 != 0) && - (trimData.DigZ1 != 0) && (trimData.DigXyz1 != 0) && (rhall != 0)) + if (z != Bmm150OverflowAdcvalZaxisHall) { - // Processing compensation equations - processCompX0 = ((float)z) - ((float)trimData.DigZ4); - processCompX1 = ((float)rhall) - ((float)trimData.DigXyz1); - processCompZ2 = (((float)trimData.DigZ3) * processCompX1); - processCompZ3 = ((float)trimData.DigZ1) * ((float)rhall) / 32768.0f; - processCompZ4 = ((float)trimData.DigZ2) + processCompZ3; - processCompZ5 = (processCompX0 * 131072.0f) - processCompZ2; - retval = (processCompZ5 / ((processCompZ4) * 4.0f)) / 16.0f; + if ((trimData.DigZ2 != 0) && (trimData.DigZ1 != 0) + && (rhall != 0) && (trimData.DigXyz1 != 0)) + { + /*Processing compensation equations*/ + process_comp_z0 = ((int)rhall) - (trimData.DigXyz1); + process_comp_z1 = ((trimData.DigZ3) * ((process_comp_z0))) / 4; + process_comp_z2 = (((z - trimData.DigZ4)) * 32768); + process_comp_z3 = (trimData.DigZ1) * ((int)rhall * 2); + process_comp_z4 = ((process_comp_z3 + (32768)) / 65536); + retval = ((process_comp_z2 - process_comp_z1) / (trimData.DigZ2 + process_comp_z4)); + + /* saturate result to +/- 2 micro-tesla */ + if (retval > Bmm150PositiveSaturationZ) + { + retval = Bmm150PositiveSaturationZ; + } + else + { + if (retval < Bmm150NegativeSaturationZ) + { + retval = Bmm150NegativeSaturationZ; + } + } + + /* Conversion of LSB to micro-tesla*/ + return retval / 16.0; + } + else + { + return Double.NaN; + + } } else { - // Overflow, set output to 0.0f - retval = 0.0f; + /* Overflow condition*/ + return double.NaN; } - - return retval; } } } diff --git a/src/devices/Bmm150/Bmm150TrimRegister.cs b/src/devices/Bmm150/Bmm150TrimRegister.cs index e443c3cfe5..61d897bc21 100644 --- a/src/devices/Bmm150/Bmm150TrimRegister.cs +++ b/src/devices/Bmm150/Bmm150TrimRegister.cs @@ -82,17 +82,17 @@ public Bmm150TrimRegisterData(Span trimX1y1Data, Span trimXyzData, S { unchecked { - DigX1 = (sbyte)trimX1y1Data[0]; - DigY1 = (sbyte)trimX1y1Data[1]; - DigX2 = (sbyte)trimXyzData[2]; - DigY2 = (sbyte)trimXyzData[3]; - DigZ1 = (ushort)(trimXy1Xy2Data[3] << 8 | trimXy1Xy2Data[2]); - DigZ2 = (short)(trimXy1Xy2Data[1] << 8 | trimXy1Xy2Data[0]); - DigZ3 = (short)(trimXy1Xy2Data[7] << 8 | trimXy1Xy2Data[6]); - DigZ4 = (short)(trimXyzData[1] << 8 | trimXyzData[0]); - DigXy1 = trimXy1Xy2Data[9]; - DigXy2 = (sbyte)trimXy1Xy2Data[8]; - DigXyz1 = (ushort)(((trimXy1Xy2Data[5] & 0x7F) << 8) | trimXy1Xy2Data[4]); + DigX1 = (sbyte)trimX1y1Data[0]; + DigY1 = (sbyte)trimX1y1Data[1]; + DigX2 = (sbyte)trimXyzData[2]; + DigY2 = (sbyte)trimXyzData[3]; + DigZ1 = (ushort)(trimXy1Xy2Data[3] << 8 | trimXy1Xy2Data[2]); + DigZ2 = (short)(trimXy1Xy2Data[1] << 8 | trimXy1Xy2Data[0]); + DigZ3 = (short)(trimXy1Xy2Data[7] << 8 | trimXy1Xy2Data[6]); + DigZ4 = (short)(trimXyzData[1] << 8 | trimXyzData[0]); + DigXy1 = trimXy1Xy2Data[9]; + DigXy2 = (sbyte)trimXy1Xy2Data[8]; + DigXyz1 = (ushort)(((trimXy1Xy2Data[5] & 0x7F) << 8) | trimXy1Xy2Data[4]); } } } diff --git a/src/devices/Bmm150/tests/Bmm150.Tests.cs b/src/devices/Bmm150/tests/Bmm150.Tests.cs new file mode 100644 index 0000000000..b4526292ce --- /dev/null +++ b/src/devices/Bmm150/tests/Bmm150.Tests.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Device.I2c; +using System.Device.Spi; +using Iot.Device.Bmp180; +using Moq; +using Xunit; + +namespace Iot.Device.Bmm150.Tests +{ + public class Bmm150Tests + { + [Fact] + public void CheckCompensation() + { + var trimData = GetTestTrimData(); + + double x = Bmm150Compensation.CompensateX(-10, 0, trimData); + double y = Bmm150Compensation.CompensateY(10, 6831, trimData); + double z = Bmm150Compensation.CompensateZ(-20, 6831, trimData); + + // Comparing against the implementation for the Arduino (Groove 3-axis compass Bmm150 library) + Assert.Equal(-3.6875, x, 3); + Assert.Equal(3.6875, y, 3); + Assert.Equal(-7.25, z, 3); + } + + [Fact] + public void CompensationFailsOnInvalidInput() + { + var trimData = GetTestTrimData(); + + double x = Bmm150Compensation.CompensateX(-4096, 0, trimData); + double y = Bmm150Compensation.CompensateY(-4096, 6831, trimData); + double z = Bmm150Compensation.CompensateZ(-20, 0, trimData); + + Assert.True(Double.IsNaN(x)); + Assert.True(Double.IsNaN(y)); + Assert.True(Double.IsNaN(z)); + } + + private Bmm150TrimRegisterData GetTestTrimData() + { + // These calibration values are from my particular sensor + Bmm150TrimRegisterData trimData = new Bmm150TrimRegisterData(); + trimData.DigX1 = 0; + trimData.DigY1 = 0; + trimData.DigX2 = 30; + trimData.DigY2 = 30; + trimData.DigZ1 = 23425; + trimData.DigZ2 = 720; + trimData.DigZ3 = 0; + trimData.DigZ4 = 0; + trimData.DigXy1 = 29; + trimData.DigXy2 = -3; + trimData.DigXyz1 = 6567; + return trimData; + } + + internal class MockedI2cDevice : I2cDevice + { + public MockedI2cDevice() + { + ConnectionSettings = new I2cConnectionSettings(0, 0x13); + } + + public override I2cConnectionSettings ConnectionSettings { get; } + public override void Read(Span buffer) + { + throw new NotImplementedException(); + } + + public override void Write(ReadOnlySpan buffer) + { + throw new NotImplementedException(); + } + + public override void WriteRead(ReadOnlySpan writeBuffer, Span readBuffer) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/devices/Bmm150/tests/Bmm150.Tests.csproj b/src/devices/Bmm150/tests/Bmm150.Tests.csproj new file mode 100644 index 0000000000..b48de1bb19 --- /dev/null +++ b/src/devices/Bmm150/tests/Bmm150.Tests.csproj @@ -0,0 +1,11 @@ + + + $(DefaultTestTfms) + 9 + false + false + + + + + \ No newline at end of file From 0e618e1bf6fa8c6192c63976806a2a781662857d Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Wed, 1 Jan 2025 18:56:42 +0100 Subject: [PATCH 04/10] Fix heading calculation Include it in API, to simplify usage and avoid errors --- src/devices/Bmm150/Bmm150.cs | 15 ++-- src/devices/Bmm150/MagnetometerData.cs | 72 +++++++++++++++++++ src/devices/Bmm150/samples/Program.cs | 10 ++- src/devices/Bmm150/tests/Bmm150.Tests.cs | 36 ++++++++++ .../Iot/Device/Common/AngleExtensions.cs | 29 ++++++++ 5 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 src/devices/Bmm150/MagnetometerData.cs diff --git a/src/devices/Bmm150/Bmm150.cs b/src/devices/Bmm150/Bmm150.cs index f333d870cb..0908e5b3d5 100644 --- a/src/devices/Bmm150/Bmm150.cs +++ b/src/devices/Bmm150/Bmm150.cs @@ -140,7 +140,7 @@ private void Initialize() /// /// Number of measurement for the calibration, default is 100 // https://platformio.org/lib/show/12697/M5_BMM150 - [Obsolete("Prefer another overload")] + [Obsolete("Not a reliable way of calibration. Use a calibration determined with MagneticDeviationCorrection instead")] public void CalibrateMagnetometer(int numberOfMeasurements = 100) { CalibrateMagnetometer(null, numberOfMeasurements); @@ -154,6 +154,7 @@ public void CalibrateMagnetometer(int numberOfMeasurements = 100) /// A progress provider (returns a value in percent) /// Number of measurement for the calibration, default is 100 // https://platformio.org/lib/show/12697/M5_BMM150 + [Obsolete("Not a reliable way of calibration. Use a calibration determined with MagneticDeviationCorrection instead")] public void CalibrateMagnetometer(IProgress? progress, int numberOfMeasurements) { Vector3 mag_min = new Vector3() { X = 9000, Y = 9000, Z = 30000 }; @@ -311,7 +312,7 @@ public Vector3 ReadMagnetometerWithoutCorrection(bool waitForData, TimeSpan time /// true to wait for new data /// The data from the magnetometer [Telemetry("Magnetometer")] - public MagneticField[] ReadMagnetometer(bool waitForData = true) => ReadMagnetometer(waitForData, DefaultTimeout); + public MagnetometerData ReadMagnetometer(bool waitForData = true) => ReadMagnetometer(waitForData, DefaultTimeout); /// /// Read the magnetometer with compensation calculation and can wait for new data to be present @@ -319,14 +320,14 @@ public Vector3 ReadMagnetometerWithoutCorrection(bool waitForData, TimeSpan time /// true to wait for new data /// timeout for waiting the data, ignored if waitForData is false /// The data from the magnetometer - public MagneticField[] ReadMagnetometer(bool waitForData, TimeSpan timeout) + public MagnetometerData ReadMagnetometer(bool waitForData, TimeSpan timeout) { var magn = ReadMagnetometerWithoutCorrection(waitForData, timeout); - MagneticField[] ret = new MagneticField[3]; - ret[0] = MagneticField.FromMilliteslas(Bmm150Compensation.CompensateX((int)magn.X, _rHall, _trimData) - CalibrationCompensation.X); - ret[1] = MagneticField.FromMilliteslas(Bmm150Compensation.CompensateY((int)magn.Y, _rHall, _trimData) - CalibrationCompensation.Y); - ret[0] = MagneticField.FromMilliteslas(Bmm150Compensation.CompensateZ((int)magn.Z, _rHall, _trimData) - CalibrationCompensation.Z); + MagnetometerData ret = new MagnetometerData( + MagneticField.FromMicroteslas(Bmm150Compensation.CompensateX((int)magn.X, _rHall, _trimData) - CalibrationCompensation.X), + MagneticField.FromMicroteslas(Bmm150Compensation.CompensateY((int)magn.Y, _rHall, _trimData) - CalibrationCompensation.Y), + MagneticField.FromMicroteslas(Bmm150Compensation.CompensateZ((int)magn.Z, _rHall, _trimData) - CalibrationCompensation.Z)); return ret; } diff --git a/src/devices/Bmm150/MagnetometerData.cs b/src/devices/Bmm150/MagnetometerData.cs new file mode 100644 index 0000000000..b0fc360f77 --- /dev/null +++ b/src/devices/Bmm150/MagnetometerData.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Iot.Device.Common; +using UnitsNet; +using UnitsNet.Units; + +namespace Iot.Device.Bmp180 +{ + /// + /// Container for the magnetometer result data + /// + public record MagnetometerData + { + /// + /// Creates a new instance of this record + /// + /// The X-component of the magnetic field + /// The Y-component of the magnetic field + /// The Z-component of the magnetic field + public MagnetometerData(MagneticField x, MagneticField y, MagneticField z) + { + FieldX = x; + FieldY = y; + FieldZ = z; + } + + /// + /// X component + /// + public MagneticField FieldX { get; } + + /// + /// Y component + /// + public MagneticField FieldY { get; } + + /// + /// Z component + /// + public MagneticField FieldZ { get; } + + /// + /// Magnetic heading of the compass. Assumes the X-Axis is pointing forward and the Z-Axis is pointing up in + /// vehicle orientation. Assumes not too much roll or pitch. + /// + public Angle Heading + { + get + { + // Reorder and inverse the axis, because Atan2 assumes X points east and Y points north, while + // in our case X points north and Y points east + Angle val = Angle.FromRadians(Math.Atan2(FieldX.Microteslas, -FieldY.Microteslas)); + return val.RadiansToAviatic(); + } + } + + /// + /// Magnetic inclination of the compass (amount the magnetic needle is pointing downwards). Assumes the X-Axis is pointing forward and the Z-Axis is pointing up in + /// vehicle orientation. Assumes not too much roll or pitch. + /// + public Angle Inclination + { + get + { + Angle val = Angle.FromRadians(Math.Atan2(FieldX.Microteslas, FieldZ.Microteslas)); + return val.RadiansToAviatic().Normalize(false); // Expected range is +/- 90° + } + } + } +} diff --git a/src/devices/Bmm150/samples/Program.cs b/src/devices/Bmm150/samples/Program.cs index fa591f8f6c..89a2fb7ca2 100644 --- a/src/devices/Bmm150/samples/Program.cs +++ b/src/devices/Bmm150/samples/Program.cs @@ -24,10 +24,10 @@ while (!Console.KeyAvailable) { - Vector3 magne; + MagnetometerData magne; try { - magne = bmm150.ReadMagnetometerWithoutCorrection(true, TimeSpan.FromMilliseconds(11)); + magne = bmm150.ReadMagnetometer(true, TimeSpan.FromMilliseconds(11)); } catch (Exception x) when (x is TimeoutException || x is IOException) { @@ -36,11 +36,9 @@ continue; } - var head_dir = Math.Atan2(magne.X, magne.Y) * 180.0 / Math.PI; + Console.WriteLine($"Mag data: X={magne.FieldX}, Y={magne.FieldY}, Z={magne.FieldZ}, Heading: {magne.Heading}, Inclination: {magne.Inclination}"); - Console.WriteLine($"Mag data: X={magne.X,15}, Y={magne.Y,15}, Z={magne.Z,15}, head_dir: {head_dir}"); - - Thread.Sleep(100); + Thread.Sleep(500); } internal class Feedback : IProgress diff --git a/src/devices/Bmm150/tests/Bmm150.Tests.cs b/src/devices/Bmm150/tests/Bmm150.Tests.cs index b4526292ce..e7a0f24862 100644 --- a/src/devices/Bmm150/tests/Bmm150.Tests.cs +++ b/src/devices/Bmm150/tests/Bmm150.Tests.cs @@ -6,6 +6,7 @@ using System.Device.Spi; using Iot.Device.Bmp180; using Moq; +using UnitsNet; using Xunit; namespace Iot.Device.Bmm150.Tests @@ -41,6 +42,41 @@ public void CompensationFailsOnInvalidInput() Assert.True(Double.IsNaN(z)); } + [Fact] + public void CalculateHeading() + { + MagnetometerData m = new MagnetometerData(MagneticField.FromMicroteslas(18.5), MagneticField.FromMicroteslas(0), MagneticField.Zero); + Assert.Equal(0, m.Heading.Degrees, 3); + + m = new MagnetometerData(MagneticField.FromMicroteslas(18.5), MagneticField.FromMicroteslas(0), MagneticField.FromMicroteslas(16.31)); + Assert.Equal(0, m.Heading.Degrees, 3); + + m = new MagnetometerData(MagneticField.FromMicroteslas(-18.5), MagneticField.FromMicroteslas(0), MagneticField.FromMicroteslas(16.31)); + Assert.Equal(180, m.Heading.Degrees, 3); + + m = new MagnetometerData(MagneticField.FromMicroteslas(18.5), MagneticField.FromMicroteslas(1), MagneticField.FromMicroteslas(16.31)); + Assert.Equal(356.90594194, m.Heading.Degrees, 3); + + m = new MagnetometerData(MagneticField.FromMicroteslas(0), MagneticField.FromMicroteslas(18.5), MagneticField.FromMicroteslas(16.31)); + Assert.Equal(270, m.Heading.Degrees, 3); + } + + [Fact] + public void CalculateInclination() + { + MagnetometerData m = new MagnetometerData(MagneticField.FromMicroteslas(18.5), MagneticField.FromMicroteslas(0), MagneticField.Zero); + Assert.Equal(0, m.Inclination.Degrees, 3); + + m = new MagnetometerData(MagneticField.FromMicroteslas(18.5), MagneticField.FromMicroteslas(0), MagneticField.FromMicroteslas(18.5)); + Assert.Equal(45, m.Inclination.Degrees, 3); + + m = new MagnetometerData(MagneticField.FromMicroteslas(0), MagneticField.FromMicroteslas(0), MagneticField.FromMicroteslas(16.31)); + Assert.Equal(90, m.Inclination.Degrees, 3); + + m = new MagnetometerData(MagneticField.FromMicroteslas(0), MagneticField.FromMicroteslas(0), MagneticField.FromMicroteslas(-16.31)); + Assert.Equal(-90, m.Inclination.Degrees, 3); + } + private Bmm150TrimRegisterData GetTestTrimData() { // These calibration values are from my particular sensor diff --git a/src/devices/Common/Iot/Device/Common/AngleExtensions.cs b/src/devices/Common/Iot/Device/Common/AngleExtensions.cs index f20e9948d6..be61f738ed 100644 --- a/src/devices/Common/Iot/Device/Common/AngleExtensions.cs +++ b/src/devices/Common/Iot/Device/Common/AngleExtensions.cs @@ -156,5 +156,34 @@ public static bool TryAverageAngle(this IEnumerable inputAngles, out Angl result = default; return false; } + + /// + /// Converts an angle in aviatic definition to mathematic definition. + /// Aviatic angles are in degrees, where 0 degrees is north, counting clockwise, mathematic angles + /// are in radians, starting east and going counterclockwise. + /// + /// Aviatic angle, default unit degrees + /// Mathematic angle, default unit radians + public static Angle AviaticToRadians(this Angle input) + { + double ret = ((-input.Degrees) + 90.0); + ret = ret * Math.PI / 180.0; + + return Angle.FromRadians(ret).Normalize(true); + } + + /// + /// Convert angle from mathematic definition to aviatic. + /// See also AviaticToRadians() + /// + /// Mathematic value, typically in radians + /// Aviatic value in degrees + public static Angle RadiansToAviatic(this Angle input) + { + double ret = input.Radians * 180.0 / Math.PI; + ret = ((-ret) + 90.0); + + return Angle.FromDegrees(ret).Normalize(true); + } } } From 9aa64292ed6acb4dc47c7c4d35d532491be7a080 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sat, 18 Jan 2025 15:02:05 +0100 Subject: [PATCH 05/10] Documentation update --- src/devices/Bmm150/Bmm150.sln | 14 ++++ src/devices/Bmm150/Bmm150TrimRegister.cs | 22 +++--- src/devices/Bmm150/README.md | 93 ++++++++++-------------- src/devices/Bmm150/samples/Program.cs | 11 +-- 4 files changed, 67 insertions(+), 73 deletions(-) diff --git a/src/devices/Bmm150/Bmm150.sln b/src/devices/Bmm150/Bmm150.sln index f157ac8bf7..1dc76e35a3 100644 --- a/src/devices/Bmm150/Bmm150.sln +++ b/src/devices/Bmm150/Bmm150.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B61A74C6 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bmm150.Tests", "tests\Bmm150.Tests.csproj", "{633BBD73-03F3-49F0-99E4-24411B760CB2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "..\Common\Common.csproj", "{AB9C073C-EC4B-43ED-8030-60208A54612C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +75,18 @@ Global {633BBD73-03F3-49F0-99E4-24411B760CB2}.Release|x64.Build.0 = Release|Any CPU {633BBD73-03F3-49F0-99E4-24411B760CB2}.Release|x86.ActiveCfg = Release|Any CPU {633BBD73-03F3-49F0-99E4-24411B760CB2}.Release|x86.Build.0 = Release|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Debug|x64.Build.0 = Debug|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Debug|x86.Build.0 = Debug|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Release|Any CPU.Build.0 = Release|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Release|x64.ActiveCfg = Release|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Release|x64.Build.0 = Release|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Release|x86.ActiveCfg = Release|Any CPU + {AB9C073C-EC4B-43ED-8030-60208A54612C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/devices/Bmm150/Bmm150TrimRegister.cs b/src/devices/Bmm150/Bmm150TrimRegister.cs index 61d897bc21..e443c3cfe5 100644 --- a/src/devices/Bmm150/Bmm150TrimRegister.cs +++ b/src/devices/Bmm150/Bmm150TrimRegister.cs @@ -82,17 +82,17 @@ public Bmm150TrimRegisterData(Span trimX1y1Data, Span trimXyzData, S { unchecked { - DigX1 = (sbyte)trimX1y1Data[0]; - DigY1 = (sbyte)trimX1y1Data[1]; - DigX2 = (sbyte)trimXyzData[2]; - DigY2 = (sbyte)trimXyzData[3]; - DigZ1 = (ushort)(trimXy1Xy2Data[3] << 8 | trimXy1Xy2Data[2]); - DigZ2 = (short)(trimXy1Xy2Data[1] << 8 | trimXy1Xy2Data[0]); - DigZ3 = (short)(trimXy1Xy2Data[7] << 8 | trimXy1Xy2Data[6]); - DigZ4 = (short)(trimXyzData[1] << 8 | trimXyzData[0]); - DigXy1 = trimXy1Xy2Data[9]; - DigXy2 = (sbyte)trimXy1Xy2Data[8]; - DigXyz1 = (ushort)(((trimXy1Xy2Data[5] & 0x7F) << 8) | trimXy1Xy2Data[4]); + DigX1 = (sbyte)trimX1y1Data[0]; + DigY1 = (sbyte)trimX1y1Data[1]; + DigX2 = (sbyte)trimXyzData[2]; + DigY2 = (sbyte)trimXyzData[3]; + DigZ1 = (ushort)(trimXy1Xy2Data[3] << 8 | trimXy1Xy2Data[2]); + DigZ2 = (short)(trimXy1Xy2Data[1] << 8 | trimXy1Xy2Data[0]); + DigZ3 = (short)(trimXy1Xy2Data[7] << 8 | trimXy1Xy2Data[6]); + DigZ4 = (short)(trimXyzData[1] << 8 | trimXyzData[0]); + DigXy1 = trimXy1Xy2Data[9]; + DigXy2 = (sbyte)trimXy1Xy2Data[8]; + DigXyz1 = (ushort)(((trimXy1Xy2Data[5] & 0x7F) << 8) | trimXy1Xy2Data[4]); } } } diff --git a/src/devices/Bmm150/README.md b/src/devices/Bmm150/README.md index 8dfbb6efd1..db6fa2e9de 100644 --- a/src/devices/Bmm150/README.md +++ b/src/devices/Bmm150/README.md @@ -1,7 +1,7 @@ # Bmm150 - Magnetometer -The Bmm150 is a magnetometer that can be controlled either thru I2C either thru SPI. -This implementation was tested in a ESP32 platform, specificaly in a [M5Stack Gray](https://shop.m5stack.com/products/grey-development-core). +The Bmm150 is a magnetometer that can be controlled either through I2C or through SPI. +This implementation was tested in a ESP32 platform, specifically in an [M5Stack Gray](https://shop.m5stack.com/products/grey-development-core). ## Documentation @@ -9,74 +9,59 @@ Documentation for the Bmm150 can be [found here](https://www.bosch-sensortec.com ## Usage -You can find an example in the [sample](./samples/Program.cs) directory. Usage is straight forward including the possibility to have a calibration. +You can find an example in the [sample](./samples/Program.cs) directory. Usage is straight forward. The "Calibration" provided +needs conceptual review and is therefore currently not recommended. Also, when using the magnetometer in a real-world application, +it is not reasonable to turn it around all axes at every startup (try that with a car!). ```csharp I2cConnectionSettings mpui2CConnectionSettingmpus = new(1, Bmm150.DefaultI2cAddress); using Bmm150 bmm150 = new Bmm150(I2cDevice.Create(mpui2CConnectionSettingmpus)); -Console.WriteLine($"Please move your device in all directions..."); -bmm150.CalibrateMagnetometer(); - -Console.WriteLine($"Calibration completed."); - -while (true) +while (!Console.KeyAvailable) { - Vector3 magne = bmm150.ReadMagnetometer(true, TimeSpan.FromMilliseconds(11)); - - var head_dir = Math.Atan2(magne.X, magne.Y) * 180.0 / Math.PI; - - Console.WriteLine($"Mag data: X={magne.X,15}, Y={magne.Y,15}, Z={magne.Z,15}, head_dir: {head_dir}"); - - Thread.Sleep(100); + MagnetometerData magne; + try + { + magne = bmm150.ReadMagnetometer(true, TimeSpan.FromMilliseconds(11)); + } + catch (Exception x) when (x is TimeoutException || x is IOException) + { + Console.WriteLine(x.Message); + Thread.Sleep(100); + continue; + } + + Console.WriteLine($"Mag data: X={magne.FieldX}, Y={magne.FieldY}, Z={magne.FieldZ}, Heading: {magne.Heading}, Inclination: {magne.Inclination}"); + + Thread.Sleep(500); } -``` ### Expected output ```console -Please move your device in all directions... -Calibration completed. -Mag data: X= 32.97089767, Y= -10.99029922, Z= -27.41439819, head_dir: 108.43494945 -Mag data: X= 38.83239364, Y= -10.62395668, Z= -22.2116661, head_dir: 105.30084201 -Mag data: X= 43.96039581, Y= -8.4257431, Z= 4.60182046, head_dir: 100.85010634 -Mag data: X= 42.49582672, Y= -8.059553146, Z= 9.0047292709, head_dir: 100.7388972 -Mag data: X= 42.86371994, Y= -12.8224802, Z= 8.20643711, head_dir: 106.65430547 -Mag data: X= 36.26864242, Y= -6.22794914, Z= -21.41402244, head_dir: 99.74364301 -Mag data: X= 29.30693054, Y= -9.89108943, Z= -32.21274185, head_dir: 108.6495335 -Mag data: X= 15.75333309, Y= -8.42620182, Z= -37.029045104, head_dir: 118.14159082 -Mag data: X= 4.7626357, Y= -6.22806167, Z= -42.23312759, head_dir: 142.59463794 -Mag data: X= -4.39627885, Y= -6.59441852, Z= -36.22841644, head_dir: -146.309933 -Mag data: X= -10.25779819, Y= -5.12889909, Z= -38.62528991, head_dir: -116.56504656 -Mag data: X= -19.050889968, Y= 0.73272651, Z= -37.033847808, head_dir: -87.7974031 -Mag data: X= -35.90294647, Y= 1.46542632, Z= -18.61460113, head_dir: -87.66269127 -Mag data: X= -37.73472976, Y= 8.42620182, Z= -13.41051959, head_dir: -77.41230537 -Mag data: X= -37.73472976, Y= 9.52527141, Z= -12.20957756, head_dir: -75.83294707 -Mag data: X= -18.31749725, Y= 0.73269987, Z= -31.42057418, head_dir: -87.70938928 -Mag data: X= -2.19813942, Y= -7.69348812, Z= -37.029045104, head_dir: -164.054600542 +Mag data: X=15.5 µT, Y=6.25 µT, Z=16.13 µT, Heading: 338.04 °, Inclination: 46.13 ° +Mag data: X=18.44 µT, Y=4.75 µT, Z=13.56 µT, Heading: 345.55 °, Inclination: 36.34 ° +Mag data: X=18.06 µT, Y=1.44 µT, Z=19.81 µT, Heading: 355.45 °, Inclination: 47.65 ° +Mag data: X=17.69 µT, Y=1.44 µT, Z=15.44 µT, Heading: 355.35 °, Inclination: 41.11 ° +Mag data: X=17.69 µT, Y=0 µT, Z=19.81 µT, Heading: 0 °, Inclination: 48.24 ° +Mag data: X=17.69 µT, Y=-0.69 µT, Z=16.13 µT, Heading: 2.23 °, Inclination: 42.35 ° +Mag data: X=18.81 µT, Y=0 µT, Z=19.06 µT, Heading: 0 °, Inclination: 45.38 ° +Mag data: X=16.63 µT, Y=-1.44 µT, Z=18 µT, Heading: 4.94 °, Inclination: 47.27 ° +Mag data: X=17.69 µT, Y=-1.44 µT, Z=16.5 µT, Heading: 4.65 °, Inclination: 43.01 ° +Mag data: X=17.69 µT, Y=0.69 µT, Z=20.94 µT, Heading: 357.77 °, Inclination: 49.81 ° +Mag data: X=18.06 µT, Y=-0.31 µT, Z=15.44 µT, Heading: 0.99 °, Inclination: 40.52 ° +Mag data: X=17 µT, Y=4.38 µT, Z=20.56 µT, Heading: 345.57 °, Inclination: 50.42 ° +Mag data: X=16.63 µT, Y=8.5 µT, Z=18 µT, Heading: 332.92 °, Inclination: 47.27 ° +Mag data: X=14 µT, Y=10.69 µT, Z=17.63 µT, Heading: 322.64 °, Inclination: 51.54 ° +Mag data: X=14.75 µT, Y=8.5 µT, Z=17.25 µT, Heading: 330.05 °, Inclination: 49.47 ° ``` ## Calibration -You can get access perfom calibration thru the ```CalibrateMagnetometer``` function which will. Be aware that the calibration takes a few seconds. - -```csharp -bmm150.CalibrateMagnetometer(); -``` - -If no calibration is performed, you will get a raw data cloud which looks like this: - -![raw data](./rawcalib.png) - -Running the calibration properly require to **move the sensor in all the possible directions** while performing the calibration. You should consider running it with enough samples, at least few hundreds. The default is set to 100. While moving the sensor in all direction, far from any magnetic field, you will get the previous clouds. Calculating the average from those clouds and subtracting it from the read value will give you a centered cloud of data like this: - -![raw data](./corrcalib.png) - -To create those cloud point graphs, every cloud is a coordinate of X-Y, Y-Z and Z-X. - -Once the calibration is done, you will be able to read the data with the bias corrected using the ```ReadMagnetometer``` function. You will still be able to read the data without any calibration using the ```ReadMagnetometerWithoutCalibration``` function. +To calibrate a compass, you need to calculate the deviation table. See `Iot.Device.Nmea0183.MagneticDeviationCorrection` for a class +to create an automatic deviation table. ## Not supported/implemented features of the Bmm150 @@ -86,4 +71,4 @@ Once the calibration is done, you will be able to read the data with the bias co ## Notes -* The BMI160 embedd this BMM150. +* The BMI160 embeds this BMM150. diff --git a/src/devices/Bmm150/samples/Program.cs b/src/devices/Bmm150/samples/Program.cs index 89a2fb7ca2..d866922608 100644 --- a/src/devices/Bmm150/samples/Program.cs +++ b/src/devices/Bmm150/samples/Program.cs @@ -15,12 +15,14 @@ using Bmm150 bmm150 = new Bmm150(board.CreateI2cDevice(settings)); +/* Calibration commented out, this is impractical and - the way it's implemented - most of the time just incorrect. Console.WriteLine($"Please move your device in all directions..."); -////bmm150.CalibrateMagnetometer(new Feedback(), 100); +bmm150.CalibrateMagnetometer(new Feedback(), 100); Console.WriteLine(); Console.WriteLine($"Calibration completed."); +*/ while (!Console.KeyAvailable) { @@ -41,10 +43,3 @@ Thread.Sleep(500); } -internal class Feedback : IProgress -{ - public void Report(double value) - { - Console.Write($"\r{value:F1}% done"); - } -} From 2e4c2be290cd453a8ef629b53a72042feb6de964 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sat, 18 Jan 2025 15:02:56 +0100 Subject: [PATCH 06/10] Fix for malfunctioning serial port drivers --- src/devices/Arduino/ArduinoBoard.cs | 1 + src/devices/Arduino/FirmataDevice.cs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/devices/Arduino/ArduinoBoard.cs b/src/devices/Arduino/ArduinoBoard.cs index 75335bac2e..618bdfdda0 100644 --- a/src/devices/Arduino/ArduinoBoard.cs +++ b/src/devices/Arduino/ArduinoBoard.cs @@ -96,6 +96,7 @@ public ArduinoBoard(string portName, int baudRate) { _dataStream = null; _serialPort = new SerialPort(portName, baudRate); + _serialPort.ReadTimeout = int.MaxValue - 10; StreamUsesHardwareFlowControl = false; // Would need to configure the serial port externally for this to work _logger = this.GetCurrentClassLogger(); } diff --git a/src/devices/Arduino/FirmataDevice.cs b/src/devices/Arduino/FirmataDevice.cs index 28f7a728ce..4b04d7b136 100644 --- a/src/devices/Arduino/FirmataDevice.cs +++ b/src/devices/Arduino/FirmataDevice.cs @@ -741,12 +741,19 @@ private bool FillQueue() throw new ObjectDisposedException(nameof(FirmataDevice)); } - Span rawData = stackalloc byte[512]; + try + { + Span rawData = stackalloc byte[512]; - int bytesRead = _firmataStream.Read(rawData); - for (int i = 0; i < bytesRead; i++) + int bytesRead = _firmataStream.Read(rawData); + for (int i = 0; i < bytesRead; i++) + { + _dataQueue.Enqueue(rawData[i]); + } + } + catch (TimeoutException x) { - _dataQueue.Enqueue(rawData[i]); + _logger.LogWarning(x, "Input stream reported timeout - likely and incorrectly configured driver and thus ignoring."); } return _dataQueue.Count > 0; From 6d013e21e281bd917e8f25658efc83daead1a779 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sat, 18 Jan 2025 15:09:15 +0100 Subject: [PATCH 07/10] Documentation fix --- src/devices/Nmea0183/MagneticDeviationCorrection.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/devices/Nmea0183/MagneticDeviationCorrection.cs b/src/devices/Nmea0183/MagneticDeviationCorrection.cs index 4b4b412627..fa5a2dbe36 100644 --- a/src/devices/Nmea0183/MagneticDeviationCorrection.cs +++ b/src/devices/Nmea0183/MagneticDeviationCorrection.cs @@ -73,12 +73,12 @@ public Identification? Identification } /// - /// BLahafasel + /// List of NMEA sentences used to perform the calibration. /// public List SentencesUsed => _interestingSentences; /// - /// Tries to calculate a correction from the given recorded file. + /// Tries to calculate a correction from the given recorded NMEA logfile. /// The recorded file should contain a data set where the vessel is turning two slow circles, one with the clock and one against the clock, /// in calm conditions and with no current. /// @@ -100,7 +100,7 @@ public void CreateCorrectionTable(Stream stream) } /// - /// Tries to calculate a correction from the given recorded files, indicating the timespan where the calibration loops were performed. + /// Tries to calculate a correction from the given recorded files, indicating the timespan when the calibration loops were performed. /// The recorded file should contain a data set where the vessel is turning two slow circles, one with the clock and one against the clock, /// in calm conditions and with no current. /// @@ -126,7 +126,7 @@ public void CreateCorrectionTable(string[] fileSet, DateTimeOffset beginCalibrat /// /// Tries to calculate a correction from the given recorded file strems, indicating the timespan where the calibration loops were performed. /// The recorded file should contain a data set where the vessel is turning two slow circles, one with the clock and one against the clock, - /// in calm conditions and with no current. + /// in calm conditions and with no current. RMC and HDM messages must be available for this timespan. /// /// The recorded nmea files (from a logged session) /// The start time of the calibration loops From 0c5c8f48231500ba2df34b9a2416a64058125a57 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sun, 19 Jan 2025 14:10:12 +0100 Subject: [PATCH 08/10] Update suppressions --- .../CompatibilitySuppressions.xml | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml index 8d82106cbf..685f93eedb 100644 --- a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml +++ b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml @@ -50,6 +50,34 @@ lib/net6.0/Iot.Device.Bindings.dll true + + CP0002 + M:Iot.Device.Bmp180.Bmm150.GetDeviceInfo + lib/net6.0/Iot.Device.Bindings.dll + lib/net6.0/Iot.Device.Bindings.dll + true + + + CP0002 + M:Iot.Device.Bmp180.Bmm150Compensation.CompensateX(System.Double,System.UInt32,Iot.Device.Bmp180.Bmm150TrimRegisterData) + lib/net6.0/Iot.Device.Bindings.dll + lib/net6.0/Iot.Device.Bindings.dll + true + + + CP0002 + M:Iot.Device.Bmp180.Bmm150Compensation.CompensateY(System.Double,System.UInt32,Iot.Device.Bmp180.Bmm150TrimRegisterData) + lib/net6.0/Iot.Device.Bindings.dll + lib/net6.0/Iot.Device.Bindings.dll + true + + + CP0002 + M:Iot.Device.Bmp180.Bmm150Compensation.CompensateZ(System.Double,System.UInt32,Iot.Device.Bmp180.Bmm150TrimRegisterData) + lib/net6.0/Iot.Device.Bindings.dll + lib/net6.0/Iot.Device.Bindings.dll + true + CP0002 M:Iot.Device.Board.Board.CreatePwmChannel(System.Int32,System.Int32,System.Int32,System.Double,System.Int32,System.Device.Gpio.PinNumberingScheme) @@ -183,6 +211,34 @@ lib/netstandard2.0/Iot.Device.Bindings.dll true + + CP0002 + M:Iot.Device.Bmp180.Bmm150.GetDeviceInfo + lib/netstandard2.0/Iot.Device.Bindings.dll + lib/netstandard2.0/Iot.Device.Bindings.dll + true + + + CP0002 + M:Iot.Device.Bmp180.Bmm150Compensation.CompensateX(System.Double,System.UInt32,Iot.Device.Bmp180.Bmm150TrimRegisterData) + lib/netstandard2.0/Iot.Device.Bindings.dll + lib/netstandard2.0/Iot.Device.Bindings.dll + true + + + CP0002 + M:Iot.Device.Bmp180.Bmm150Compensation.CompensateY(System.Double,System.UInt32,Iot.Device.Bmp180.Bmm150TrimRegisterData) + lib/netstandard2.0/Iot.Device.Bindings.dll + lib/netstandard2.0/Iot.Device.Bindings.dll + true + + + CP0002 + M:Iot.Device.Bmp180.Bmm150Compensation.CompensateZ(System.Double,System.UInt32,Iot.Device.Bmp180.Bmm150TrimRegisterData) + lib/netstandard2.0/Iot.Device.Bindings.dll + lib/netstandard2.0/Iot.Device.Bindings.dll + true + CP0002 M:Iot.Device.Board.Board.CreatePwmChannel(System.Int32,System.Int32,System.Int32,System.Double,System.Int32,System.Device.Gpio.PinNumberingScheme) From c777f98ce962c6537f9df29be61cfb74f01da4ff Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Mon, 27 Jan 2025 11:29:17 +0100 Subject: [PATCH 09/10] Review findings --- .../CompatibilitySuppressions.xml | 14 +++ src/devices/Arduino/ArduinoBoard.cs | 6 ++ src/devices/Bmm150/Bmm150.cs | 85 ------------------ src/devices/Bmm150/README.md | 6 +- src/devices/Bmm150/corrcalib.png | Bin 38167 -> 0 bytes src/devices/Bmm150/rawcalib.png | Bin 12289 -> 0 bytes 6 files changed, 22 insertions(+), 89 deletions(-) delete mode 100644 src/devices/Bmm150/corrcalib.png delete mode 100644 src/devices/Bmm150/rawcalib.png diff --git a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml index 685f93eedb..cc443c7dc1 100644 --- a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml +++ b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml @@ -50,6 +50,13 @@ lib/net6.0/Iot.Device.Bindings.dll true + + CP0002 + M:Iot.Device.Bmp180.Bmm150.CalibrateMagnetometer(System.Int32) + lib/net6.0/Iot.Device.Bindings.dll + lib/net6.0/Iot.Device.Bindings.dll + true + CP0002 M:Iot.Device.Bmp180.Bmm150.GetDeviceInfo @@ -211,6 +218,13 @@ lib/netstandard2.0/Iot.Device.Bindings.dll true + + CP0002 + M:Iot.Device.Bmp180.Bmm150.CalibrateMagnetometer(System.Int32) + lib/netstandard2.0/Iot.Device.Bindings.dll + lib/netstandard2.0/Iot.Device.Bindings.dll + true + CP0002 M:Iot.Device.Bmp180.Bmm150.GetDeviceInfo diff --git a/src/devices/Arduino/ArduinoBoard.cs b/src/devices/Arduino/ArduinoBoard.cs index 618bdfdda0..3c6fa663b3 100644 --- a/src/devices/Arduino/ArduinoBoard.cs +++ b/src/devices/Arduino/ArduinoBoard.cs @@ -79,6 +79,11 @@ public ArduinoBoard(Stream serialPortStream, bool usesHardwareFlowControl) /// /// The device is initialized when the first command is sent. The constructor always succeeds. /// + /// + /// The stream must have a blocking read operation, or the connection might fail. Some serial port drivers incorrectly + /// return immediately when no data is available and the ReadTimeout is set to infinite (the default). In such a case, set the + /// ReadTimeout to a large value (such as Int.Max - 10), which will simulate a blocking call. + /// /// A stream to an Arduino/Firmata device public ArduinoBoard(Stream serialPortStream) : this(serialPortStream, false) @@ -96,6 +101,7 @@ public ArduinoBoard(string portName, int baudRate) { _dataStream = null; _serialPort = new SerialPort(portName, baudRate); + // Set the timeout to a long time, but not infinite. See the note for the constructor above. _serialPort.ReadTimeout = int.MaxValue - 10; StreamUsesHardwareFlowControl = false; // Would need to configure the serial port externally for this to work _logger = this.GetCurrentClassLogger(); diff --git a/src/devices/Bmm150/Bmm150.cs b/src/devices/Bmm150/Bmm150.cs index 0908e5b3d5..0a2c13cd33 100644 --- a/src/devices/Bmm150/Bmm150.cs +++ b/src/devices/Bmm150/Bmm150.cs @@ -133,91 +133,6 @@ private void Initialize() WriteRegister(Bmp180Register.OP_MODE_ADDR, 0x00); } - /// - /// Calibrate the magnetometer. - /// Please make sure you are not close to any magnetic field like magnet or phone - /// Please make sure you are moving the magnetometer all over space, rotating it. - /// - /// Number of measurement for the calibration, default is 100 - // https://platformio.org/lib/show/12697/M5_BMM150 - [Obsolete("Not a reliable way of calibration. Use a calibration determined with MagneticDeviationCorrection instead")] - public void CalibrateMagnetometer(int numberOfMeasurements = 100) - { - CalibrateMagnetometer(null, numberOfMeasurements); - } - - /// - /// Calibrate the magnetometer. - /// Please make sure you are not close to any magnetic field like magnet or phone - /// Please make sure you are moving the magnetometer all over space, rotating it. - /// - /// A progress provider (returns a value in percent) - /// Number of measurement for the calibration, default is 100 - // https://platformio.org/lib/show/12697/M5_BMM150 - [Obsolete("Not a reliable way of calibration. Use a calibration determined with MagneticDeviationCorrection instead")] - public void CalibrateMagnetometer(IProgress? progress, int numberOfMeasurements) - { - Vector3 mag_min = new Vector3() { X = 9000, Y = 9000, Z = 30000 }; - Vector3 mag_max = new Vector3() { X = -9000, Y = -9000, Z = -30000 }; - Vector3 rawMagnetometerData; - if (numberOfMeasurements <= 0) - { - throw new ArgumentOutOfRangeException(nameof(numberOfMeasurements), "The number of measurements must be > 0"); - } - - for (int i = 0; i < numberOfMeasurements; i++) - { - try - { - rawMagnetometerData = ReadMagnetometerWithoutCorrection(); - - if (rawMagnetometerData.X != 0) - { - mag_min.X = (rawMagnetometerData.X < mag_min.X) ? rawMagnetometerData.X : mag_min.X; - mag_max.X = (rawMagnetometerData.X > mag_max.X) ? rawMagnetometerData.X : mag_max.X; - } - - if (rawMagnetometerData.Y != 0) - { - mag_max.Y = (rawMagnetometerData.Y > mag_max.Y) ? rawMagnetometerData.Y : mag_max.Y; - mag_min.Y = (rawMagnetometerData.Y < mag_min.Y) ? rawMagnetometerData.Y : mag_min.Y; - } - - if (rawMagnetometerData.Z != 0) - { - mag_min.Z = (rawMagnetometerData.Z < mag_min.Z) ? rawMagnetometerData.Z : mag_min.Z; - mag_max.Z = (rawMagnetometerData.Z > mag_max.Z) ? rawMagnetometerData.Z : mag_max.Z; - } - - // Wait for 100ms until next reading - Wait(100); - } - catch - { - // skip this reading - } - - if (progress != null) - { - double percentDone = ((double)i / numberOfMeasurements) * 100.0; - progress.Report(percentDone); - } - } - - if (progress != null) - { - progress.Report(100.0); - } - - // Refresh CalibrationCompensation vector - CalibrationCompensation = new Vector3() - { - X = (mag_max.X + mag_min.X) / 2, - Y = (mag_max.Y + mag_min.Y) / 2, - Z = (mag_max.Z + mag_min.Z) / 2 - }; - } - /// /// True if there is a data to read /// diff --git a/src/devices/Bmm150/README.md b/src/devices/Bmm150/README.md index db6fa2e9de..b368ae363f 100644 --- a/src/devices/Bmm150/README.md +++ b/src/devices/Bmm150/README.md @@ -9,16 +9,14 @@ Documentation for the Bmm150 can be [found here](https://www.bosch-sensortec.com ## Usage -You can find an example in the [sample](./samples/Program.cs) directory. Usage is straight forward. The "Calibration" provided -needs conceptual review and is therefore currently not recommended. Also, when using the magnetometer in a real-world application, -it is not reasonable to turn it around all axes at every startup (try that with a car!). +You can find an example in the [sample](./samples/Program.cs) directory. Usage is straight forward. The previous "Calibration" method +was removed, as it would need to be completely rewritten to do something useful. ```csharp I2cConnectionSettings mpui2CConnectionSettingmpus = new(1, Bmm150.DefaultI2cAddress); using Bmm150 bmm150 = new Bmm150(I2cDevice.Create(mpui2CConnectionSettingmpus)); - while (!Console.KeyAvailable) { MagnetometerData magne; diff --git a/src/devices/Bmm150/corrcalib.png b/src/devices/Bmm150/corrcalib.png deleted file mode 100644 index 89291e21518115a165a294781f934eacbde7c093..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38167 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 diff --git a/src/devices/Bmm150/rawcalib.png b/src/devices/Bmm150/rawcalib.png deleted file mode 100644 index 9c250833759eae64195483e9c9518b642493190d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12289 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 Date: Tue, 28 Jan 2025 08:07:08 +0100 Subject: [PATCH 10/10] Format fix --- src/devices/Bmm150/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/devices/Bmm150/README.md b/src/devices/Bmm150/README.md index b368ae363f..796b61aafa 100644 --- a/src/devices/Bmm150/README.md +++ b/src/devices/Bmm150/README.md @@ -35,6 +35,7 @@ while (!Console.KeyAvailable) Thread.Sleep(500); } +``` ### Expected output