Skip to content

Commit

Permalink
Add binding for generic HX711 (load cell amplifier and 24-Bit ADC Mod…
Browse files Browse the repository at this point in the history
…ule) driver (#1994)

* Add Hx711 device

- Add Hx711 options
- Add `SetCalibration` method
- Fix weight reading process

---------

Co-authored-by: Matteo Tosi <matteo.tosi@ericsoft.com>
Co-authored-by: Matteo Tosi <Matteo.Tosi@zucchetti.it>
Co-authored-by: Matteo Tosi <matteo.tosi@euris.it>
  • Loading branch information
4 people committed Jul 16, 2023
1 parent 2291697 commit 489759a
Show file tree
Hide file tree
Showing 12 changed files with 938 additions and 0 deletions.
21 changes: 21 additions & 0 deletions src/devices/Hx711/ByteFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Iot.Device.Hx711
{
/// <summary>
/// Byte order ("endianness") in an architecture
/// </summary>
internal enum ByteFormat
{
/// <summary>
/// Less Significant Bit (aka Little-endian) byte format sequence
/// </summary>
Lsb = 0,

/// <summary>
/// Most Significant Bit (aka Big-endian) byte format sequence
/// </summary>
Msb = 1,
}
}
273 changes: 273 additions & 0 deletions src/devices/Hx711/HX711.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
// 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;
using System.Device.Gpio;
using System.Linq;
using UnitsNet;

namespace Iot.Device.Hx711
{
/// <summary>
/// Hx711 - Weight scale Module
/// </summary>
public sealed class Hx711 : IDisposable
{
private readonly GpioController _gpioController;
private readonly bool _shouldDispose;

private readonly int _pinDout;
private readonly int _pinPdSck;

private readonly Hx711Options _options;
private readonly object _readLock;
private readonly Hx711Reader _reader;

private readonly List<double> _conversionRatioList = new();

private bool _isInitialize;
private bool _isCalibrated;
private int _tareValue;

/// <summary>
/// Offset value from 0 at startup
/// </summary>
private int _offsetFormZero;

/// <summary>
/// Conversion ratio between Hx711 units and grams
/// </summary>
public double ConversionRatio { get; private set; }

/// <summary>
/// Weight set as tare
/// </summary>
public Mass TareValue
{
get
{
return ConversionRatio == 0 ? Mass.FromGrams(_tareValue) : Mass.FromGrams(_tareValue / ConversionRatio);
}
}

/// <summary>
/// Creates a new instance of the Hx711 module.
/// </summary>
/// <param name="pinDout">Trigger pulse output. (Digital OUTput)</param>
/// <param name="pinPdSck">Trigger pulse input. (Power Down control and Serial Clock input)</param>
/// <param name="options">How to use the Hx711 module.</param>
/// <param name="gpioController">GPIO controller related with the pins.</param>
/// <param name="pinNumberingScheme">Scheme and numeration used by controller.</param>
/// <param name="shouldDispose">True to dispose the Gpio Controller.</param>
public Hx711(int pinDout, int pinPdSck, Hx711Options? options = null, GpioController? gpioController = null,
PinNumberingScheme pinNumberingScheme = PinNumberingScheme.Logical, bool shouldDispose = true)
{
_pinDout = pinDout;
_pinPdSck = pinPdSck;

_shouldDispose = shouldDispose || gpioController is null;
_gpioController = gpioController ?? new(pinNumberingScheme);

// Mutex for reading from the Hx711, in case multiple threads in client
// software try to access get values from the class at the same time.
_readLock = new object();

_gpioController.OpenPin(_pinPdSck, PinMode.Output);
_gpioController.OpenPin(_pinDout, PinMode.Input);

// Needed initialize
_isInitialize = false;

// Needed calibration
_isCalibrated = false;

_offsetFormZero = 0;
_tareValue = 0;
ConversionRatio = 0;

_options = options ?? new Hx711Options();

_reader = new Hx711Reader(_gpioController, _options, pinDout, pinPdSck, _readLock);
}

/// <summary>
/// Load cells always return different values also based on their range and sensitivity.
/// For this reason, a first calibration step with a known weight is required.
/// You can repeat it several times to get a more precise value.
/// </summary>
/// <param name="knowWeight">Known weight currently on load cell and detected by the Hx711.</param>
/// <param name="numberOfReads">Number of readings to take from which to average, to get a more accurate value.</param>
/// <exception cref="ArgumentOutOfRangeException">Throw if know weight have invalid value</exception>
public void SetCalibration(Mass knowWeight, int numberOfReads = 15)
{
if (knowWeight.Grams == 0)
{
throw new ArgumentOutOfRangeException(paramName: nameof(knowWeight), message: "Param value must be greater than zero!");
}

var readValue = _reader.Read(numberOfReads, _offsetFormZero);

lock (_readLock)
{
var referenceValue = readValue / knowWeight.Grams;

// If we do several calibrations, the most accurate value is the average.
_conversionRatioList.Add(referenceValue);
ConversionRatio = _conversionRatioList.Average();

_isCalibrated = true;
}
}

/// <summary>
/// If you already know the reference unit between the Hx711 value and grams, you can set it and skip the calibration.
/// </summary>
/// <param name="conversionRatio">Conversion ratio between Hx711 units and grams</param>
/// <exception cref="ArgumentOutOfRangeException">Throw if know weight have invalid value</exception>
public void SetConversionRatio(double conversionRatio)
{
if (conversionRatio == 0)
{
throw new ArgumentOutOfRangeException(paramName: nameof(conversionRatio), message: "Param value must be greater than zero!");
}

lock (_readLock)
{
ConversionRatio = conversionRatio;

// Calibration no longer required
_isCalibrated = true;
}
}

/// <summary>
/// Read the weight from the Hx711 through channel A to which the load cell is connected,
/// range and precision depend on load cell connected.
/// </summary>
/// <param name="numberOfReads">Number of readings to take from which to average, to get a more accurate value.</param>
/// <returns>Return a weigh read from Hx711</returns>
public Mass GetWeight(int numberOfReads = 3)
{
if (!_isCalibrated)
{
throw new Hx711CalibrationNotDoneException();
}

// Lock is internal in fisical read
var value = GetNetWeight(numberOfReads);

return Mass.FromGrams(Math.Round(value / ConversionRatio, digits: 0));
}

/// <summary>
/// Sets tare for channel A for compatibility purposes
/// </summary>
/// <param name="numberOfReads">Number of readings to take from which to average, to get a more accurate value.</param>
public void Tare(int numberOfReads = 15)
{
lock (_readLock)
{
if (!_isCalibrated)
{
throw new Hx711CalibrationNotDoneException();
}

_tareValue = GetNetWeight(numberOfReads);
}
}

/// <summary>
/// Power up Hx711 and set it ready to work
/// </summary>
public void PowerUp()
{
// Wait for and get the Read Lock, incase another thread is already
// driving the Hx711 serial interface.
lock (_readLock)
{
// Lower the Hx711 Digital Serial Clock (PD_SCK) line.
// Docs says "When PD_SCK Input is low, chip is in normal working mode."
// page 5
// https://html.alldatasheet.com/html-pdf/1132222/AVIA/Hx711/573/5/Hx711.html
_gpioController.Write(_pinPdSck, PinValue.Low);

// Wait 100µs for the Hx711 to power back up.
DelayHelper.DelayMicroseconds(microseconds: 100, allowThreadYield: true);

// Release the Read Lock, now that we've finished driving the Hx711
// serial interface.
}

// Hx711 will now be defaulted to Channel A with gain of 128. If this
// isn't what client software has requested from us, take a sample and
// throw it away, so that next sample from the Hx711 will be from the
// correct channel/gain.
if (_options.Mode != Hx711Mode.ChannelAGain128 || !_isInitialize)
{
_ = _reader.Read(numberOfReads: 15, offsetFromZero: 0);
_isInitialize = true;
}

// Read offset from 0
var value = _reader.Read(numberOfReads: 15, offsetFromZero: 0);
_offsetFormZero = value;
}

/// <summary>
/// Power down Hx711
/// </summary>
public void PowerDown()
{
// Wait for and get the Read Lock, incase another thread is already
// driving the Hx711 serial interface.
lock (_readLock)
{
// Cause a rising edge on Hx711 Digital Serial Clock (PD_SCK). We then
// leave it held up and wait 100µs. After 60µs the Hx711 should be
// powered down.
// Docs says "When PD_SCK pin changes from low to high
// and stays at high for longer than 60µs, Hx711
// enters power down mode", page 5 https://html.alldatasheet.com/html-pdf/1132222/AVIA/Hx711/573/5/Hx711.html
_gpioController.Write(_pinPdSck, PinValue.Low);
_gpioController.Write(_pinPdSck, PinValue.High);

DelayHelper.DelayMicroseconds(microseconds: 65, allowThreadYield: true);

// Release the Read Lock, now that we've finished driving the Hx711
// serial interface.
}
}

/// <summary>
/// PowerDown and restart component
/// </summary>
public void Reset()
{
PowerDown();
PowerUp();
}

/// <inheritdoc />
public void Dispose()
{
if (_shouldDispose)
{
_gpioController?.Dispose();
}
else
{
_gpioController?.ClosePin(_pinPdSck);
_gpioController?.ClosePin(_pinDout);
}
}

/// <summary>
/// Read weight from Hx711
/// </summary>
/// <param name="numberOfReads">Number of readings to take from which to average, to get a more accurate value.</param>
/// <returns>Return total weight - tare weight</returns>
private int GetNetWeight(int numberOfReads) => _reader.Read(numberOfReads, _offsetFormZero) - _tareValue;
}
}
11 changes: 11 additions & 0 deletions src/devices/Hx711/HX711.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(DefaultBindingTfms)</TargetFrameworks>
<EnableDefaultItems>false</EnableDefaultItems>
<LangVersion>10</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="*.cs" />
<None Include="README.md" />
</ItemGroup>
</Project>
32 changes: 32 additions & 0 deletions src/devices/Hx711/HX711CalibrationNotDoneException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Iot.Device.Hx711
{
/// <summary>
/// Exception thorw if Hx711 miss calibration process
/// </summary>
public class Hx711CalibrationNotDoneException : Exception
{
private new const string Message = "Hx711 component need a calibration process first.";

/// <summary>
/// Initializes a new instance of the <see cref="Hx711CalibrationNotDoneException"/> class.
/// </summary>
public Hx711CalibrationNotDoneException()
: base(message: Message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="Hx711CalibrationNotDoneException"/> class.
/// </summary>
/// <param name="inner">The exception that is the cause of the current exception.</param>
public Hx711CalibrationNotDoneException(Exception inner)
: base(message: Message, inner)
{
}
}
}
44 changes: 44 additions & 0 deletions src/devices/Hx711/HX711Options.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Iot.Device.Hx711
{
/// <summary>
/// Hx711 options for all manufacturers
/// </summary>
public sealed class Hx711Options
{
/// <summary>
/// Hx711 has 3 modes of operation, choose the one based on the fisical connection with load cell.
/// Default value: <code>Mode = Hx711Mode.ChannelAGain128</code>
/// </summary>
public Hx711Mode Mode { get; private set; }

/// <summary>
/// If <code>true</code> bytes read from Hx711 made by Lsb format.
/// Some Hx711 manufacturers return bytes in Lsb, but most in Msb.
/// Default value: <code>UseByteLittleEndian = false</code>
/// </summary>
public bool UseByteLittleEndian { get; private set; }

/// <summary>
/// Initializes a new instance of the <see cref="Hx711Options"/> class with default values.
/// </summary>
public Hx711Options()
{
Mode = Hx711Mode.ChannelAGain128;
UseByteLittleEndian = false;
}

/// <summary>
/// Initializes a new instance of the <see cref="Hx711Options"/> class.
/// </summary>
/// <param name="mode">Hx711 has 3 modes of operation, choose the one based on the fisical connection with load cell.</param>
/// <param name="useByteLittleEndian">If <code>true</code> bytes read from Hx711 made by Lsb format.</param>
public Hx711Options(Hx711Mode mode, bool useByteLittleEndian)
{
Mode = mode;
UseByteLittleEndian = useByteLittleEndian;
}
}
}
Loading

0 comments on commit 489759a

Please sign in to comment.