diff --git a/src/devices/Common/Common.csproj b/src/devices/Common/Common.csproj index 4dbd080868..b0297d729f 100644 --- a/src/devices/Common/Common.csproj +++ b/src/devices/Common/Common.csproj @@ -12,6 +12,7 @@ + diff --git a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs new file mode 100644 index 0000000000..928bf5be22 --- /dev/null +++ b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs @@ -0,0 +1,285 @@ +// 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.Globalization; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using UnitsNet; + +namespace System.Device.I2c; + +/// +/// This class can be used to create a simulated I2C device. +/// Derive from it and implement the and commands +/// to behave as expected. +/// Can also serve as base for a testing mock. +/// +public abstract class I2cSimulatedDeviceBase : I2cDevice +{ + private bool _disposed; + private Dictionary _registerMap; + private byte _currentRegister; + + /// + /// Default constructor + /// + /// The connection settings for this device. + public I2cSimulatedDeviceBase(I2cConnectionSettings settings) + { + ConnectionSettings = settings; + _registerMap = new Dictionary(); + _disposed = false; + _currentRegister = 0; + } + + /// + /// The registermap of this device. + /// This should only be accessed from a derived class, except for test purposes. + /// + public Dictionary RegisterMap => _registerMap; + + /// + /// The active connection settings + /// + public override I2cConnectionSettings ConnectionSettings { get; } + + /// + /// The active register. + /// Can be set to mimic some non-standard behavior of setting a register (or if reading increases + /// the register pointer, which is the case on some chips) + /// + protected byte CurrentRegister + { + get + { + return _currentRegister; + } + set + { + _currentRegister = value; + } + } + + /// + /// Reads a byte from the bus + /// + /// + /// The instance is disposed already + /// + public override byte ReadByte() + { + if (_disposed) + { + throw new ObjectDisposedException("This instance is disposed"); + } + + byte[] buffer = new byte[1]; + if (WriteRead([], buffer) == 1) + { + return buffer[0]; + } + + throw new IOException("Unable to read a byte from the device"); + } + + /// + /// This method implements the read operation from the device. + /// + /// Buffer with input data to the device, buffer[0] is usually the command byte + /// The return data from the device + /// How many bytes where read. Should usually match the length of the output buffer + /// This doesn't use as argument type to be mockable. Be sure + /// to use this method in mocks, not any that take or , as that + /// will cause runtime exceptions + public abstract int WriteRead(byte[] inputBuffer, byte[] outputBuffer); + + /// + public override void Read(Span buffer) + { + byte[] buffer2 = buffer.ToArray(); + if (WriteRead([], buffer2) == buffer.Length) + { + buffer2.CopyTo(buffer); + return; + } + + throw new IOException($"Unable to read {buffer.Length} bytes from the device"); + } + + /// + public override void WriteByte(byte value) + { + if (WriteRead([value], []) == 1) + { + return; + } + + throw new IOException("Unable to write a byte to the device"); + } + + /// + public override void Write(ReadOnlySpan buffer) + { + WriteRead(buffer.ToArray(), []); + } + + /// + public override void WriteRead(ReadOnlySpan writeBuffer, Span readBuffer) + { + byte[] outBuffer = new byte[readBuffer.Length]; + if (WriteRead(writeBuffer.ToArray(), outBuffer) != readBuffer.Length) + { + throw new IOException($"Unable to read {readBuffer.Length} bytes from the device"); + } + + outBuffer.CopyTo(readBuffer); + } + + /// + protected override void Dispose(bool disposing) + { + _disposed = true; + base.Dispose(disposing); + } + + /// + public override ComponentInformation QueryComponentInformation() + { + var self = new ComponentInformation(this, "Simulated I2C Device"); + self.Properties["BusNo"] = ConnectionSettings.BusId.ToString(CultureInfo.InvariantCulture); + self.Properties["DeviceAddress"] = $"0x{ConnectionSettings.DeviceAddress:x2}"; + return self; + } + + /// + /// Base class for generic register access + /// + public abstract record class RegisterBase : IComparable + { + /// + /// Writes the register, regardless of its actual type + /// + /// The value to write + public abstract void WriteRegister(int value); + + /// + /// Reads the register value regardless of its actual type + /// + /// The register value, sign-extended to int + public abstract int ReadRegister(); + + /// + public abstract int CompareTo(object? obj); + } + + /// + /// Represents a register value + /// + /// Size of the register, usually byte or int + public record class Register : RegisterBase + where T : struct, IEquatable, INumber, IComparable + { + /// + /// Event that is raised when the register is written + /// + private readonly Func? _registerUpdateHandler; + + /// + /// Event that is raised to read the register. Gets the internal value of the register + /// and returns the value the client should see (e.g a random measurement value) + /// + private readonly Func? _registerReadHandler; + + private T _value; + + /// + /// Create a new register + /// + public Register() + : this(default(T)) + { + } + + /// + /// Creates a new register + /// + /// The initial (power-on-reset) value of the register + public Register(T initialValue) + { + _value = initialValue; + } + + /// + /// Creates a new register with handlers + /// + /// The initial value of the register at power-up + /// A handler for a register write. Can be null. + /// A handler for a register read. Can be null. + public Register(T initialValue, Func? updateHandler, Func? readHandler) + { + _value = initialValue; + _registerUpdateHandler = updateHandler; + _registerReadHandler = readHandler; + } + + /// + /// The current value of the register + /// + public T Value + { + get + { + if (_registerReadHandler != null) + { + return _registerReadHandler(_value); + } + + return _value; + } + set + { + if (_registerUpdateHandler != null) + { + _value = _registerUpdateHandler(value); + return; + } + + _value = value; + } + } + + /// + public override void WriteRegister(int value) + { + Value = T.CreateChecked(value); + } + + /// + public override int ReadRegister() + { + return int.CreateChecked(Value); + } + + /// + public override int CompareTo(object? obj) + { + if (obj == null) + { + return 1; + } + + if (obj is Register t1) + { + return _value.CompareTo(t1._value); + } + + throw new ArgumentException("These types can't be compared"); + } + } +} diff --git a/src/devices/Ina236/Ina236.cs b/src/devices/Ina236/Ina236.cs new file mode 100644 index 0000000000..3f20bee6cf --- /dev/null +++ b/src/devices/Ina236/Ina236.cs @@ -0,0 +1,369 @@ +// 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.Collections.Generic; +using System.Device; +using System.Device.I2c; +using System.Device.Model; +using UnitsNet; +using static System.Math; + +namespace Iot.Device.Adc +{ + /// + /// INA236 Bidirectional Current/Power monitor. + /// + /// The INA236 current shunt and power monitor with an I2C interface. + /// The INA236 monitors both shunt drop and supply voltage, with programmable conversion + /// times and filtering. A programmable calibration value, combined with an internal multiplier, + /// enables direct readouts in amperes. An additional multiplying register calculates power in watts. + /// + /// + [Interface("INA236 Bidirectional Current/Power monitor")] + public class Ina236 : IDisposable + { + /// + /// The default I2C Address for this device. + /// According to the datasheet, the device comes in two variants, A and B. + /// Type A has addresses 0x40 to 0x43, depending on whether the ADDR pin is connected to GND, VS, SDA or SCL. + /// Type B has addresses 0x44 to 0x47, depending on the ADDR pin. + /// + public const int DefaultI2cAddress = 0x40; + private I2cDevice _i2cDevice; + private ElectricResistance _shuntResistance; + private ElectricCurrent _currentLsb; + private ElectricCurrent _maxCurrent; + private ElectricPotential _voltageLsb; + + /// + /// Construct an Ina236 device using an I2cDevice + /// + /// The I2cDevice initialized to communicate with the INA236. + /// Maximum expected current. Typical values are 8-10 Amps. + /// The resistance of the shunt between input and output. + /// Example breakout boards from manufacturers such as Adafruit have 0.008 Ohms, so try this + /// value if you are unsure. + /// The power dissipation at the resistor is P = I*I*R at the maximum current. + public Ina236(I2cDevice i2cDevice, ElectricResistance shuntResistance, ElectricCurrent maxCurrent) + { + _i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice)); + _shuntResistance = shuntResistance; + if (_shuntResistance <= ElectricResistance.Zero) + { + throw new ArgumentOutOfRangeException(nameof(shuntResistance), "The shuntResistance parameter must be greater than zero"); + } + + _maxCurrent = maxCurrent; + + Reset(_shuntResistance, _maxCurrent); + } + + /// + /// Reset the INA236 to default values; + /// + [Command] + public void Reset(ElectricResistance shuntResistance, ElectricCurrent current) + { + // Reset the device by sending a value to the configuration register with the reset bit set. + WriteRegister(Ina236Register.Configuration, 0x8000); + + ushort deviceId = ReadRegisterUnsigned(Ina236Register.DeviceId); + + if ((deviceId & 0xFFF0) != 0xa080) + { + throw new InvalidOperationException($"The device on I2C address {_i2cDevice.ConnectionSettings.DeviceAddress} doesn't seem to be an INA236. Device ID was {deviceId:X4} instead of 0xA080"); + } + + // See datasheet. Use twice the value to allow later rounding + ElectricCurrent currentLsbMinimum = current / Pow(2.0, 15) * 2; + double exactCalibrationValue = 0.00512 / (currentLsbMinimum.Amperes * shuntResistance.Ohms); + int valueToSet = (int)exactCalibrationValue; + if (valueToSet > 0xFFFF) + { + throw new InvalidOperationException("Invalid combination of settings - the calibration value is out of spec"); + } + + // Reverse calculation, to get the exact lsb + // Solve this for x: + // calibrationValue = 0.00512 / (x * resistance) // * (x * resistance) + // calibrationValue * (x * resistance) = 0.00512 // / resistance + // calibrationValue * x = 0.00512 / resistance // / calibrationvalue + // x = 0.00512 / resistance / calibrationValue + double exactLsbMinimum = 0.00512 / shuntResistance.Ohms / valueToSet; + + _currentLsb = ElectricCurrent.FromAmperes(exactLsbMinimum); + + // The LSB of the shunt voltage register is 2.5uV if ADCRANGE==0, otherwise it's 625nV. We currently + // do not support ADCRANGE=1, to keep things simple. + _voltageLsb = ElectricPotential.FromMicrovolts(2.5); + WriteRegister(Ina236Register.Calibration, (ushort)valueToSet); + } + + /// + /// Property representing the Operating mode of the INA236 + /// + /// + /// This allows the user to selects continuous, triggered, or power-down mode of operation along with which of the shunt and bus voltage measurements are made. + /// + [Property] + public Ina236OperatingMode OperatingMode + { + get + { + return (Ina236OperatingMode)(ReadRegisterUnsigned(Ina236Register.Configuration) & (ushort)Ina236OperatingMode.ModeMask); + } + set + { + int regValue = ReadRegisterUnsigned(Ina236Register.Configuration); + + regValue &= ~0b111; + regValue |= (int)value; + + WriteRegister(Ina236Register.Configuration, (ushort)regValue); + } + } + + /// + /// How many samples should be combined into one result. + /// A high value returns less often a new reading, but is more stable. + /// + /// Valid values are: 1, 4, 16, 64, 128, 256, 512 and 1024. Other values will be rounded accordingly. + /// + /// + [Property] + public uint AverageOverNoSamples + { + get + { + int reg = (ReadRegisterUnsigned(Ina236Register.Configuration) >> 9) & 0x7; + return reg switch + { + 0b000 => 1, + 0b001 => 4, + 0b010 => 16, + 0b011 => 64, + 0b100 => 128, + 0b101 => 256, + 0b110 => 512, + 0b111 => 1024, + _ => throw new InvalidOperationException("This is not possible") + }; + } + set + { + int valueToSet = value switch + { + <= 1 => 0b000, + <= 4 => 0b001, + <= 16 => 0b010, + <= 64 => 0b011, + <= 128 => 0b100, + <= 256 => 0b101, + <= 512 => 0b110, + >= 513 => 0b111, + }; + + int reg = ReadRegisterUnsigned(Ina236Register.Configuration) & 0xF1FF; + reg = reg | (valueToSet << 9); + WriteRegister(Ina236Register.Configuration, (ushort)reg); + } + } + + /// + /// Conversion time for a single bus value, in microseconds + /// + /// Valid values are: 140, 204, 332, 588, 1100 (default), 2116, 4156 and 8244us. Other values will be rounded + public int BusConversionTime + { + get + { + int reg = (ReadRegisterUnsigned(Ina236Register.Configuration) >> 6) & 0x7; + return ConversionPeriodFromValue(reg); + } + + set + { + int valueToSet = ValueFromConversionPeriod(value); + int reg = ReadRegisterUnsigned(Ina236Register.Configuration) & 0xFE3F; + reg = reg | (valueToSet << 6); + WriteRegister(Ina236Register.Configuration, (ushort)reg); + } + } + + /// + /// Conversion time for a single shunt value, in microseconds + /// + /// Valid values are: 140, 204, 332, 588, 1100 (default), 2116, 4156 and 8244us. Other values will be rounded + public int ShuntConversionTime + { + get + { + int reg = (ReadRegisterUnsigned(Ina236Register.Configuration) >> 3) & 0x7; + return ConversionPeriodFromValue(reg); + } + + set + { + int valueToSet = ValueFromConversionPeriod(value); + int reg = ReadRegisterUnsigned(Ina236Register.Configuration) & 0xFFC7; + reg = reg | (valueToSet << 3); + WriteRegister(Ina236Register.Configuration, (ushort)reg); + } + } + + /// + /// Converts the given conversion period in us into the binary equivalent + /// + /// Period in microseconds + /// An integer value to be set to the register (with appropriate shift, used for VBUSCT and VSHCT + /// in the configuration register) + private int ValueFromConversionPeriod(int period) + { + return period switch + { + <= 140 => 0b000, + <= 204 => 0b001, + <= 332 => 0b010, + <= 588 => 0b011, + <= 1100 => 0b100, + <= 2116 => 0b101, + <= 4156 => 0b110, + >= 4157 => 0b111 + }; + } + + /// + /// Inverse of the above + /// + /// The time period + /// The bit value for the conversion register + private int ConversionPeriodFromValue(int period) + { + return period switch + { + 0b000 => 140, + 0b001 => 204, + 0b010 => 332, + 0b011 => 588, + 0b100 => 1100, + 0b101 => 2116, + 0b110 => 4156, + 0b111 => 8244, + _ => throw new InvalidOperationException("This cannot really happen") + }; + } + + /// + /// Dispose instance + /// + public void Dispose() + { + _i2cDevice?.Dispose(); + _i2cDevice = null!; + } + + /// + /// Read the measured shunt voltage. + /// + /// The shunt potential difference + /// The LSB is 2.5uV when ADCRANGE=0 + [Telemetry("ShuntVoltage")] + public ElectricPotential ReadShuntVoltage() + { + return ReadRegisterUnsigned(Ina236Register.ShuntVoltage) * _voltageLsb; + } + + /// + /// Read the measured Bus voltage. + /// This is the voltage on the primary side of the shunt. + /// + /// The Bus potential (voltage) + /// The LSB is 1.6mV. + [Telemetry("BusVoltage")] + public ElectricPotential ReadBusVoltage() + { + return ReadRegisterUnsigned(Ina236Register.BusVoltage) * ElectricPotential.FromMillivolts(1.6); + } + + /// + /// Read the calculated current through the INA236. + /// + /// + /// This value is determined by an internal calculation using the calibration register and the read shunt voltage and then scaled. + /// The value can be negative, when power flows to the bus. + /// + /// The calculated current + [Telemetry("Current")] + public ElectricCurrent ReadCurrent() + { + return ReadRegisterSigned(Ina236Register.Current) * _currentLsb; + } + + /// + /// Reads the current power consumed by the attached device. + /// + /// The power being used + /// Clarify whether this register is signed or unsigned. Since it is the product of the current and the bus voltage + /// registers, it should be possible to get a negative value, but the documentation says it's always positive + [Telemetry("Power")] + public Power ReadPower() + { + return Power.FromWatts(ReadRegisterUnsigned(Ina236Register.Power) * _currentLsb.Amperes * 32); + } + + /// + /// Read a register from the INA236 device + /// + /// The register to read. + /// Am unsiged short integer representing the regsiter contents. + private ushort ReadRegisterUnsigned(Ina236Register register) + { + Span buffer = stackalloc byte[2]; + + byte registerNumber = (byte)register; + // set a value in the buffer representing the register that we want to read and send it to the INA219 + _i2cDevice.WriteRead(new ReadOnlySpan(ref registerNumber), buffer); + + // massage the big endian value read from the INA219 unto a ushort. + return BinaryPrimitives.ReadUInt16BigEndian(buffer); + } + + /// + /// Read a register from the INA236 device + /// + /// The register to read. + /// A signed short integer representing the regsiter contents. + private short ReadRegisterSigned(Ina236Register register) + { + Span buffer = stackalloc byte[2]; + + byte registerNumber = (byte)register; + // set a value in the buffer representing the register that we want to read and send it to the INA219 + _i2cDevice.WriteRead(new ReadOnlySpan(ref registerNumber), buffer); + + // massage the big endian value read from the INA219 unto a ushort. + return BinaryPrimitives.ReadInt16BigEndian(buffer); + } + + /// + /// Write a value to an INA236 register. + /// + /// The register to be written to. + /// The value to be written to the register. + private void WriteRegister(Ina236Register register, ushort value) + { + Span buffer = stackalloc byte[3]; + + // set the first byte of the buffer to the register to be written + buffer[0] = (byte)register; + + // write the value to be written to the second and third bytes in big-endian order. + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(1, 2), value); + + // write the value to the register via the I2c Bus. + _i2cDevice.Write(buffer); + } + } +} diff --git a/src/devices/Ina236/Ina236.csproj b/src/devices/Ina236/Ina236.csproj new file mode 100644 index 0000000000..b116cbab81 --- /dev/null +++ b/src/devices/Ina236/Ina236.csproj @@ -0,0 +1,11 @@ + + + $(DefaultBindingTfms) + + false + + + + + + \ No newline at end of file diff --git a/src/devices/Ina236/Ina236.sln b/src/devices/Ina236/Ina236.sln new file mode 100644 index 0000000000..57c4f40ceb --- /dev/null +++ b/src/devices/Ina236/Ina236.sln @@ -0,0 +1,101 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36603.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{0D478BAB-AFEA-4AF1-866C-E3AC32E11C5A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ina236.Samples", "samples\Ina236.Samples.csproj", "{D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ina236", "Ina236.csproj", "{1398DC15-97F0-4048-965A-CCB3D44BFE06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arduino", "..\Arduino\Arduino.csproj", "{5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ina236.Tests", "tests\Ina236.Tests.csproj", "{7328E5E9-483A-00C5-2E02-D2796B496CD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "..\Common\Common.csproj", "{9F0601AB-EA31-A20F-1B21-30C901BDC579}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|x64.Build.0 = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|x86.Build.0 = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|Any CPU.Build.0 = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|x64.ActiveCfg = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|x64.Build.0 = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|x86.ActiveCfg = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|x86.Build.0 = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|x64.ActiveCfg = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|x64.Build.0 = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|x86.ActiveCfg = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|x86.Build.0 = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|Any CPU.Build.0 = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x64.ActiveCfg = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x64.Build.0 = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x86.ActiveCfg = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x86.Build.0 = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|x64.ActiveCfg = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|x64.Build.0 = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|x86.ActiveCfg = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|x86.Build.0 = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|Any CPU.Build.0 = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x64.ActiveCfg = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x64.Build.0 = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x86.ActiveCfg = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x86.Build.0 = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|x64.Build.0 = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|x86.Build.0 = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|Any CPU.Build.0 = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|x64.ActiveCfg = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|x64.Build.0 = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|x86.ActiveCfg = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|x86.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x64.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x64.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x86.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x86.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|Any CPU.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x64.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x64.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x86.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6} = {0D478BAB-AFEA-4AF1-866C-E3AC32E11C5A} + {7328E5E9-483A-00C5-2E02-D2796B496CD2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {34E688F0-222B-4C21-A3C2-B6C54D9D78BF} + EndGlobalSection +EndGlobal diff --git a/src/devices/Ina236/Ina236OperatingMode.cs b/src/devices/Ina236/Ina236OperatingMode.cs new file mode 100644 index 0000000000..42d6627598 --- /dev/null +++ b/src/devices/Ina236/Ina236OperatingMode.cs @@ -0,0 +1,57 @@ +// 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.Adc +{ + /// + /// Current device operating mode + /// + public enum Ina236OperatingMode + { + /// + /// The device is off + /// + Shutdown = 0, + + /// + /// Generate a single measurement of the shunt voltage value, then wait for a command again + /// + SingeShuntVoltage = 0b001, + + /// + /// Generate s single measurement of the shunt voltage value, then wait for a command again + /// + SingleBusVoltage = 0b010, + + /// + /// Generate a single measurement of both bus voltage and shunt voltage, then wait for a command again + /// + SingleShuntAndBusVoltage = 0b011, + + /// + /// Enter shutdown mode + /// + Shutdown2 = 0b100, + + /// + /// Continuously measure the shunt voltage + /// + ContinuousShuntVoltage = 0b101, + + /// + /// Continuously measure the bus voltage + /// + ContinuousBusVoltage = 0b110, + + /// + /// Continuously measure both bus and shut voltages. + /// This is the default setting. + /// + ContinuousShuntAndBusVoltage = 0b111, + + /// + /// A mask field + /// + ModeMask = 0b111 + } +} diff --git a/src/devices/Ina236/Ina236Register.cs b/src/devices/Ina236/Ina236Register.cs new file mode 100644 index 0000000000..6f9622a720 --- /dev/null +++ b/src/devices/Ina236/Ina236Register.cs @@ -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.Adc; + +/// +/// The Ina236 Register map. Note that all registers are 16 bit wide. +/// +internal enum Ina236Register +{ + Configuration = 0, + ShuntVoltage = 1, + BusVoltage = 2, + Power = 3, + Current = 4, + Calibration = 5, + MaskEnable = 6, + AlertLimit = 7, + ManufacturerId = 0x3E, + DeviceId = 0x3F +} diff --git a/src/devices/Ina236/README.md b/src/devices/Ina236/README.md new file mode 100644 index 0000000000..183c8da84a --- /dev/null +++ b/src/devices/Ina236/README.md @@ -0,0 +1,47 @@ +# INA219 - Bidirectional Current/Power Monitor + +The INA236 is a current shunt and power monitor with an I2C-compatible interface. It is an improved version of the INA219 with a higher accuracy and an extra voltage sensor for the secondary side. The device monitors both shunt voltage drop and bus supply voltage, with programmable conversion times and filtering. A programmable calibration value, combined with an internal multiplier, enables direct readouts of current in amperes. An additional multiplying register calculates power in watts. + +* Senses Bus Voltages from 0 to 26 V +* Reports Current, Voltage, and Power +* 16 Programmable Addresses +* High Accuracy: 0.5% (Maximum) Over Temperature +* Filtering Options +* Calibration Registers +* Two variants available: Address range 0x40-0x43 or 0x60-0x63 + +## Documentation + +* [INA236 Datasheet](http://www.ti.com/lit/ds/symlink/ina236.pdf) + +## Usage + +```csharp +const byte Adafruit_Ina236_I2cAddress = 0x40; + +// create an INA236 device on I2C bus 1 addressing channel 64 +// Known breakouts often have a shunt resistor of 0.008 Ohms and are designed to measure up to 10 Amperes. +using Ina219 device = new Ina236(new I2cConnectionSettings(Adafruit_Ina236_I2cBus, Adafruit_Ina219_I2cAddress), + ElectricResistance.FromMilliohms(8), ElectricCurrent.FromAmperes(10.0)); + +Console.WriteLine("Device initialized. Default settings used:"); +Console.WriteLine($"Operating Mode: {device.OperatingMode}"); +Console.WriteLine($"Number of Samples to average: {device.AverageOverNoSamples}"); +Console.WriteLine($"Bus conversion time: {device.BusConversionTime}us"); +Console.WriteLine($"Shunt conversion time: {device.ShuntConversionTime}us"); + +while (!Console.KeyAvailable) +{ + // write out the current values from the INA219 device. + Console.WriteLine($"Bus Voltage {device.ReadBusVoltage()} Shunt Voltage {device.ReadShuntVoltage().Millivolts}mV Current {device.ReadCurrent()} Power {device.ReadPower()}"); + Thread.Sleep(1000); +} + +``` + +### Notes + +To set up the binding, the shunt resistor value and the maximum expected current need to be provided. Known breakout boards +(e.g. from Adafruit or Joy-It) have a shunt resistor of 0.008 Ohms. With a 10 A load, the voltage drop at the resistor is thus +0.08 V, resulting in a power dissipation of 0.8 Watts. + diff --git a/src/devices/Ina236/category.txt b/src/devices/Ina236/category.txt new file mode 100644 index 0000000000..3954b07309 --- /dev/null +++ b/src/devices/Ina236/category.txt @@ -0,0 +1,2 @@ +adc +power diff --git a/src/devices/Ina236/samples/Ina236.Samples.csproj b/src/devices/Ina236/samples/Ina236.Samples.csproj new file mode 100644 index 0000000000..6ab982f9ef --- /dev/null +++ b/src/devices/Ina236/samples/Ina236.Samples.csproj @@ -0,0 +1,10 @@ + + + Exe + $(DefaultSampleTfms) + + + + + + \ No newline at end of file diff --git a/src/devices/Ina236/samples/Program.cs b/src/devices/Ina236/samples/Program.cs new file mode 100644 index 0000000000..620e7ab6fc --- /dev/null +++ b/src/devices/Ina236/samples/Program.cs @@ -0,0 +1,27 @@ +// 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.Threading; +using System.Device.I2c; +using Iot.Device; +using Iot.Device.Adc; +using Iot.Device.Arduino; +using UnitsNet; + +using ArduinoBoard board = new ArduinoBoard("COM5", 115200); +using Ina236 device = new(board.CreateI2cDevice(new I2cConnectionSettings(0, 0x40)), ElectricResistance.FromMilliohms(8), + ElectricCurrent.FromAmperes(10.0)); + +Console.WriteLine("Device initialized. Default settings used:"); +Console.WriteLine($"Operating Mode: {device.OperatingMode}"); +Console.WriteLine($"Number of Samples to average: {device.AverageOverNoSamples}"); +Console.WriteLine($"Bus conversion time: {device.BusConversionTime}us"); +Console.WriteLine($"Shunt conversion time: {device.ShuntConversionTime}us"); + +while (!Console.KeyAvailable) +{ + // write out the current values from the INA219 device. + Console.WriteLine($"Bus Voltage {device.ReadBusVoltage()} Shunt Voltage {device.ReadShuntVoltage().Millivolts}mV Current {device.ReadCurrent()} Power {device.ReadPower()}"); + Thread.Sleep(1000); +} diff --git a/src/devices/Ina236/tests/Ina236.Tests.csproj b/src/devices/Ina236/tests/Ina236.Tests.csproj new file mode 100644 index 0000000000..7332effc31 --- /dev/null +++ b/src/devices/Ina236/tests/Ina236.Tests.csproj @@ -0,0 +1,14 @@ + + + + $(DefaultSampleTfms) + 10 + false + false + + + + + + + diff --git a/src/devices/Ina236/tests/Ina236Tests.cs b/src/devices/Ina236/tests/Ina236Tests.cs new file mode 100644 index 0000000000..fdf0bb1607 --- /dev/null +++ b/src/devices/Ina236/tests/Ina236Tests.cs @@ -0,0 +1,64 @@ +// 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.Gpio; +using System.Device.I2c; +using System.Device.Spi; +using System.Drawing; +using Ina236.Tests; +using Iot.Device.Graphics; +using Moq; +using UnitsNet; +using Xunit; + +namespace Iot.Device.Adc.Tests +{ + public sealed class Ina236Tests : IDisposable + { + private Ina236 _ina236; + private SimulatedIna236 _simulatedIna236; + + public Ina236Tests() + { + _simulatedIna236 = new SimulatedIna236(new I2cConnectionSettings(1, 0x40)); + // Use the setting that corresponds to the example values in the data sheet + _ina236 = new Ina236(_simulatedIna236, ElectricResistance.FromMilliohms(8), ElectricCurrent.FromAmperes(16.384 / 2.0)); + } + + [Fact] + public void InitialValues() + { + Assert.Equal(Ina236OperatingMode.ContinuousShuntAndBusVoltage, _ina236.OperatingMode); + Assert.Equal(1u, _ina236.AverageOverNoSamples); + Assert.Equal(1100, _ina236.BusConversionTime); + Assert.Equal(1100, _ina236.ShuntConversionTime); + } + + [Fact] + public void CheckSetupComplete() + { + int calibrationValue = _simulatedIna236.RegisterMap[5].ReadRegister(); + Assert.Equal(1280, calibrationValue); + } + + [Fact] + public void ReadValues() + { + // Calibration has been set up, so we should get the values mentioned in the data sheet + ElectricPotential voltage = _ina236.ReadBusVoltage(); + Assert.Equal(12.0, voltage.Volts); + + ElectricCurrent current = _ina236.ReadCurrent(); + Assert.Equal(6.0, current.Amperes); + + Power p = _ina236.ReadPower(); + Assert.Equal(72.0m, p.Watts); + } + + public void Dispose() + { + _ina236.Dispose(); + } + } +} diff --git a/src/devices/Ina236/tests/SimulatedIna236.cs b/src/devices/Ina236/tests/SimulatedIna236.cs new file mode 100644 index 0000000000..9e77538074 --- /dev/null +++ b/src/devices/Ina236/tests/SimulatedIna236.cs @@ -0,0 +1,71 @@ +// 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.Collections.Generic; +using System.Device.I2c; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ina236.Tests +{ + internal class SimulatedIna236 : I2cSimulatedDeviceBase + { + public SimulatedIna236(I2cConnectionSettings settings) + : base(settings) + { + RegisterMap.Add(0, new Register(0x4127, ConfigurationRegisterHandler, null)); // 0x4127 is the power-on default of the configuration register + RegisterMap.Add(1, new Register(19200)); // Default values for example from data sheet + RegisterMap.Add(2, new Register(7500)); + RegisterMap.Add(3, new Register(4500)); + RegisterMap.Add(4, new Register(12000)); + RegisterMap.Add(5, new Register()); + RegisterMap.Add(6, new Register()); + RegisterMap.Add(7, new Register()); + // the value is big-endian, but that is taken care of later + RegisterMap.Add(0x3F, new Register(0xA080, DeviceIdentificationRegisterHandler, null)); + } + + private ushort DeviceIdentificationRegisterHandler(ushort arg) + { + // This register is read-only + return 0xA080; + } + + private ushort ConfigurationRegisterHandler(ushort newValue) + { + // When the reset bit is set, set everything to default + if ((newValue & 0x8000) != 0) + { + RegisterMap[5].WriteRegister(0); // Reset the calibration register + return 0x4127; + } + + return newValue; + } + + public override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) + { + if (inputBuffer.Length > 0) + { + CurrentRegister = inputBuffer[0]; + if (inputBuffer.Length >= 3 && RegisterMap.TryGetValue(CurrentRegister, out var register)) + { + ushort reg = BinaryPrimitives.ReadUInt16BigEndian(inputBuffer.AsSpan().Slice(1)); + register.WriteRegister(reg); + } + } + + // All registers of this device are 16 bit, so we need to read that or nothing + if (outputBuffer.Length >= 2 && RegisterMap.TryGetValue(CurrentRegister, out var register2)) + { + ushort ret = (ushort)register2.ReadRegister(); + BinaryPrimitives.WriteUInt16BigEndian(outputBuffer, ret); + } + + return outputBuffer.Length; + } + } +} diff --git a/src/devices/Tca955x/README.md b/src/devices/Tca955x/README.md index 52a4036d27..a6efcdb5c0 100644 --- a/src/devices/Tca955x/README.md +++ b/src/devices/Tca955x/README.md @@ -2,7 +2,7 @@ ## Summary -The TCA955X device family provides 8/16-bit, general purpose I/O expansion for I2C. The devices can be configured with polariy invertion and interrupts. +The TCA955X device family provides 8/16-bit, general purpose I/O expansion for I2C. The devices can be configured with polarity inversion and interrupts. ## Device Family diff --git a/src/devices/Tca955x/Tca955x.sln b/src/devices/Tca955x/Tca955x.sln index fd3136b2ac..e227882739 100644 --- a/src/devices/Tca955x/Tca955x.sln +++ b/src/devices/Tca955x/Tca955x.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AC41B656 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tca955x.Tests", "tests\Tca955x.Tests.csproj", "{F3BCAFCA-A6B8-4530-B435-36FA238D947A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "..\Common\Common.csproj", "{9F0601AB-EA31-A20F-1B21-30C901BDC579}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +61,18 @@ Global {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x64.Build.0 = Release|Any CPU {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x86.ActiveCfg = Release|Any CPU {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x86.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x64.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x64.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x86.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x86.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|Any CPU.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x64.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x64.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x86.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/devices/Tca955x/tests/MockableI2cDevice.cs b/src/devices/Tca955x/tests/MockableI2cDevice.cs deleted file mode 100644 index 3f15863024..0000000000 --- a/src/devices/Tca955x/tests/MockableI2cDevice.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Iot.Device.Tca955x.Tests -{ - /// - /// This class allows mocking of the Read/Write functions of I2cDevice taking Spans - /// - public abstract class MockableI2cDevice : I2cDevice - { - /// - /// These are mockable, operations taking Span<T> are not - /// - /// - public abstract void Read(byte[] data); - public sealed override void Read(Span buffer) - { - byte[] b = new byte[buffer.Length]; - Read(b); - b.CopyTo(buffer); - } - - public abstract void Write(byte[] data); - - public sealed override void Write(ReadOnlySpan buffer) - { - byte[] data = new byte[buffer.Length]; - buffer.CopyTo(data); - Write(data); - } - - public sealed override void WriteRead(ReadOnlySpan writeBuffer, Span readBuffer) - { - Write(writeBuffer); - Read(readBuffer); - } - } -} diff --git a/src/devices/Tca955x/tests/Tca9554Tests.cs b/src/devices/Tca955x/tests/Tca9554Tests.cs index 8f8bcf2fb0..fb61a2ec91 100644 --- a/src/devices/Tca955x/tests/Tca9554Tests.cs +++ b/src/devices/Tca955x/tests/Tca9554Tests.cs @@ -8,33 +8,32 @@ using System.Threading; using Moq; +using Tca955x.Tests; using Xunit; namespace Iot.Device.Tca955x.Tests { public class Tca9554Tests { - private readonly Mock _device; - private readonly Mock _deviceWithBadAddress; + private readonly Tca955xSimulatedDevice _device; + private readonly Mock _deviceWithBadAddress; private readonly GpioController _controller; private readonly Mock _driver; public Tca9554Tests() { - _device = new Mock(MockBehavior.Loose); - _deviceWithBadAddress = new Mock(MockBehavior.Loose); - _device.CallBase = true; + _device = new Tca955xSimulatedDevice(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress)); + _deviceWithBadAddress = new Mock(MockBehavior.Loose, new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress + Tca9554.AddressRange + 1)); + _deviceWithBadAddress.Setup(x => x.ConnectionSettings).CallBase(); _driver = new Mock(); _driver.CallBase = true; _controller = new GpioController(_driver.Object); - _device.Setup(x => x.ConnectionSettings).Returns(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress)); - _deviceWithBadAddress.Setup(x => x.ConnectionSettings).Returns(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress + Tca9554.AddressRange + 1)); } [Fact] public void CreateWithInterrupt() { - var testee = new Tca9554(_device.Object, 10, _controller); + var testee = new Tca9554(_device, 10, _controller); } [Fact] @@ -46,22 +45,13 @@ public void CreateWithBadAddress() [Fact] public void CreateWithoutInterrupt() { - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); } [Fact] public void TestRead() { - _device.Setup(x => x.Write(new byte[1] - { - 0 - })); - _device.Setup(x => x.Read(It.IsAny())).Callback((byte[] b) => - { - b[0] = 1; - }); - - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); var tcaController = new GpioController(testee); Assert.Equal(8, tcaController.PinCount); GpioPin pin0 = tcaController.OpenPin(0); @@ -78,7 +68,7 @@ public void InterruptCallbackIsInvokedOnPinChange() { // Arrange var interruptPin = 10; - var testee = new Tca9554(_device.Object, interruptPin, _controller); + var testee = new Tca9554(_device, interruptPin, _controller); var tcaController = new GpioController(testee); tcaController.OpenPin(1, PinMode.Input); bool callbackInvoked = false; @@ -92,23 +82,15 @@ void Callback(object sender, PinValueChangedEventArgs args) mre.Set(); } - // Change the device setup to simulate pin1 as high - _device.Setup(x => x.Read(It.IsAny())).Callback((byte[] b) => - { - b[0] = 0x02; - }); + _device.SetPinState(1, PinValue.High); // Register callback for rising edge tcaController.RegisterCallbackForPinValueChangedEvent(1, PinEventTypes.Falling, Callback); // Change the device setup to simulate pin1 as low. - _device.Setup(x => x.Read(It.IsAny())).Callback((byte[] b) => - { - b[0] = 0x00; - }); - + _device.SetPinState(1, PinValue.Low); // Act - // Simulate the hardware int pin pin change using the _controller mock + // Simulate the hardware int pin change using the _controller mock _driver.Object.FireEventHandler(interruptPin, PinEventTypes.Rising); mre.Wait(2000); // Wait for the callback to be invoked @@ -122,7 +104,7 @@ void Callback(object sender, PinValueChangedEventArgs args) [Fact] public void TestReadOfIllegalPinThrows() { - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); var tcaController = new GpioController(testee); Assert.Equal(8, tcaController.PinCount); GpioPin pin0 = tcaController.OpenPin(0); @@ -136,12 +118,12 @@ public void CanNotConstructIfInterruptConfiguredIncorrectly() { Assert.Throws(() => { - var testee = new Tca9554(_device.Object, -1, _controller); + var testee = new Tca9554(_device, -1, _controller); }); Assert.Throws(() => { - var testee = new Tca9554(_device.Object, 2); + var testee = new Tca9554(_device, 2); }); } diff --git a/src/devices/Tca955x/tests/Tca9555Tests.cs b/src/devices/Tca955x/tests/Tca9555Tests.cs index f04943dd1d..dfc80d94e8 100644 --- a/src/devices/Tca955x/tests/Tca9555Tests.cs +++ b/src/devices/Tca955x/tests/Tca9555Tests.cs @@ -6,67 +6,57 @@ using System.Device.Gpio.Tests; using System.Device.I2c; using Moq; +using Tca955x.Tests; using Xunit; namespace Iot.Device.Tca955x.Tests { public class Tca9555Tests { - private readonly Mock _device; + private readonly Tca955xSimulatedDevice _device; private readonly GpioController _controller; private readonly Mock _driver; public Tca9555Tests() { - _device = new Mock(MockBehavior.Loose); - _device.CallBase = true; + _device = new Tca955xSimulatedDevice(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress)); _driver = new Mock(); _controller = new GpioController(_driver.Object); - _device.Setup(x => x.ConnectionSettings).Returns(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress)); } [Fact] public void CreateWithInterrupt() { - var testee = new Tca9555(_device.Object, 10, _controller); + var testee = new Tca9555(_device, 10, _controller); Assert.NotNull(testee); } [Fact] public void CreateWithoutInterrupt() { - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); Assert.NotNull(testee); } [Fact] public void TestRead() { - _device.Setup(x => x.Write(new byte[1] - { - 0 - })); - _device.Setup(x => x.Read(It.IsAny())).Callback((byte[] b) => - { - b[0] = 1; - }); - - var testee = new Tca9555(_device.Object, -1); + var testee = new Tca9555(_device, -1); var tcaController = new GpioController(testee); Assert.Equal(16, tcaController.PinCount); - GpioPin pin8 = tcaController.OpenPin(8); - Assert.NotNull(pin8); - Assert.True(tcaController.IsPinOpen(8)); - var value = pin8.Read(); + GpioPin pin0 = tcaController.OpenPin(0); + Assert.NotNull(pin0); + Assert.True(tcaController.IsPinOpen(0)); + var value = pin0.Read(); Assert.Equal(PinValue.High, value); - pin8.Dispose(); + pin0.Dispose(); Assert.False(tcaController.IsPinOpen(8)); } [Fact] public void TestReadOfIllegalPinThrows() { - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); var tcaController = new GpioController(testee); Assert.Equal(8, tcaController.PinCount); GpioPin pin0 = tcaController.OpenPin(0); diff --git a/src/devices/Tca955x/tests/Tca955xSimulatedDevice.cs b/src/devices/Tca955x/tests/Tca955xSimulatedDevice.cs new file mode 100644 index 0000000000..fc62e33c83 --- /dev/null +++ b/src/devices/Tca955x/tests/Tca955xSimulatedDevice.cs @@ -0,0 +1,85 @@ +// 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.Gpio; +using System.Device.I2c; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tca955x.Tests +{ + internal class Tca955xSimulatedDevice : I2cSimulatedDeviceBase + { + private Register _register0; // pin register 0 + private Register _register2; // polarity inversion register + private Register _register3; // Configuration Register + + public Tca955xSimulatedDevice(I2cConnectionSettings settings) + : base(settings) + { + _register0 = new Register(1); // Pin 0 is high + _register2 = new Register(0); + _register3 = new Register(0); + } + + public void SetPinState(int pin, PinValue value) + { + int bit = 1 << pin; + if (value == PinValue.High) + { + _register0.WriteRegister(_register0.ReadRegister() | bit); + } + else + { + _register0.WriteRegister(_register0.ReadRegister() & ~bit); + } + } + + public override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) + { + if (inputBuffer.Length >= 1) + { + CurrentRegister = inputBuffer[0]; + } + + if (CurrentRegister == 0) + { + outputBuffer[0] = _register0.Value; + return 1; + } + + if (CurrentRegister == 2) + { + if (inputBuffer.Length > 1) + { + _register2.Value = inputBuffer[1]; + } + + if (outputBuffer.Length > 0) + { + outputBuffer[0] = _register2.Value; + return 1; + } + } + + if (CurrentRegister == 3) + { + if (inputBuffer.Length > 1) + { + _register3.Value = inputBuffer[1]; + } + + if (outputBuffer.Length > 0) + { + outputBuffer[0] = _register3.Value; + return 1; + } + } + + return 0; + } + } +}