|
// 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 UnitsNet;
namespace Iot.Device.Amg88xx
{
/// <summary>
/// AMG88xx - family of infrared array sensors
/// </summary>
public class Amg88xx : IDisposable
{
/// <summary>
/// Standard device address
/// (AD_SELECT pin is low, c.f. reference specification, pg. 11)
/// </summary>
public const int DefaultI2cAddress = 0x68;
/// <summary>
/// Alternative device address
/// (AD_SELECT pin is high, c.f. reference specification, pg. 11)
/// </summary>
public const int AlternativeI2cAddress = 0x69;
/// <summary>
/// Number of sensor pixel array columns
/// </summary>
public const int Width = 0x8;
/// <summary>
/// Number of sensor pixel array rows
/// </summary>
public const int Height = 0x8;
/// <summary>
/// Total number of pixels.
/// </summary>
public const int PixelCount = Width * Height;
/// <summary>
/// Number of bytes per pixel
/// </summary>
private const int BytesPerPixel = 2;
/// <summary>
/// Temperature resolution of thermistor (in degrees Celsius)
/// </summary>
private const double ThermistorTemperatureResolution = 0.0625;
/// <summary>
/// Internal storage for the most recently image read from the sensor
/// </summary>
private readonly byte[] _imageData = new byte[PixelCount * BytesPerPixel];
private I2cDevice _i2cDevice;
/// <summary>
/// Initializes a new instance of the <see cref="Amg88xx"/> binding.
/// </summary>
public Amg88xx(I2cDevice i2cDevice)
{
_i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice));
}
#region Infrared sensor
/// <summary>
/// Gets temperature of the specified pixel from the current thermal image.
/// </summary>
/// <param name="x">The x-coordinate of the pixel to retrieve.</param>
/// <param name="y">The y-coordinate of the pixel to retrieve.</param>
/// <exception cref="ArgumentException">x is less than 0, or greater than or equal to Width.</exception>
/// <exception cref="ArgumentException">y is less than 0, or greater than or equal to Height.</exception>
/// <returns>Temperature of the specified pixel.</returns>
public Temperature this[int x, int y]
{
get
{
if (x < 0 || x >= Width)
{
throw new ArgumentOutOfRangeException(nameof(x));
}
if (y < 0 || y >= Height)
{
throw new ArgumentOutOfRangeException(nameof(y));
}
Span<byte> buffer = _imageData;
return Amg88xxUtils.ConvertToTemperature(buffer.Slice(BytesPerPixel * (Width * y + x), BytesPerPixel));
}
}
/// <summary>
/// Gets temperature for all pixels from the current thermal image as a two-dimensional array.
/// First index specifies the x-coordinate of the pixel and second index specifies y-coordinate of the pixel.
/// </summary>
/// <returns>Temperature as a two-dimensional array.</returns>
public Temperature[,] TemperatureImage
{
get
{
Temperature[,] temperatureImage = new Temperature[Width, Height];
for (int y = 0; y < Height; y++)
{
for (int x = 0; x < Width; x++)
{
temperatureImage[x, y] = this[x, y];
}
}
return temperatureImage;
}
}
/// <summary>
/// Gets raw reading (12-bit two's complement format) of the specified pixel from the current thermal image.
/// </summary>
/// <param name="n">The number of the pixel to retrieve.</param>
/// <exception cref="ArgumentException">n is less than 0, or greater than or equal to PixelCount.</exception>
/// <returns>Reading of the specified pixel.</returns>
public Int16 this[int n]
{
get
{
if (n < 0 || n >= PixelCount)
{
throw new ArgumentOutOfRangeException(nameof(n));
}
Span<byte> buffer = _imageData;
return BinaryPrimitives.ReadInt16LittleEndian(buffer.Slice(n * BytesPerPixel, BytesPerPixel));
}
}
/// <summary>
/// Reads the current image from the sensor
/// </summary>
public void ReadImage()
{
// the readout process gets triggered by writing to pixel 0 of the sensor w/o any additional data
_i2cDevice.WriteByte((byte)Register.T01L);
_i2cDevice.Read(_imageData);
}
/// <summary>
/// Gets the temperature reading from the internal thermistor.
/// </summary>
/// <value>Temperature reading</value>
public Temperature SensorTemperature
{
get
{
byte tthl = GetRegister(Register.TTHL);
byte tthh = GetRegister(Register.TTHH);
int reading = (tthh & 0x7) << 8 | tthl;
reading = tthh >> 3 == 0 ? reading : -reading;
// The LSB is equivalent to 0.0625℃.
return Temperature.FromDegreesCelsius(reading * ThermistorTemperatureResolution);
}
}
#endregion
#region Status
/// <summary>
/// Gets whether any pixel measured a temperature higher than the normal operation range.
/// The event of an overflow does not prevent from continuing reading the sensor.
/// The overflow indication will last even if all pixels are returned to readings within normal range.
/// The indicator is reset using <see cfref="ClearTemperatureOverflow"/>.
/// </summary>
/// <returns>True, if an overflow occured</returns>
public bool HasTemperatureOverflow()
{
return GetBit(Register.STAT, (byte)StatusFlagBit.OVF_IRS);
}
/// <summary>
/// Clears the temperature overflow indication.
/// </summary>
public void ClearTemperatureOverflow()
{
// only the bit to be cleared is set, the other bits need to be 0
SetRegister(Register.SCLR, 1 << (byte)StatusClearBit.OVFCLR);
}
/// <summary>
/// Gets the thermistor overflow flag from the status register.
/// The overflow indication will last even if the thermistor temperature returned to normal range.
/// The event of an overflow does not prevent from continuing reading the sensor.
/// The indicator is reset using <see cfref="ClearThermistorOverflow"/>.
/// Note: the bit is only menthioned in early versions of the reference specification.
/// It is not clear whether this is a specification error or a change in a newer
/// revision of the sensor.
/// </summary>
/// <returns>True, if an overflow occured</returns>
public bool HasThermistorOverflow()
{
return GetBit(Register.STAT, (byte)StatusFlagBit.OVF_THS);
}
/// <summary>
/// Clears the temperature overflow indication.
/// </summary>
public void ClearThermistorOverflow()
{
// only the bit to be cleared is set, the other bits need to be 0
SetRegister(Register.SCLR, 1 << (byte)StatusClearBit.OVFTHCLR);
}
/// <summary>
/// Gets the interrupt flag from the status register
/// </summary>
/// <returns>Interrupt flag</returns>
public bool HasInterrupt()
{
return GetBit(Register.STAT, (byte)StatusFlagBit.INTF);
}
/// <summary>
/// Clears the interrupt flag in the status register
/// </summary>
public void ClearInterrupt()
{
// only the bit to be cleared is set, the other bits need to be 0
SetRegister(Register.SCLR, 1 << (byte)StatusClearBit.INTCLR);
}
/// <summary>
/// Clears all flags in the status register.
/// Note: it does not clear the interrupt flags of the individual pixels.
/// </summary>
public void ClearAllFlags()
{
// only the bit to be cleared is set, the other bits need to be 0
SetRegister(Register.SCLR, (1 << (byte)StatusClearBit.OVFCLR) | (1 << (byte)StatusClearBit.OVFTHCLR) | (1 << (byte)StatusClearBit.INTCLR));
}
#endregion
#region Moving average
/// <summary>
/// Get or sets the state of the moving average mode
/// Important: the reference specification states that the current mode can be read,
/// but it doesn't seem to work at the time being.
/// In this case the property is always read as ```false```.
/// </summary>
/// <value>True if the moving average should be calculated; otherwise, false. The default is false.</value>
public bool UseMovingAverageMode
{
get => GetBit(Register.AVE, (byte)MovingAverageModeBit.MAMOD);
set => SetBit(Register.AVE, (byte)MovingAverageModeBit.MAMOD, value);
}
#endregion
#region Frame Rate
/// <summary>
/// Get or sets the frame rate of the sensor internal thermal image update.
/// </summary>
/// <exception cref="ArgumentException">Thrown when attempting to set a frame rate other than 1 or 10 frames per second</exception>
/// <value>The frame rate for the pixel update interval (either 1 or 10fps). The default is 10fps.</value>
public FrameRate FrameRate
{
get => GetBit(Register.FPSC, (byte)FrameRateBit.FPS) ? FrameRate.Rate1FramePerSecond : FrameRate.Rate10FramesPerSecond;
set
{
if (value != FrameRate.Rate1FramePerSecond && value != FrameRate.Rate10FramesPerSecond)
{
throw new ArgumentException("Frame rate must either be 1 or 10.");
}
SetBit(Register.FPSC, (byte)FrameRateBit.FPS, value == FrameRate.Rate1FramePerSecond);
}
}
#endregion
#region Operating Mode / Power Control
/// <summary>
/// Gets or sets the current operating mode
/// Refer to the sensor reference specification for a description of the mode
/// depending sensor bevaviour and the valid mode transistions.
/// </summary>
/// <value>The operating mode of the sensor. The default is Normal.</value>
public OperatingMode OperatingMode
{
get => (OperatingMode)GetRegister(Register.PCLT);
set => SetRegister(Register.PCLT, (byte)value);
}
#endregion
#region Reset
/// <summary>
/// Performs an reset of the sensor. The flags and all configuration registers
/// are reset to default values.
/// </summary>
public void Reset()
{
// a reset (factory defaults) is initiated by writing 0x3f into the reset register (RST)
SetRegister(Register.RST, (byte)ResetType.Initial);
}
/// <summary>
/// Performs a reset of all flags (status register, interrupt flag and interrupt table).
/// This method is useful, if using the interrupt mechanism for pixel temperatures.
/// If an upper and lower level has been set along with a hysteresis this reset can clear the interrupt state of all pixels
/// which are within the range between upper and lower level, but still above/below the hystersis level.
/// If this applies to ALL pixels the interrupt flag gets cleared as well.
/// Refer to the binding documentation for more details on interrupt level, hysteresis and flagging.
/// </summary>
public void ResetAllFlags()
{
// a reset of all flags (status register, interrupt flag and interrupt table) is initiated by writing 0x30
// into the reset register (RST)
SetRegister(Register.RST, (byte)ResetType.Flag);
}
#endregion
#region Interrupt control, levels and pixel flags
/// <summary>
/// Gets or sets the pixel temperature interrupt mode.
/// </summary>
/// <value>The interrupt mode, which is either aboslute or differential. The default is ```Difference```.</value>
public InterruptMode InterruptMode
{
get => GetBit(Register.INTC, (byte)InterruptModeBit.INTMODE) ? InterruptMode.Absolute : InterruptMode.Difference;
set => SetBit(Register.INTC, (byte)InterruptModeBit.INTMODE, value == InterruptMode.Absolute);
}
/// <summary>
/// Get or sets whether the interrupt output pin of the sensor is enabled.
/// If enabled, the pin is pulled down if an interrupt is active.
/// </summary>
/// <value>True, if the INT pin sould be enabled; otherwise false. The default is false."</value>
public bool InterruptPinEnabled
{
get => GetBit(Register.INTC, (byte)InterruptModeBit.INTEN);
set => SetBit(Register.INTC, (byte)InterruptModeBit.INTEN, value);
}
/// <summary>
/// Gets or sets the pixel temperature lower interrupt level.
/// </summary>
/// <value>Temperature level to trigger an interrupt if the any pixel falls below. The default is 0.</value>
public Temperature InterruptLowerLevel
{
get
{
byte tl = GetRegister(Register.INTLL);
byte th = GetRegister(Register.INTLH);
return Amg88xxUtils.ConvertToTemperature(tl, th);
}
set
{
(byte tl, byte th) = Amg88xxUtils.ConvertFromTemperature(value);
SetRegister(Register.INTLL, tl);
SetRegister(Register.INTLH, th);
}
}
/// <summary>
/// Gets or sets the pixel temperature upper interrupt level.
/// </summary>
/// <value>Temperature level to trigger an interrupt if the any pixel exceeds. The default is 0.</value>
public Temperature InterruptUpperLevel
{
get
{
byte tl = GetRegister(Register.INTHL);
byte th = GetRegister(Register.INTHH);
return Amg88xxUtils.ConvertToTemperature(tl, th);
}
set
{
(byte tl, byte th) = Amg88xxUtils.ConvertFromTemperature(value);
SetRegister(Register.INTHL, tl);
SetRegister(Register.INTHH, th);
}
}
/// <summary>
/// Gets or sets the pixel temperature interrupt hysteresis.
/// </summary>
/// <value>Temperature hysteresis for lower and upper interrupt triggering. The default is 0.</value>
public Temperature InterruptHysteresis
{
get
{
byte tl = GetRegister(Register.INTSL);
byte th = GetRegister(Register.INTSH);
return Amg88xxUtils.ConvertToTemperature(tl, th);
}
set
{
(byte tl, byte th) = Amg88xxUtils.ConvertFromTemperature(value);
SetRegister(Register.INTSL, tl);
SetRegister(Register.INTSH, th);
}
}
/// <summary>
/// Gets the interrupt flags of all pixels.
/// </summary>
/// <returns>Interrupt flags</returns>
public bool[,] GetInterruptFlagTable()
{
var registers = new Register[]
{
Register.INT0, Register.INT1, Register.INT2, Register.INT3,
Register.INT4, Register.INT5, Register.INT6, Register.INT7,
};
// read all registers from the sensor
var flagRegisters = new Queue<byte>();
foreach (Register register in registers)
{
flagRegisters.Enqueue(GetRegister(register));
}
var flags = new bool[Width, Height];
for (int row = 0; row < Height; row++)
{
var flagRegister = flagRegisters.Dequeue();
for (int col = 0; col < Width; col++)
{
flags[col, row] = (flagRegister & (1 << col)) > 0;
}
}
return flags;
}
#endregion
private byte GetRegister(Register register)
{
_i2cDevice.WriteByte((byte)register);
return _i2cDevice.ReadByte();
}
private bool GetBit(Register register, byte bit)
{
return (GetRegister(register) & (1 << bit)) > 0;
}
private void SetRegister(Register register, byte value)
{
Span<byte> buffer = stackalloc byte[2]
{
(byte)register,
value
};
_i2cDevice.Write(buffer);
}
private void SetBit(Register register, byte bit, bool state)
{
var b = GetRegister(register);
b = (byte)(state ? (b | (1 << bit)) : (b & (~(1 << bit))));
SetRegister(register, b);
}
/// <inheritdoc />
public void Dispose()
{
if (_i2cDevice != null)
{
_i2cDevice?.Dispose();
_i2cDevice = null!;
}
}
}
}
|