mlx9064x 0.2.1

Library for interfacing with Melexis MLX9064* thermal cameras
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright © 2021 Will Ross
//! Common functionality between MLX90640 and MLX90641 cameras.
use core::fmt;

use arrayvec::ArrayVec;

use crate::register::{AccessPattern, Subpage};
use crate::util::Sealed;

/// A trait for types that can be created by reading data from an I²C device.
pub trait FromI2C<I2C> {
    type Error;
    type Ok;

    /// Create an instance of a type using data retrieved over I²C.
    fn from_i2c(bus: &mut I2C, i2c_address: u8) -> Result<Self::Ok, Self::Error>;
}

/// This trait provides access to the module-specific calibration data.
///
/// Each MLX9064\* camera has calibration data from the factory stored on its EEPROM. The
/// factory-provided data is then used as the input to generate the constants needed to convert the
/// raw output of the camera into concrete temperatures (or even just raw infrared radition
/// measurements). The naming scheme for the methods in this trait is taken from the names of the
/// variables used in the formulas in the datasheet. Most users of this library can make use fo the
/// provided implementations, but if you're trying to minimize memory usage or tweak performance
/// for a specific use case, this might be a way to do it.
#[doc = include_str!("katex.html")]
pub trait CalibrationData<'a> {
    /// The camera model this caliberation data is for.
    type Camera: MelexisCamera;

    /// Pixel supply voltage constant ($K_{V_{DD}}$).
    fn k_v_dd(&self) -> i16;

    /// Constant for pixel supply voltage at 25℃ ($K_{V_{DD_{25}}}$).
    fn v_dd_25(&self) -> i16;

    /// ADC resolution this camera was calibrated at.
    // TODO: Should this return `Resolution`?
    fn resolution(&self) -> u8;

    /// Pixel supply voltage ($K_{DD_0}$).
    ///
    /// This is the voltage supplied to the device, and should be 3.3V for the MLX90640 and
    /// MLX90641. The default implementation is hardcoded to return `3.3f32`, but if there's a
    /// reason it needs to be overridden, it's possible.
    fn v_dd_0(&self) -> f32 {
        3.3f32
    }

    /// Voltage proportional to ambient temperature constant ($K_{V_{PTAT}}$).
    fn k_v_ptat(&self) -> f32;

    /// Temperature proportional to ambient temperature constant ($K_{T_{PTAT}}$).
    fn k_t_ptat(&self) -> f32;

    /// Voltage proportional to ambient temperature at 25℃ ($V_{PTAT_{25}}$).
    fn v_ptat_25(&self) -> f32;

    /// Sensitivity proportional to ambient temperature ($\alpha_{PTAT}$).
    fn alpha_ptat(&self) -> f32;

    /// The gain constant. Usually written as <var>GAIN</var> in the datasheets.
    fn gain(&self) -> f32;

    /// Sensitivity constant for ambient temperature ($K_{S_{T_{a}}}$).
    fn k_s_ta(&self) -> f32;

    /// A slice of the "corner temperatures".
    ///
    /// These define temperature ranges with different sensitivity characteristics. They are
    /// indexed in the datasheet starting from 1 but everything in this library is 0-indexed, so be
    /// aware of the difference.
    fn corner_temperatures(&self) -> &[i16];

    /// Constant for the object temperature sensitivity ($K_{s_{T_{o}N}}$)
    /// depending on the temperature range.
    ///
    /// This is a slight variance from the datasheet's nomenclature. In the symbol above,
    /// <var>N</var> is the index of the temperature range, which the datasheet normally just
    /// writes out (ex: $K_{S_{T_{o}1}}$ through how every many temperature
    /// ranges the camera has).
    ///
    /// This method returns a slice of values equal in length to
    /// [`corner_temperatures`](CalibrationData::corner_temperatures).
    fn k_s_to(&self) -> &[f32];

    /// Temperature range sensitivity correction ($\alpha_{\text{correction}_{N}}$)
    ///
    /// Like [`k_s_to`], the name of this method is slightly different that the naming in the
    /// datasheet. Also like `k_s_to`, this method returns a slice of values with a length equal to
    /// the length of the slice returned by
    /// [`corner_temperatures`](CalibrationData::corner_temperatures),
    ///
    /// [`k_s_to`]: CalibrationData::k_s_to
    fn alpha_correction(&self) -> &[f32];

    /// The emissivity stored on the device.
    ///
    /// Not all devices support storing the emissivity, in which case they should return [None]
    /// (which is what the provided implementation does).
    fn emissivity(&self) -> Option<f32> {
        None
    }

    type OffsetReferenceIterator: Iterator<Item = &'a i16>;

    /// An iterator over the per-pixel offset reference values for the given subpage
    /// ($\text{Offset}_\text{reference}(i, j)$).
    ///
    /// The iterator must yield pixels by row, then by columns, with the rows increasing from left
    /// to right and the columns increasing from top to bottom. The iterator must yield *all*
    /// pixels, even if they would not normally be present in the given subpage.
    fn offset_reference_pixels(&'a self, subpage: Subpage) -> Self::OffsetReferenceIterator;

    /// The offset reference value for the compensation pixel corresponding to the given subpage
    /// ($\text{Offset}\_{\text{reference}\_{CP}}$).
    fn offset_reference_cp(&self, subpage: Subpage) -> i16;

    type AlphaIterator: Iterator<Item = &'a f32>;

    /// An iterator over the per-pixel sensitivity calibration values ($\alpha_{pixel}(i, j)$).
    ///
    /// The iterator must yield pixels by row, then by columns, with the rows increasing from left
    /// to right and the columns increasing from top to bottom. The iterator must yield *all*
    /// pixels, even if they would not normally be present in the given subpage.
    fn alpha_pixels(&'a self, subpage: Subpage) -> Self::AlphaIterator;

    /// The sensitivity calibration value for the compensation pixel for the given subpage
    /// ($\alpha_{CP}$).
    fn alpha_cp(&self, subpage: Subpage) -> f32;

    type KvIterator: Iterator<Item = &'a f32>;

    /// An iterator over the per-pixel voltage calibration constants ($K_{V_{pixel}}$).
    ///
    /// The iterator must yield pixels by row, then by columns, with the rows increasing from left
    /// to right and the columns increasing from top to bottom. The iterator must yield *all*
    /// pixels, even if they would not normally be present in the given subpage.
    fn k_v_pixels(&'a self, subpage: Subpage) -> Self::KvIterator;

    /// The voltage calibration constant for the compensation pixel for the given subpage
    /// ($K_{V_{CP}}$).
    fn k_v_cp(&self, subpage: Subpage) -> f32;

    type KtaIterator: Iterator<Item = &'a f32>;

    /// The per pixel ambient temperature calibration constants ($K_{T_{a}pixel}$).
    ///
    /// The iterator must yield pixels by row, then by columns, with the rows increasing from left
    /// to right and the columns increasing from top to bottom. The iterator must yield *all*
    /// pixels, even if they would not normally be present in the given subpage.
    fn k_ta_pixels(&'a self, subpage: Subpage) -> Self::KtaIterator;

    /// The ambient temperature calibration constant for the compensation pixel for the given
    /// subpage ($K_{T_{a}CP}$).
    fn k_ta_cp(&self, subpage: Subpage) -> f32;

    /// Temperature gradient coefficient (<var>TGC</var>).
    ///
    /// Some devices do not support a TGC (it can also be disabled manually on other devices).
    fn temperature_gradient_coefficient(&self) -> Option<f32>;

    type AccessPatternCompensation: Iterator<Item = Option<&'a f32>>;

    /// A sequence of per-pixel correction values that are added to the pixel gain value.
    ///
    /// The MLX90640 can be used in interleaved mode, but for optimal performance a correction
    /// needs to be applied. This value is summed with the pixel gain value and reference offset
    /// (the reference offset being scaled relative to the temperature difference).
    fn access_pattern_compensation_pixels(
        &'a self,
        access_pattern: AccessPattern,
    ) -> Self::AccessPatternCompensation;

    /// Equivalent to [`Self::access_pattern_compensation_pixels`] for compensation pixels.
    fn access_pattern_compensation_cp(
        &self,
        subpage: Subpage,
        access_pattern: AccessPattern,
    ) -> Option<f32>;
}
/// Marker newtype for addresses accessible over I<sup>2</sup>C.
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
pub struct Address(u16);

impl Address {
    /// Wrap the given address in an `Address`.
    ///
    /// This function is intended to be used in const contexts, in other cases the
    /// [`From`][core::convert::From]/[`TryFrom`][core::convert::TryFrom] implementations are
    /// probably easier to use.
    pub const fn new(address: u16) -> Self {
        Self(address)
    }

    pub(crate) fn as_bytes(&self) -> [u8; 2] {
        self.0.to_be_bytes()
    }
}

impl fmt::Debug for Address {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Address({:#X})", self.0)
    }
}

impl From<u16> for Address {
    fn from(raw_address: u16) -> Self {
        Self::new(raw_address)
    }
}

impl From<Address> for u16 {
    fn from(address: Address) -> Self {
        address.0
    }
}

impl From<Address> for usize {
    fn from(address: Address) -> Self {
        address.0 as usize
    }
}

/// Define common constants specific to a camera model.
///
/// The values from this trait are common between all cameras of a single model, and do not depend
/// on the calibration values from a specific camera.
///
/// This is a sealed trait, and can only be implemented by types defined within this crate.
#[doc = include_str!("katex.html")]
pub trait MelexisCamera: Sealed {
    type PixelRangeIterator: IntoIterator<Item = PixelAddressRange>;

    type PixelsInSubpageIterator: IntoIterator<Item = bool>;

    /// Ranges of memory that should be read to load a subpage's data from RAM.
    ///
    /// Different cameras with different [access patterns][crate::AccessPattern] have different optimal
    /// ways of loading data from RAM. In some cases loading by row and then ignoring half the data
    /// may be appropriate, in other loading individual pixels may be more efficient.
    ///
    /// The returned iterator will yield at most [`Self::HEIGHT`] items.
    fn pixel_ranges(subpage: Subpage, access_pattern: AccessPattern) -> Self::PixelRangeIterator;

    /// Returns an iterator of booleans for whether or not a pixel should be considered for a
    /// subpage.
    ///
    /// This is a complement to [`pixel_ranges`][MelexisCamera::pixel_ranges], in that it lets an
    /// implementation load extra memory when it's more efficient but then ignore the pixels for
    /// later computations.
    ///
    /// The iterator should return true when the pixel is part of this subpage, and false when it is
    /// not. The ordering is rows, then columns. The iterator must not be infinite; it should only
    /// yield as many values as there are pixels.
    fn pixels_in_subpage(
        subpage: Subpage,
        access_pattern: AccessPattern,
    ) -> Self::PixelsInSubpageIterator;

    /// The address for $T_{a_{V_{BE}}}$.
    const T_A_V_BE: Address;

    /// The address for $T_{a_{PTAT}}$.
    const T_A_PTAT: Address;

    /// The address of the compensation pixel for the given subpage.
    fn compensation_pixel(subpage: Subpage) -> Address;

    /// The address of the current gain.
    const GAIN: Address;

    /// The address for $V_{DD_{pixel}}$.
    const V_DD_PIXEL: Address;

    /// Calculate the ADC resolution correction factor
    fn resolution_correction(calibrated_resolution: u8, current_resolution: u8) -> f32;

    /// The index of the basic temperature range.
    ///
    /// Temperature ranges (delimited by the control temperatures) outside of the basic range
    /// are "extended temperature ranges" and require extra processing for accuracy. The datasheets
    /// don't give a generic definition of the basic range, but for this library it is defined as
    /// the temperature range with α<sub>correction</sub>(r) = 1. Also note that this library uses
    /// 0-indexing as opposed to the datasheets that use 1-indexing.
    const BASIC_TEMPERATURE_RANGE: usize;

    /// The expected amount of self-heating for this camera.
    ///
    /// In normal operation the camera generates some heat. If $T_r$ is not available, it
    /// can be calculated by subtracting this value from $T_a$.
    const SELF_HEATING: f32;

    /// The height of the thermal image in pixels.
    const HEIGHT: usize;

    /// The width of the thermal image in pixels.
    const WIDTH: usize;

    /// The total number of pixels in the thermal image.
    const NUM_PIXELS: usize;
}

/// A range of camera memory.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PixelAddressRange {
    /// The address of memory to start reading from.
    pub(crate) start_address: Address,
    /// The offset of this range of pixels in the larger image.
    pub(crate) buffer_offset: usize,
    /// The number of bytes in this range of pixels.
    ///
    /// Remember that each pixel is *two* bytes.
    pub(crate) length: usize,
}

/// A helper function for calculating the sensitivity correction coefficients
/// (Alpha<sub>corr<sub>range<sub>n</sub></sub></sub>) for the different temperature ranges.
///
/// This function will `panic` if the passed in slices both do not have exactly `N` elements.
pub(crate) fn alpha_correction_coefficients<const NUM_RANGES: usize>(
    basic_range: usize,
    corner_temperatures: &[i16],
    k_s_to: &[f32],
) -> [f32; NUM_RANGES] {
    // This is the actual calculation. The values are built up recursively from the base case of
    // the basic range (which doesn't need correcting, so it's 1).
    // Memoizing would be nice here, but these calculations are done only once, at start up, so the
    // impact isn't that big.
    let results: ArrayVec<f32, NUM_RANGES> = (0..NUM_RANGES)
        .map(|n| alpha_corr_n(n, basic_range, corner_temperatures, k_s_to))
        .collect();
    results
        .into_inner()
        .expect("The Rust-range 0..NUM_RANGES should fill an array of NUM_RANGES elements")
}

/// The actual calculations for [alpha_correction_coefficients] as a recursive function. Memoizing
/// would be nice, but these calculations are only performed once, at start up.
fn alpha_corr_n(n: usize, basic_range: usize, ct: &[i16], k_s_to: &[f32]) -> f32 {
    match n.cmp(&basic_range) {
        core::cmp::Ordering::Equal => 1f32,
        core::cmp::Ordering::Less => {
            (1f32 + k_s_to[n] * f32::from(ct[n + 1] - ct[n])).recip()
                * alpha_corr_n(n + 1, basic_range, ct, k_s_to)
        }
        core::cmp::Ordering::Greater => {
            (1f32 + k_s_to[n - 1] * f32::from(ct[n] - ct[n - 1]))
                * alpha_corr_n(n - 1, basic_range, ct, k_s_to)
        }
    }
}