diff --git a/src/Iot.Device.Bindings/Iot.Device.Bindings.csproj b/src/Iot.Device.Bindings/Iot.Device.Bindings.csproj index 2b5e8bdaacc7e7d0b9fe8ef3436344ae2d12038c..76c0ad036b6c1a0484dc16d9eab3e6d7320b63e1 100644 --- a/src/Iot.Device.Bindings/Iot.Device.Bindings.csproj +++ b/src/Iot.Device.Bindings/Iot.Device.Bindings.csproj @@ -2,7 +2,7 @@ netstandard2.0;net6.0;netcoreapp3.1 - 9 + 10 enable $(DefineConstants);BUILDING_IOT_DEVICE_BINDINGS true diff --git a/src/devices/Hx711I2c/Hx711I2c.cs b/src/devices/Hx711I2c/Hx711I2c.cs new file mode 100644 index 0000000000000000000000000000000000000000..9e5468c789e29bd00637c870708a367aab919d4e --- /dev/null +++ b/src/devices/Hx711I2c/Hx711I2c.cs @@ -0,0 +1,393 @@ +// 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.Buffers.Binary; +using System.Device.Gpio; +using System.Device.I2c; +using System.Device.Model; +using System.Diagnostics; +using System.Threading; +using UnitsNet; + +namespace Iot.Device.Hx711; + +/// +/// DFRobot KIT0176: I2C 1kg Weight Sensor Kit +/// +[Interface("DFRobot KIT0176: I2C 1kg Weight Sensor Kit")] +public sealed class Hx711I2c : IDisposable +{ + /// + /// Default address for I2C, only use when pins A0 and A1 are set to 0. + /// Otherwise use GetI2cAddress + /// + public const int DefaultI2cAddress = 0x64; + + /// + /// Arbitrarily picked value for empty scale. + /// + public const float DefaultOffset = 6780606.5f; + + private I2cDevice _i2c; + + /// + /// Raw value telling where 0 is. + /// It will be set to current weight when Tare function is used. + /// Value passed must be a raw reading - use . + /// This value does not have specific unit but is linearly correlated to weight reading. + /// + [Property] + public float Offset { get; set; } + + /// + /// Value which scales raw units into grams. + /// Weight in grams = (Raw Reading - Offset) / CalibrationScale. + /// + [Property] + public float CalibrationScale { get; set; } + + /// + /// When set to true, CAL button will not have any effect on the current calibration setting. + /// + [Property] + public bool IgnoreCalibrationButton { get; set; } + + /// + /// When set to true, RST button will not change Offset (it won't Tare). + /// + [Property] + public bool IgnoreResetButton { get; set; } + + /// + /// Sets the weight (in grams) used for automatic calibration. + /// Value is only relevant when CAL button has been pressed. + /// + [Property] + public ushort AutomaticCalibrationWeight + { + // It does not seem to be possible to read from this register so we use the last value + get => _automaticCalibrationWeight; + set + { + _automaticCalibrationWeight = value; + WriteUInt16(Hx711I2cRegister.REG_SET_TRIGGER_WEIGHT, value); + } + } + + private ushort _automaticCalibrationWeight; + + /// + /// Sets the minimum weight in grams which will trigger calibration after CAL button is pressed. + /// This value should always be less than calibration weight. + /// Value is only relevant when CAL button has been pressed. + /// + [Property] + public ushort AutomaticCalibrationThreshold + { + // It does not seem to be possible to read from this register so we use the last value + get => _automaticCalibrationThreshold; + set + { + _automaticCalibrationThreshold = value; + WriteUInt16(Hx711I2cRegister.REG_SET_CAL_THRESHOLD, value); + } + } + + private ushort _automaticCalibrationThreshold; + + /// + /// Gets or sets the number of samples that will be taken and then averaged when performing a operation. + /// + /// + /// + /// The default value is 20 samples. + /// + /// + /// Larger value gives more accurate reading but also increases time it takes for operation to complete. + /// + /// + [Property] + public uint SampleAveraging { get; set; } = 20; + + /// + /// Gets or sets the delay after every read or write operation. + /// + /// + /// + /// The default value is 1ms. + /// + /// + /// The delay has impact on the time it takes for a operation to complete. + /// + /// + /// Too small delay may cause ocassional or persistent reading errors. + /// + /// + [Property] + public TimeSpan ReadWriteDelay { get; set; } = TimeSpan.FromMilliseconds(1); + + /// + /// Hx711I2c - DFRobot KIT0176: I2C 1kg Weight Sensor Kit + /// + public Hx711I2c(I2cDevice i2cDevice) + { + _i2c = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice)); + ResetSensor(); + } + + /// + /// Gets I2C address depending on A0 and A1 pin settings. + /// + /// Value of A0 pin. + /// Value of A1 pin. + /// Address of the device. + public static int GetI2cAddress(PinValue a0, PinValue a1) + => DefaultI2cAddress + ((bool)a0 ? 1 : 0) + ((bool)a1 ? 2 : 0); + + /// + /// Read current weight and use it as 0. Equivalent to pressing RST button. + /// + /// When set to true will also blink LED next to RST button. + [Command] + public void Tare(bool blinkLed = false) + { + if (blinkLed) + { + // this doesn't seem to set peel flag to 1 as RST button does + // so we still need to set Offset manually + WriteRegisterEmpty(Hx711I2cRegister.REG_CLICK_RST); + } + + Offset = GetRawReading(); + } + + /// + /// Re-initializes the sensor and sets arbitrarly chosen calibration values. + /// + [Command] + public void ResetSensor() + { + // This is purposefully not called "Reset" to not confuse with RST button and Tare functionality + const byte REG_CLEAR_REG_STATE = 0x65; + WriteByte(Hx711I2cRegister.REG_DATA_INIT_SENSOR, REG_CLEAR_REG_STATE); + + AutomaticCalibrationWeight = 100; + AutomaticCalibrationThreshold = 10; + + Offset = DefaultOffset; + CalibrationScale = GetAutomaticCalibrationScale(); + } + + /// + /// Gets data from sensor with scale which was determined with automatic calibration made when CAL button was pressed. + /// This value might differ from actual calibration scale used if no measurements have been made after CAL button was pressed. + /// + /// Calibration scale + public float GetAutomaticCalibrationScale() + { + // TODO: in netstandard2.0 we cannot use ReadOnlySpan overload of BitConverter + // TODO: once we drop support we should use Span overload + // TODO: once we're at 6.0 or higher we should use BinaryPrimitives.ReadSingleBigEndian + byte[] calibrationBytes = new byte[4]; + ReadRegister(Hx711I2cRegister.REG_DATA_GET_CALIBRATION, calibrationBytes); + Reverse4ByteArray(calibrationBytes); + return BitConverter.ToSingle(calibrationBytes, 0); + } + + /// + /// Equivalent to physically clicking CAL button. + /// When CAL button is pressed or this method is called the LED turns orange + /// then user needs to wait a bit (1-2 seconds) then place calibration weight. + /// Use AutomaticCalibrationWeight to set weight you use for calibration. + /// The calibration is finished when placed weight exceeds AutomaticCalibrationThreshold. + /// After that orange LED flashes 3 times and turns off. + /// It means calibration is successfully finished. + /// If flashing doesn't happen and LED turns off it means calibration didn't succeed. + /// + [Command] + public void StartCalibration() => WriteRegisterEmpty(Hx711I2cRegister.REG_CLICK_CAL); + + private int GetPeelFlag() + { + // Original code calls it Peel flag + // There is not much documentation on what this actually means + // but from testing it seems that when this register is read we + // get events from last time this was read: + // - RST was pressed + // - calibration is finished. + // Specific values are described below. + byte flag = ReadRegister(Hx711I2cRegister.REG_DATA_GET_PEEL_FLAG); + switch (flag) + { + case 1: // RST is pressed + case 129: // Unclear - python collapses 129 + return 1; + case 2: // calibration is finished + // When CAL button is pressed the LED turns orange + // then user needs to wait a bit (1-2 seconds) then place calibration weight. + // Use AutomaticCalibrationWeight to set weight you use for calibration. + // After that orange LED flashes 3 times and turns off. + // It means calibration is finished and this register will be 2. + // On unsucessful calibration (i.e. nothing was placed) there is no extra flag + // and the register is still 0. + return 2; + default: + // Nothing happened + return 0; + } + } + + /// + /// Gets weight reading. Tare should be called first. + /// + /// + [Telemetry("Weight")] + public Mass GetWeight() + { + float rawReading = GetRawReading(); + + switch (GetPeelFlag()) + { + case 1: // RST was pressed + if (!IgnoreResetButton) + { + Tare(); + return Mass.FromGrams(0); + } + + break; + case 2: // calibration is finished + if (!IgnoreCalibrationButton) + { + CalibrationScale = GetAutomaticCalibrationScale(); + } + + break; + } + + return Mass.FromGrams((rawReading - Offset) / CalibrationScale); + } + + /// + /// Gets average raw reading. + /// + /// Raw reading value + /// + /// + /// The and have direct effect on how long this operation takes to complete. + /// + /// + public float GetRawReading() + { + uint samples = SampleAveraging; + long watchdog = samples * 10; + + // Single sample is 24-bit, if user set SampleAveraging to max value + // the result will fit in 56-bit value and therefore overflow is not possible. + long sum = 0; + for (ulong i = 0; i < samples; i++) + { + uint weightGrams; + while (!TryReadSample(out weightGrams)) + { + watchdog--; + } + + if (watchdog < 0) + { + // This can happen when delay after read is too small. + throw new InvalidOperationException("Getting weight failed. Consider increasing ReadWriteDelay."); + } + + sum += weightGrams; + } + + return sum / (float)samples; + } + + private bool TryReadSample(out uint sample) + { + Span reading = stackalloc byte[4]; + ReadRegister(Hx711I2cRegister.REG_DATA_GET_RAM_DATA, reading); + + if (reading[0] == 0x12) + { + // clearing first byte so we can read UInt24 using UInt32 utility + reading[0] = 0; + sample = BinaryPrimitives.ReadUInt32BigEndian(reading) ^ 0x800000; + return true; + } + else + { + sample = default; + return false; + } + } + + private void WriteUInt16(Hx711I2cRegister register, ushort data) + { + Span bytes = stackalloc byte[3]; + bytes[0] = (byte)register; + BinaryPrimitives.WriteUInt16BigEndian(bytes.Slice(1), data); + _i2c.Write(bytes); + DelayAfterWrite(); + } + + private void WriteByte(Hx711I2cRegister register, byte data) + { + Span buff = stackalloc byte[2] + { + (byte)register, + data + }; + + _i2c.Write(buff); + DelayAfterWrite(); + } + + private void WriteRegisterEmpty(Hx711I2cRegister register) + { + _i2c.WriteByte((byte)register); + DelayAfterWrite(); + } + + private void ReadRegister(Hx711I2cRegister register, Span buffer) + { + _i2c.WriteByte((byte)register); + DelayAfterWrite(); + _i2c.Read(buffer); + } + + private byte ReadRegister(Hx711I2cRegister register) + { + _i2c.WriteByte((byte)register); + DelayAfterWrite(); + return _i2c.ReadByte(); + } + + /// + public void Dispose() + { + _i2c?.Dispose(); + _i2c = null!; + } + + private static void Reverse4ByteArray(byte[] bytes) + { + Debug.Assert(bytes.Length == 4, "bytes.Length should be 4"); + + byte tmp = bytes[0]; + bytes[0] = bytes[3]; + bytes[3] = tmp; + + tmp = bytes[1]; + bytes[1] = bytes[2]; + bytes[2] = tmp; + } + + private void DelayAfterWrite() + { + Thread.Sleep(ReadWriteDelay); + } +} diff --git a/src/devices/Hx711I2c/Hx711I2c.csproj b/src/devices/Hx711I2c/Hx711I2c.csproj new file mode 100644 index 0000000000000000000000000000000000000000..9b6809df74a6903ab509306917991a76ec721b7d --- /dev/null +++ b/src/devices/Hx711I2c/Hx711I2c.csproj @@ -0,0 +1,11 @@ + + + $(DefaultBindingTfms) + false + 10 + + + + + + \ No newline at end of file diff --git a/src/devices/Hx711I2c/Hx711I2c.sln b/src/devices/Hx711I2c/Hx711I2c.sln new file mode 100644 index 0000000000000000000000000000000000000000..162ff9331e2d5a9d7928fabbc506d52469e9cfca --- /dev/null +++ b/src/devices/Hx711I2c/Hx711I2c.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33002.27 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hx711I2c", "Hx711I2c.csproj", "{D8A8592A-E2D0-42F7-BAC0-436C3E4E09BF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hx711I2c.Samples", "samples\Hx711I2c.Samples.csproj", "{9CBDF969-540E-489E-914C-17C846FA0248}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D8A8592A-E2D0-42F7-BAC0-436C3E4E09BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8A8592A-E2D0-42F7-BAC0-436C3E4E09BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8A8592A-E2D0-42F7-BAC0-436C3E4E09BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8A8592A-E2D0-42F7-BAC0-436C3E4E09BF}.Release|Any CPU.Build.0 = Release|Any CPU + {9CBDF969-540E-489E-914C-17C846FA0248}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CBDF969-540E-489E-914C-17C846FA0248}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CBDF969-540E-489E-914C-17C846FA0248}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CBDF969-540E-489E-914C-17C846FA0248}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6740CF3D-9E09-42DA-A205-CB73970AF1FA} + EndGlobalSection +EndGlobal diff --git a/src/devices/Hx711I2c/Hx711I2cRegister.cs b/src/devices/Hx711I2c/Hx711I2cRegister.cs new file mode 100644 index 0000000000000000000000000000000000000000..72906fa0959c4a451450f0489d2d846dd4e9f36e --- /dev/null +++ b/src/devices/Hx711I2c/Hx711I2cRegister.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable + +namespace Iot.Device.Hx711 +{ + internal enum Hx711I2cRegister : byte + { + REG_DATA_GET_RAM_DATA = 0x66, + REG_DATA_GET_CALIBRATION = 0x67, + REG_DATA_GET_PEEL_FLAG = 0x69, + REG_DATA_INIT_SENSOR = 0x70, + REG_SET_CAL_THRESHOLD = 0x71, + REG_SET_TRIGGER_WEIGHT = 0x72, + + REG_CLICK_RST = 0x73, + REG_CLICK_CAL = 0x74, + } +} diff --git a/src/devices/Hx711I2c/README.md b/src/devices/Hx711I2c/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1416d17674ff3fb922501b53efbbe74530b7b4ef --- /dev/null +++ b/src/devices/Hx711I2c/README.md @@ -0,0 +1,85 @@ +# DFRobot KIT0176: I2C 1kg Weight Sensor Kit - HX711 (Gravity: I2CWeight Sensor) + +Binding for I2C interface for HX711 provided by DFRobot. +Note that this binding does not provide way to communicate with HX711 directly. + +## Documentation + +There is no official datasheet as of writing this document. Some resources with partial information are available though: + +- [DFRobot HX711 Weight Sensor Kit](https://wiki.dfrobot.com/HX711_Weight_Sensor_Kit_SKU_KIT0176) +- [C++/Python implementation](https://github.com/DFRobot/DFRobot_HX711_I2C) +- [Product website](https://www.dfrobot.com/product-2289.html) + +## How to connect + +| Name | PCB description | +| ----------------- | --------------- | +| Sensor red wire | E+ | +| Sensor black wire | E- | +| Sensor white wire | S- | +| Sensor green wire | S+ | + +## Usage + +This specific sample uses FT4222 for I2C: + +```csharp +using System; +using System.Collections.Generic; +using System.Device.I2c; +using System.Threading; +using Iot.Device.Ft4222; +using Iot.Device.Hx711; + +List ft4222s = Ft4222Device.GetFt4222(); +if (ft4222s.Count == 0) +{ + Console.WriteLine("FT4222 not plugged in"); + return; +} + +Ft4222Device ft4222 = ft4222s[0]; + +// If using Raspberry Pi rather than FT4222 following initialization method can be used instead: +// using Hx711I2c hx711 = new(I2cDevice.Create(new I2cConnectionSettings(1, Hx711I2c.DefaultI2cAddress))); +using Hx711I2c hx711 = new(ft4222.CreateI2cDevice(new I2cConnectionSettings(0, Hx711I2c.DefaultI2cAddress))); +hx711.CalibrationScale = 2236.0f; +hx711.Tare(blinkLed: true); + +// less accuracy but faster response time +hx711.SampleAveraging = 10; + +// To simulate pressing CAL button: +// hx711.StartCalibration(); +while (true) +{ + Console.WriteLine($"{hx711.GetWeight().Grams:0.0}g"); + Thread.Sleep(1000); +} +``` + +## Calibration + +- Calibration starts when CAL button is pressed or `hx711.StartCalibration()` is called +- When calibration starts orange LED will turn on +- After that wait a bit (min 1 second up to 5 seconds) and place calibration weight (100g by default) +- When weight is placed the calibration will be triggered automatically after around 1 second after placing the weight +- When calibration is successful orange LED will blink 3 times +- When calibration is not succesful orange LED will just turn off without blinking +- Calibration weight can be specified using `AutomaticCalibrationWeight` +- Weight triggering calibration (after CAL button is pressed) can be set using `AutomaticCalibrationThreshold` and is 10g by default +- `AutomaticCalibrationThreshold` should be smaller than `AutomaticCalibrationWeight` + +```csharp +hx711.StartCalibration(); + +// when orange LED blinks 3 times this value can be read and assigned: +hx711.CalibrationScale = GetAutomaticCalibrationScale(); + +// The assignment is optional - it will also happen automatically when hx711.GetWeight() is called +``` + +## Auto-calibration + +[See official documentation how to use CAL button](https://wiki.dfrobot.com/HX711_Weight_Sensor_Kit_SKU_KIT0176#target_9) diff --git a/src/devices/Hx711I2c/category.txt b/src/devices/Hx711I2c/category.txt new file mode 100644 index 0000000000000000000000000000000000000000..94c3c4bac3399b65e785a637ab4ac4040c08cea5 --- /dev/null +++ b/src/devices/Hx711I2c/category.txt @@ -0,0 +1 @@ +weight diff --git a/src/devices/Hx711I2c/samples/Hx711I2c.Samples.csproj b/src/devices/Hx711I2c/samples/Hx711I2c.Samples.csproj new file mode 100644 index 0000000000000000000000000000000000000000..60c187bf259514abf29647ca3ca28216ae995a61 --- /dev/null +++ b/src/devices/Hx711I2c/samples/Hx711I2c.Samples.csproj @@ -0,0 +1,11 @@ + + + Exe + $(DefaultSampleTfms) + + + + + + + \ No newline at end of file diff --git a/src/devices/Hx711I2c/samples/Program.cs b/src/devices/Hx711I2c/samples/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..6cd551281c33d6ec1525f39aaab50c1c55e42124 --- /dev/null +++ b/src/devices/Hx711I2c/samples/Program.cs @@ -0,0 +1,35 @@ +// 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.Collections.Generic; +using System.Device.I2c; +using System.Threading; +using Iot.Device.Ft4222; +using Iot.Device.Hx711; + +List ft4222s = Ft4222Device.GetFt4222(); +if (ft4222s.Count == 0) +{ + Console.WriteLine("FT4222 not plugged in"); + return; +} + +Ft4222Device ft4222 = ft4222s[0]; + +// If using Raspberry Pi rather than FT4222 following initialization method can be used instead: +// using Hx711I2c hx711 = new(I2cDevice.Create(new I2cConnectionSettings(1, Hx711I2c.DefaultI2cAddress))); +using Hx711I2c hx711 = new(ft4222.CreateI2cDevice(new I2cConnectionSettings(0, Hx711I2c.DefaultI2cAddress))); +hx711.CalibrationScale = 2236.0f; +hx711.Tare(blinkLed: true); + +// less accuracy but faster response time +hx711.SampleAveraging = 10; + +// To simulate pressing CAL button: +// hx711.StartCalibration(); +while (true) +{ + Console.WriteLine($"{hx711.GetWeight().Grams:0.0}g"); + Thread.Sleep(1000); +}