From b3bf510e14b584f2d71d77bddbe7479d1c00eaa4 Mon Sep 17 00:00:00 2001
From: MarcoMiano <58668074+MarcoMiano@users.noreply.github.com>
Date: Wed, 1 Jan 2025 20:14:37 +0100
Subject: [PATCH] drivers/sensor/mcp9808: Add MCP9808 temperature sensor driver

Add single-file module for the Microchip MCP9808 precision temperature
sensor. The module provides a class for reading the temperature and for
configuring the sensor. It has type/value checking for some of the
critical parts of the configuration as well as a debug mode for easier
testing during development.

It includes a test file designe to run directly on a board with a sensor
connected to test both the module and the sensor.

Both module and tests file are thoroughly documented in the code
directly. For more information read README.md on
https://github.com/MarcoMiano/mip-mcp9808
---
 .../drivers/sensor/mcp9808/manifest.py        |   9 +
 micropython/drivers/sensor/mcp9808/mcp9808.py | 721 ++++++++++++++++++
 .../drivers/sensor/mcp9808/test_mcp9808.py    | 379 +++++++++
 3 files changed, 1109 insertions(+)
 create mode 100644 micropython/drivers/sensor/mcp9808/manifest.py
 create mode 100644 micropython/drivers/sensor/mcp9808/mcp9808.py
 create mode 100644 micropython/drivers/sensor/mcp9808/test_mcp9808.py

diff --git a/micropython/drivers/sensor/mcp9808/manifest.py b/micropython/drivers/sensor/mcp9808/manifest.py
new file mode 100644
index 000000000..b941d68ab
--- /dev/null
+++ b/micropython/drivers/sensor/mcp9808/manifest.py
@@ -0,0 +1,9 @@
+metadata(
+    description="Microchip MCP9808 temperature sensor driver",
+    version="1.0.0",
+    license="MIT",
+    author="Marco Miano",
+)
+
+# opt=2 so line numbers are preserved in case of exceptions
+module("mcp9808.py", opt=2)
diff --git a/micropython/drivers/sensor/mcp9808/mcp9808.py b/micropython/drivers/sensor/mcp9808/mcp9808.py
new file mode 100644
index 000000000..cb6ebbcf2
--- /dev/null
+++ b/micropython/drivers/sensor/mcp9808/mcp9808.py
@@ -0,0 +1,721 @@
+"""MIT License
+
+Copyright (c) 2024 Marco Miano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+
+Microchip MCP9808 driver for MicroPython
+
+THE MCP9808 IS A COMPLEX SENSOR WITH MANY FEATURES. IS IT ADVISABLE TO READ THE DATASHEET.
+
+DO NOT ACCESS REGISTERS WITH ADDRESSES HIGHER THAN 0x08 AS THEY CONTAIN CALIBRATION CODES.
+DOING SO MAY IRREPARABLY DAMAGE THE SENSOR.
+
+This driver is a comprehensive implementation of the MCP9808 sensor's features. It is designed 
+to be easy to use and offers a high level of abstraction from the sensor's registers.
+The driver includes built-in error checking (such as type validation and bounds checking 
+for register access) and a debug mode to assist with development.
+
+
+
+Example usage:
+
+from mcp9808 import MCP9808, HYST_15, RES_0_125
+from machine import SoftI2C, Pin
+
+i2c = SoftI2C(scl=Pin(17), sda=Pin(16), freq=400000)
+t_sensor = MCP9808(i2c)
+
+# Get temeperature with deafult settings
+temperature: float = t_sensor.get_temperature()
+
+# Various settings
+t_sensor.set_hysteresis_mode(hyst_mode=HYST_15)
+t_sensor.set_resolution(resolution=RES_0_125)
+t_sensor.set_alert_crit_limit(crit_limit=65.0)
+t_sensor.set_alert_upper_limit(upper_limit=50.0)
+t_sensor.set_alert_lower_limit(lower_limit=-10.0)
+t_sensor.enable_alert()
+
+# Enable debug mode to get warnings
+t_sensor._debug = True
+
+
+# For more information, see the README file at
+https://github.com/MarcoMiano/mip-mcp9808 
+"""
+
+from machine import SoftI2C, I2C
+
+# Handy Constants
+HYST_00 = 0b00  # Hysteresis 0°C (power-up default)
+HYST_15 = 0b01  # Hysteresis 1,5°C
+HYST_30 = 0b10  # Hysteresis 3,0°C
+HYST_60 = 0b11  # Hysteresis 6,0°C
+
+RES_0_5 = 0b00  # Resolution 0.5°C
+RES_0_25 = 0b01  # Resolution 0.25°C
+RES_0_125 = 0b10  # Resolution 0.125°C
+RES_0_0625 = 0b11  # Resolution 0.0625°C (power-up default)
+
+
+class MCP9808(object):
+    """A class to interface with the Microchip MCP9808 temperature sensor over I2C.
+
+    Attributes:
+        ``BASE_ADDR`` (int): The base I2C address for the MCP9808 sensor.
+        ``REG_CFG`` (int): Address of the configuration register.
+        ``REG_ATU`` (int): Address of the alert temperature upper boundary trip register.
+        ``REG_ATL`` (int): Address of the alert temperature lower boundary trip register.
+        ``REG_ATC`` (int): Address of the critical temperature trip register.
+        ``REG_TEM`` (int): Address of the temperature register.
+        ``REG_MFR`` (int): Address of the manufacturer ID register.
+        ``REG_DEV`` (int): Address of the device ID register.
+        ``REG_RES`` (int): Address of the resolution register.
+    """
+
+    BASE_ADDR = 0x18
+    #######################################################
+    # DON'T ACCESS REGISTER WITH ADDRESS HIGHER THAN 0X08 #
+    #######################################################
+    REG_CFG = 0x01  # Config register
+    REG_ATU = 0x02  # Alert Temperature Upper boundary trip register
+    REG_ATL = 0x03  # Alert Temperature Lower boundary trip register
+    REG_ATC = 0x04  # Critical Temperature Trip register
+    REG_TEM = 0x05  # Temperature register
+    REG_MFR = 0x06  # Manufacturer ID register
+    REG_DEV = 0x07  # Device ID register
+    REG_RES = 0x08  # Resolution register
+
+    def __init__(
+        self,
+        i2c: SoftI2C | I2C,
+        addr: int | None = None,
+        A0: bool = False,
+        A1: bool = False,
+        A2: bool = False,
+        debug: bool = False,
+    ) -> None:
+        """Initialize the sensor object instance.
+
+        Args:
+            ``i2c`` (SoftI2C | I2C): The I2C bus instance to use for communication.
+            ``addr`` (int | None, optional): The I2C address of the sensor. If not provided,
+                the address will be calculated based on A0, A1, and A2. Defaults to None.
+            ``A0`` (bool, optional): The state of address pin A0. Defaults to False.
+            ``A1`` (bool, optional): The state of address pin A1. Defaults to False.
+            ``A2`` (bool, optional): The state of address pin A2. Defaults to False.
+            ``debug`` (bool, optional): Enable or disable debug mode. Defaults to False.
+        Returns:
+            ``None``
+        """
+
+        self._i2c: SoftI2C | I2C = i2c
+        self._debug: bool = debug
+        if addr:
+            self._addr = addr
+        else:
+            self._addr: int = self.BASE_ADDR | (A2 << 2) | (A1 << 1) | A0
+        self._check_device()
+        self._get_config()
+
+    def _check_device(self) -> None:
+        """Checks the device's manufacturer ID and device ID to ensure it is the correct device.
+
+        Raises:
+            ``Exception``: If the manufacturer ID does not match the expected value.
+            ``Exception``: If the device ID does not match the expected value.
+        Warns:
+            If the hardware revision does not match the expected value and debug mode is enabled.
+        Returns:
+            ``None``
+        """
+
+        self._mfr_id: bytes = self._i2c.readfrom_mem(self._addr, self.REG_MFR, 2)
+        if self._mfr_id != b"\x00\x54":
+            raise Exception(f"Invalid manufacturer ID {self._mfr_id}")
+        self._dev_id: bytes = self._i2c.readfrom_mem(self._addr, self.REG_DEV, 2)
+        if self._dev_id[0] != 4:
+            raise Exception(f"Invalid device ID {self._dev_id[0]}")
+        if self._dev_id[1] != 0 and self._debug == True:
+            print(
+                f"[WARN] Module written for HW revision 0 but got {self._dev_id[1]}.",
+            )
+
+    def _get_config(self) -> None:
+        """Private method to read the configuration register from the sensor.
+
+        This method reads 2 bytes from the configuration register of the sensor.
+        It then parses the bytes to update the following instance attributes:
+            ``_hyst_mode``: Hysteresis mode (int)
+            ``_shdn``: Shutdown mode (bool)
+            ``_crit_lock``: Critical temperature register lock (bool)
+            ``_alerts_lock``: Alerts temperature registers lock (bool)
+            ``_irq_clear_bit``: Interrupt clear bit (bool)
+            ``_alert``: Alert output status (bool)
+            ``_alert_ctrl``: Alert control (bool)
+            ``_alert_sel``: Alert output select (bool)
+            ``_alert_pol``: Alert output polarity (bool)
+            ``_alert_mode``: Alert output mode (bool)
+        Returns:
+            ``None``
+        """
+
+        buf: bytes = self._i2c.readfrom_mem(self._addr, self.REG_CFG, 2)
+        self._hyst_mode: int = (buf[0] >> 1) & 0x03
+        self._shdn = bool(buf[0] & 0x01)
+        self._crit_lock = bool(buf[1] & 0x80)
+        self._alerts_lock = bool(buf[1] & 0x40)
+        self.irq_clear_bit = bool(buf[1] & 0x20)
+        self._alert = bool(buf[1] & 0x10)
+        self._alert_ctrl = bool(buf[1] & 0x08)
+        self._alert_sel = bool(buf[1] & 0x04)
+        self.alert_pol = bool(buf[1] & 0x02)
+        self._alert_mode = bool(buf[1] & 0x01)
+
+    def _set_config(
+        self,
+        hyst_mode: int | None = None,
+        shdn: bool | None = None,
+        crit_lock: bool | None = None,
+        alerts_lock: bool | None = None,
+        irq_clear_bit: bool = False,
+        alert_ctrl: bool | None = None,
+        alert_sel: bool | None = None,
+        alert_pol: bool | None = None,
+        alert_mode: bool | None = None,
+    ) -> None:
+        """Private method to set the configuration of the sensor.
+
+        Parameters:
+            ``hyst_mode`` (int | None): Hysteresis mode. Valid values are HYST_00, HYST_15, HYST_30, HYST_60.
+            ``shdn (bool`` | None): Shutdown mode.
+            ``crit_lock`` (bool | None): Critical temperature register lock.
+            ``alerts_lock`` (bool | None): Alerts temperature registers lock.
+            ``irq_clear``_bit (bool): Interrupt clear bit.
+            ``alert_ctrl`` (bool | None): Alert output control.
+            ``alert_sel`` (bool | None): Alert output select.
+            ``alert_pol`` (bool | None): Alert output polarity.
+            ``alert_mode`` (bool | None): Alert output mode.
+        Raises:
+            ``ValueError``: If hyst_mode is not one of the valid values.
+            ``TypeError``: If any of the boolean parameters are not of type bool.
+        Returns:
+            ``None``
+        """
+
+        if hyst_mode is None:
+            hyst_mode = self._hyst_mode
+        if shdn is None:
+            shdn = self._shdn
+        if crit_lock is None:
+            crit_lock = self._crit_lock
+        if alerts_lock is None:
+            alerts_lock = self._alerts_lock
+        if alert_ctrl is None:
+            alert_ctrl = self._alert_ctrl
+        if alert_sel is None:
+            alert_sel = self._alert_sel
+        if alert_pol is None:
+            alert_pol = self.alert_pol
+        if alert_mode is None:
+            alert_mode = self._alert_mode
+
+        # Type/value check the parameters
+        if hyst_mode not in [HYST_00, HYST_15, HYST_30, HYST_60]:
+            raise ValueError(
+                f"hyst_mode: {hyst_mode}. Value should be between 0 and 3 inclusive."
+            )
+        if shdn.__class__ != bool:
+            raise TypeError(
+                f"shdn: {shdn} {shdn.__class__}. Expecting a bool.",
+            )
+        if crit_lock.__class__ != bool:
+            raise TypeError(
+                f"crit_lock: {crit_lock} {crit_lock.__class__}. Expecting a bool.",
+            )
+        if alerts_lock.__class__ != bool:
+            raise TypeError(
+                f"alerts_lock: {alerts_lock} {alerts_lock.__class__}. Expecting a bool.",
+            )
+        if irq_clear_bit.__class__ != bool:
+            raise TypeError(
+                f"irq_clear_bit: {irq_clear_bit} {irq_clear_bit.__class__}. Expecting a bool.",
+            )
+        if alert_ctrl.__class__ != bool:
+            raise TypeError(
+                f"alert_ctrl: {alert_ctrl} {alert_ctrl.__class__}. Expecting a bool.",
+            )
+        if alert_sel.__class__ != bool:
+            raise TypeError(
+                f"alert_sel: {alert_sel} {alert_sel.__class__}. Expecting a bool.",
+            )
+        if alert_pol.__class__ != bool:
+            raise TypeError(
+                f"alert_pol: {alert_pol} {alert_pol.__class__}. Expecting a bool.",
+            )
+        if alert_mode.__class__ != bool:
+            raise TypeError(
+                f"alert_mode: {alert_mode} {alert_mode.__class__}. Expecting a bool.",
+            )
+
+        # Build the send buffer
+        buf = bytearray(b"\x00\x00")
+        buf[0] = (hyst_mode << 1) | shdn
+        buf[1] = (
+            (crit_lock << 7)
+            | (alerts_lock << 6)
+            | (irq_clear_bit << 5)
+            | (alert_ctrl << 3)
+            | (alert_sel << 2)
+            | (alert_pol << 1)
+            | alert_mode
+        )
+        # Write the buffer to the sensor
+        self._i2c.writeto_mem(self._addr, self.REG_CFG, buf)
+        self._get_config()
+        # Check if the configuration was set correctly id debug mode is enabled
+        if self._debug:
+            if self._hyst_mode != hyst_mode:
+                print(
+                    f"[WARN] Failed to set hyst_mode. Set {hyst_mode} got {self._hyst_mode}",
+                )
+            if self._shdn != shdn:
+                print(
+                    f"[WARN] Failed to set shdn. Set {shdn} got {self._shdn}",
+                )
+            if self._crit_lock != crit_lock:
+                print(
+                    f"[WARN] Failed to set crit_lock. Set {crit_lock} got {self._crit_lock}",
+                )
+            if self.irq_clear_bit == True:
+                print(
+                    "[WARN] Something wrong with irq_clear_bit. Should always read False"
+                )
+            if self._alerts_lock != alerts_lock:
+                print(
+                    f"[WARN] Failed to set alerts_lock. Set {alerts_lock} got {self._alerts_lock}",
+                )
+            if self._alert_ctrl != alert_ctrl:
+                print(
+                    f"[WARN] Failed to set alert_ctrl. Set {alert_ctrl} got {self._alert_ctrl}.",
+                )
+            if self._alert_sel != alert_sel:
+                print(
+                    f"[WARN] Failed to set alert_sel. Set {alert_sel} got {self._alert_sel}.",
+                )
+            if self.alert_pol != alert_pol:
+                print(
+                    f"[WARN] Failed to set alert_pol. Set {alert_pol} got {self.alert_pol}.",
+                )
+            if self._alert_mode != alert_mode:
+                print(
+                    f"[WARN] Failed to set alert_mode. Set {alert_mode} got {self._alert_mode}.",
+                )
+
+    def _set_alert_limit(self, limit: float | int, register: int) -> None:
+        """Private method to set the alert limit register.
+
+        Inteded to be used by the set_alert_XXXXX_limit wrapper methods.
+        Args:
+            ``limit`` (float | int): The temperature limit to set. Must be between -128 and 127.
+            ``register`` (int): The register address to write the limit to.
+        Raises:
+            ``TypeError``: If the limit is not a float or int.
+            ``ValueError``: If the limit is out of the range [-128, 127].
+            ``ValueError``: If the register address is not valid.
+        Debug:
+            - Issue a warning if the threshold is outside of the operational range.
+            - Issue a warning if the alert limit was not set correctly.
+        Returns:
+            ``None``
+        """
+
+        if not limit.__class__ in [float, int]:
+            raise TypeError(
+                f"limit: {limit} {limit.__class__}. Expecting float|int.",
+            )
+        if limit < -128 or limit > 127:
+            raise ValueError("Temperature out of range [-128, 127]")
+        if (limit < -40 or limit > 125) and self._debug == True:
+            print(
+                "[WARN] Temperature outside of operational range, limit won't be ever reached.",
+            )
+        if register not in [self.REG_ATU, self.REG_ATL, self.REG_ATC]:
+            raise ValueError(f"Invalid register address {register}")
+
+        buf = bytearray(b"\x00\x00")
+
+        # If limit is negative set sign fifth bit ON otherwise leave it OFF
+        if limit < 0:
+            sign = 0x10
+        else:
+            sign = 0x00
+
+        # If limit is between -1 and 0 (like -0.25) set integral to 0xFF (-0 in 2's complement)
+        if -1 < limit < 0:
+            integral: int = 0xFF
+        # Otherwise truncate limit to a int and keep only the rightmost byte
+        else:
+            integral: int = int(limit) & 0xFF
+
+        # Calculate the fractional part by keeping the 2 rightmost bits of the integer division
+        # of 0.25 (the sensitivity) and the remainder part of the decimal part of limit
+        frac_normal: int = int((limit - integral) / 0.25) & 0x03
+
+        # Build the send buffer highest byte combining (bitwise-or) sign and the integral
+        # right-shifted by 4
+        buf[0] = sign | (integral >> 4)
+        # Build the send buffer lowest byte combining (bitwise-or) the integral
+        # left-shifted by 4 and the fractional part left shifted by 2 (last 2 bit are 0)
+        buf[1] = (integral << 4) | (frac_normal << 2)
+
+        self._i2c.writeto_mem(self._addr, register, buf)
+
+        if self._debug:
+            check: bytes = self._i2c.readfrom_mem(self._addr, register, 2)
+            if check != buf:
+                print(
+                    f"[WARN] Failed to set alert limit. Set {buf[0]:08b}-{buf[1]:08b}",
+                    f"but got {check[0]:08b}-{check[1]:08b}",
+                )
+
+    def shutdown(self) -> None:
+        """Put the sensor in low power mode.
+
+        Returns:
+            ``None``
+        """
+        self._set_config(shdn=True)
+
+    def wake(self) -> None:
+        """Wake the sensor from low power mode.
+
+        Returns:
+            ``None``
+        """
+        self._set_config(shdn=False)
+
+    def lock_crit_limit(self) -> None:
+        """Locks the critical temperature limit.
+
+        When the critical temperature limit is locked, it cannot be changed
+        until the sensor is power cycled.
+        Returns:
+            ``None``
+        """
+        self._set_config(crit_lock=True)
+
+    def lock_alerts_limit(self) -> None:
+        """Locks the alerts limits.
+
+        When the alerts limits are locked, they cannot be changed
+        until the sensor is power cycled.
+        Returns:
+            ``None``
+        """
+        self._set_config(alerts_lock=True)
+
+    def irq_clear(self) -> None:
+        """Clears the interrupt output.
+
+        This method clears the interrupt output.
+        Returns:
+            ``None``
+        """
+        self._set_config(irq_clear_bit=True)
+
+    def get_alert_status(self) -> bool:
+        """Get the alert status.
+
+        This method reads the alert status from the sensor.
+        Returns:
+            ``bool``: The alert status.
+        """
+        self._get_config()
+        return self.alert
+
+    def enable_alert(self) -> None:
+        """Enable the alert output.
+
+        Returns:
+            ``None``
+        """
+        self._set_config(alert_ctrl=True)
+
+    def disable_alert(self) -> None:
+        """Disable the alert output.
+
+        Returns:
+            ``None``
+        """
+        self._set_config(alert_ctrl=False)
+
+    def set_alert_threshold(self, only_crit=False) -> None:
+        """Set the alert output select.
+
+        Select if the alert output should be activated only by the critical limit or both critical
+        and upper/lower limits.
+        Args:
+            ``only_crit`` (bool, optional): Set the alert output to only critical. Defaults to False.
+        Returns:
+            ``None``
+        """
+        self._set_config(alert_sel=only_crit)
+
+    def set_alert_polarity(self, active_high=False) -> None:
+        """Set the alert output polarity.
+
+        Set the alert output polarity to active high or active low.
+        Args:
+            ``active_high`` (bool, optional): Set the alert output polarity to active high.
+                                          Defaults to False.
+        Returns:
+            ``None``
+        """
+        self._set_config(alert_pol=active_high)
+
+    def set_alert_mode(self, irq=False) -> None:
+        """Set the alert output mode.
+
+        Set the alert output mode to interrupt or comparator.
+        Args:
+            ``irq`` (bool, optional): Set the alert output mode to interrupt. Defaults to False.
+        Returns:
+            ``None``
+        """
+        self._set_config(alert_mode=irq)
+
+    def set_alert_upper_limit(self, upper_limit: float | int) -> None:
+        """Set the alert upper limit.
+
+        Args:
+            upper_limit (float | int): The upper limit to set.
+                                       It will rounded to the nearest 0.25°C.
+        Raises:
+            ``TypeError``: If the limit is not a float or int.
+            ``ValueError``: If the limit is out of the range [-128, 127].
+        Debug:
+            - Issue a warning if the threshold is outside of the operational range.
+            - Issue a warning if the alert limit was not set correctly.
+        Returns:
+            ``None``
+        """
+        self._set_alert_limit(upper_limit, self.REG_ATU)
+
+    def set_alert_lower_limit(self, lower_limit: float | int) -> None:
+        """Set the alert lower limit.
+
+        Args:
+            lower_limit (float | int): The lower limit to set.
+        Raises:
+            ``TypeError``: If the limit is not a float or int.
+            ``ValueError``: If the limit is out of the range [-128, 127].
+        Debug:
+            - Issue a warning if the threshold is outside of the operational range.
+            - Issue a warning if the alert limit was not set correctly.
+        Returns:
+            ``None``
+        """
+        self._set_alert_limit(lower_limit, self.REG_ATL)
+
+    def set_alert_crit_limit(self, crit_limit: float | int) -> None:
+        """Set the alert critical limit.
+
+        Args:
+            crit_limit (float | int): The critical limit to set.
+        Raises:
+            ``TypeError``: If the limit is not a float or int.
+            ``ValueError``: If the limit is out of the range [-128, 127].
+        Debug:
+            - Issue a warning if the threshold is outside of the operational range.
+            - Issue a warning if the alert limit was not set correctly.
+        Returns:
+            ``None``
+        """
+        self._set_alert_limit(crit_limit, self.REG_ATC)
+
+    def get_temperature(self) -> float:
+        """Get the temperature from the sensor.
+
+        Returns:
+            ``float``: The temperature in degrees Celsius.
+        """
+        # Read temperature register from sensor
+        buf: bytes = self._i2c.readfrom_mem(self._addr, self.REG_TEM, 2)
+        # Extract the sign bit
+        sign: int = buf[0] & 0x10
+        # Calculate the 4 upper bit of the integral by left shifting the first byte by 4
+        upper: int = (buf[0] << 4) & 0xFF
+        # Calculate the 4 lower bit of the integral and the fractional part into a float
+        # dividing by 16 the second buf byte
+        lower: float = (buf[1] & 0xFF) / 16
+        # Calculate the temperature as a float, adding the upper byte (leftmost 4 bit of integral)
+        # and the lower byte (rightmost 4 bit of integral + fractional).
+        # In case of negative value subtract 256 from the sum to convert from 2's complement 8+4bit
+        # fractional value to negative float
+        temp: float = (upper + lower) - 256 if sign else upper + lower
+        return temp
+
+    def get_alert_triggers(self) -> tuple[bool, bool, bool]:
+        """Get the alert triggers.
+
+        Trigger bits are not influenced by the alert output mode (compare or interrupt) or by the
+        alert polarity (active high or active low) or by the alert control (enable or disable).
+        Returns:
+            ``tuple[bool, bool, bool]``: A tuple containing the alert triggers.
+                The first element is True if the temperature is greater or equal to the critical
+                limit.
+                The second element is True if the temperature is greater than the upper limit.
+                The third element is True if the temperature is less than the lower limit.
+        """
+        # Read temperature register from sensor
+        buf: bytes = self._i2c.readfrom_mem(self._addr, self.REG_TEM, 2)
+        # Extract the 16th bit (last), Ta vs. Tcrit.    False = Ta < Tcrit   | True = Ta >= Tcrit
+        ta_tcrit = bool(buf[0] & 0x80)
+        # Extract the 15th bit, Ta vs. Tupper.          False = Ta <= Tupper | True = Ta > Tupper
+        ta_tupper = bool(buf[0] & 0x40)
+        # Extract the 14th bit, Ta vs Tlower.           False = Ta >= Tlower | True = Ta < Tlower
+        ta_tlower = bool(buf[0] & 0x20)
+
+        return ta_tcrit, ta_tupper, ta_tlower
+
+    def set_resolution(self, resolution=RES_0_0625) -> None:
+        """Set the resolution of the sensor.
+
+        Args:
+            resolution (int, optional): The resolution to set.
+                Valid values are RES_0_5, RES_0_25, RES_0_125, RES_0_0625.
+                Defaults to RES_0_0625.
+        Raises:
+            ValueError: If the resolution is not a valid value.
+        Debug:
+            - Issue a warning if the resolution was not set correctly.
+        Returns:
+            ``None``
+        """
+        # Check if resolution is a compatible value
+        if not resolution in [RES_0_5, RES_0_25, RES_0_125, RES_0_0625]:
+            raise ValueError(
+                f"Invalid resolution: {resolution}. Value should be between 0 and 3 inclusive.",
+            )
+
+        buf = bytearray(b"\x00")
+
+        buf[0] |= resolution & 0x03
+        self._i2c.writeto_mem(self._addr, self.REG_RES, buf)
+        if self._debug:
+            check = self._i2c.readfrom_mem(self._addr, self.REG_RES, 1)
+            if check != buf:
+                print(
+                    f"[WARN] Failed to set resolution. Set {resolution} got {check[0]}"
+                )
+
+    @property
+    def hyst_mode(self) -> int:
+        """Get the hysteresis mode.
+
+        Returns:
+            ``int``: The hysteresis mode.
+        """
+        self._get_config()
+        return self._hyst_mode
+
+    @hyst_mode.setter
+    def hyst_mode(self, hyst_mode: int) -> None:
+        """Set the hysteresis mode.
+
+        Args:
+            ``hyst_mode`` (int): The hysteresis mode to set.
+                Valid values are HYST_00, HYST_15, HYST_30, HYST_60.
+        """
+        self._set_config(hyst_mode=hyst_mode)
+
+    @property
+    def shdn(self) -> bool:
+        """Get the shutdown mode.
+
+        Returns:
+            ``bool``: The shutdown mode.
+        """
+        self._get_config()
+        return self._shdn
+
+    @property
+    def crit_lock(self) -> bool:
+        """Get the critical temperature register lock.
+
+        Returns:
+            ``bool``: The critical temperature register lock.
+        """
+        self._get_config()
+        return self._crit_lock
+
+    @property
+    def alerts_lock(self) -> bool:
+        """Get the alerts temperature registers lock.
+
+        Returns:
+            ``bool``: The alerts temperature registers lock.
+        """
+        self._get_config()
+        return self._alerts_lock
+
+    @property
+    def alert(self) -> bool:
+        """Get the alert control.
+
+        Returns:
+            ``bool``: The alert control.
+        """
+        self._get_config()
+        return self._alert
+
+    @property
+    def alert_ctrl(self) -> bool:
+        """Get the alert control.
+
+        Returns:
+            ``bool``: The alert control.
+        """
+        self._get_config()
+        return self._alert_ctrl
+
+    @property
+    def alert_sel(self) -> bool:
+        """Get the alert output select.
+
+        Returns:
+            ``bool``: The alert output select.
+        """
+        self._get_config()
+        return self._alert_sel
+
+    @property
+    def alert_mode(self) -> bool:
+        """Get the alert output mode.
+
+        Returns:
+            ``bool``: The alert output mode.
+        """
+        self._get_config()
+        return self._alert_mode
diff --git a/micropython/drivers/sensor/mcp9808/test_mcp9808.py b/micropython/drivers/sensor/mcp9808/test_mcp9808.py
new file mode 100644
index 000000000..c291b972a
--- /dev/null
+++ b/micropython/drivers/sensor/mcp9808/test_mcp9808.py
@@ -0,0 +1,379 @@
+"""MIT License
+
+Copyright (c) 2024 Marco Miano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+
+Microchip MCP9808 driver/sensor test suite for MicroPython
+
+THE MCP9808 IS A COMPLEX SENSOR WITH MANY FEATURES. IS IT ADVISABLE TO READ THE DATASHEET.
+
+DO NOT ACCESS REGISTERS WITH ADDRESSES HIGHER THAN 0x08 AS THEY CONTAIN CALIBRATION CODES.
+DOING SO MAY IRREPARABLY DAMAGE THE SENSOR.
+
+This test suite is designed to check the correct operation of the MCP9808 sensor driver and the 
+sensor itself. It is advisable to run this test suite if anything is changed in the driver code,
+or if the sensor is not behaving right.
+This test suite is written and tested on a Raspberry Pi Pico W Board with MicroPython v1.24.1.
+
+
+
+Pin connections:
+    - POWER: pin15 
+             The sensor is powered from a GPIO pin to be able to power cycle the sensor during the 
+             tests.
+             CHECK THAT YOUR GPIO PIN CAN SUPPLY ENOUGH CURRENT AND VOLTAGE TO POWER THE SENSOR. 
+             (2.7V-5.5V AT 0.4mA)
+    - SDA:   pin16
+    - SCL:   pin17
+    - ALERT: pin18
+             The sensor alert pin is connected to a GPIO pin to check if the sensor is triggering 
+             alerts. The pin is pulled to VCC via the board internal pull-up resistor.
+             The pin is active low. Wire an external pull-up resistor to VCC if needed.
+
+Prerequisites:
+    - Install the unittest module in the MicroPython device.
+    - Install the MCP9808 driver in the MicroPython device.
+    - Wire the sensor to the board as described above.
+    - Run the test suite.
+"""
+
+import unittest
+import mcp9808
+from mcp9808 import MCP9808
+from machine import SoftI2C, Pin
+from time import sleep_ms
+
+##############################################
+# Change the pin numbers to match your setup #
+##############################################
+power_pin = Pin(15, Pin.OUT)
+alert_pin = Pin(18, Pin.IN, pull=Pin.PULL_UP)
+i2c_bus = SoftI2C(scl=Pin(17), sda=Pin(16), freq=400000)
+
+
+class TestMCP9808(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.power: Pin = power_pin
+        cls.alert: Pin = alert_pin
+        cls.i2c: SoftI2C = i2c_bus
+        cls.sensor = MCP9808(cls.i2c)
+
+    def setUp(self) -> None:
+        self.power.off()
+        sleep_ms(20)
+        self.power.on()
+        sleep_ms(2000)
+
+    def test_powerup_defaults(self) -> None:
+        self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_00)
+        self.assertFalse(self.sensor.shdn)
+        self.assertFalse(self.sensor.crit_lock)
+        self.assertFalse(self.sensor.alerts_lock)
+        self.assertFalse(self.sensor.irq_clear_bit)
+        self.assertFalse(self.sensor.alert)
+        self.assertFalse(self.sensor.alert_ctrl)
+        self.assertFalse(self.sensor.alert_sel)
+        self.assertFalse(self.sensor.alert_pol)
+        self.assertFalse(self.sensor.alert_mode)
+
+    def test_hysteresis_set(self) -> None:
+        self.sensor.hyst_mode = mcp9808.HYST_15
+        self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_15)
+        self.sensor.hyst_mode = mcp9808.HYST_30
+        self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_30)
+        self.sensor.hyst_mode = mcp9808.HYST_60
+        self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_60)
+        self.sensor.hyst_mode = mcp9808.HYST_00
+        self.assertEqual(self.sensor.hyst_mode, mcp9808.HYST_00)
+
+    def test_shutdown(self) -> None:
+        self.sensor.shutdown()
+        self.assertTrue(self.sensor.shdn)
+        self.sensor.wake()
+        self.assertFalse(self.sensor.shdn)
+
+    def test_crit_lock(self) -> None:
+        # Lock critical limit register
+        self.sensor.lock_crit_limit()
+        # Check if the critical limit register is locked
+        self.assertTrue(self.sensor.crit_lock)
+        # Try to enable alerts
+        self.sensor.enable_alert()
+        # Alerts should not be enabled
+        self.assertFalse(self.sensor.alert_ctrl)
+        # Reset sensor
+        self.setUp()
+        # Check if the critical limit register is unlocked
+        self.assertFalse(self.sensor.crit_lock)
+
+    def test_alerts_lock(self) -> None:
+        # Lock alerts limit registers
+        self.sensor.lock_alerts_limit()
+        # Check if the alerts limit registers are locked
+        self.assertTrue(self.sensor.alerts_lock)
+        # Try to enable alerts
+        self.sensor.enable_alert()
+        # Alerts should not be enabled
+        self.assertTrue(self.sensor.alerts_lock)
+        # Reset sensor
+        self.setUp()
+        # Check if the alerts limit registers are unlocked
+        self.assertFalse(self.sensor.alerts_lock)
+
+    def test_alert_control(self) -> None:
+        # Get current temperature
+        temp: float = self.sensor.get_temperature()
+        # Enable alerts
+        self.sensor.enable_alert()
+        # Check if alerts are enabled
+        self.assertTrue(self.sensor.alert_ctrl)
+        # Set lower limit to current temperature + 10°C
+        self.sensor.set_alert_lower_limit(temp + 10)
+        sleep_ms(10)
+        # Check if hardware alert is triggered
+        self.assertEqual(self.alert.value(), 0)
+        # Disable alerts
+        self.sensor.disable_alert()
+        # Check if alerts are disabled
+        self.assertFalse(self.sensor.alert_ctrl)
+        # Check if hardware alert is cleared
+        self.assertEqual(self.alert.value(), 1)
+
+    def test_comp_lower_alerts(self) -> None:
+        # Get current temperature
+        temp: float = self.sensor.get_temperature()
+        # Check if alert is disabled and in comparator mode
+        self.assertFalse(self.sensor.alert_ctrl)
+        self.assertFalse(self.sensor.alert_mode)
+        # Set upper limit to 100°C
+        #     crit limit to 100°C
+        #     lower limit to current temperature + 10°C
+        self.sensor.set_alert_upper_limit(100)
+        self.sensor.set_alert_crit_limit(100)
+        self.sensor.set_alert_lower_limit(temp + 10)
+        # Enable alerts
+        self.sensor.enable_alert()
+        sleep_ms(10)
+        # Check if hardware alert is triggered
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is set
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, True))
+        # Set lower limit to current temperature - 10°C (simulating temperature increase)
+        self.sensor.set_alert_lower_limit(temp - 10)
+        sleep_ms(10)
+        # Check if hardware alert is cleared
+        self.assertEqual(self.alert.value(), 1)
+        # Check if correct alert trigger bit is cleared
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False))
+
+    def test_comp_upper_alerts(self) -> None:
+        # Get current temperature
+        temp: float = self.sensor.get_temperature()
+        # Check if alert is disabled and in comparator mode
+        self.assertFalse(self.sensor.alert_ctrl)
+        self.assertFalse(self.sensor.alert_mode)
+        # Set lower limit to 0°C
+        #     crit limit to 100°C
+        #     upper limit to current temperature - 10°C
+        self.sensor.set_alert_lower_limit(0)
+        self.sensor.set_alert_crit_limit(100)
+        self.sensor.set_alert_upper_limit(temp - 10)
+        # Enable alerts
+        self.sensor.enable_alert()
+        sleep_ms(10)
+        # Check if hardware alert is triggered
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is set
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False))
+        # Set lower limit to current temperature + 10°C (simulating temperature drop)
+        self.sensor.set_alert_upper_limit(temp + 10)
+        sleep_ms(10)
+        # Check if hardware alert is cleared
+        self.assertEqual(self.alert.value(), 1)
+        # Check if correct alert trigger bit is cleared
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False))
+
+    def test_comp_crit_alerts(self) -> None:
+        # Get current temperature
+        temp: float = self.sensor.get_temperature()
+        # Check if alert is disabled and in comparator mode
+        self.assertFalse(self.sensor.alert_ctrl)
+        self.assertFalse(self.sensor.alert_mode)
+        # Set lower limit to 0°C
+        #     upper limit to 100°C
+        #     crit limit to current temperature - 10°C
+        self.sensor.set_alert_lower_limit(0)
+        self.sensor.set_alert_upper_limit(100)
+        self.sensor.set_alert_crit_limit(temp - 10)
+        # Enable alerts
+        self.sensor.enable_alert()
+        sleep_ms(10)
+        # Check if hardware alert is triggered
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is set
+        self.assertEqual(self.sensor.get_alert_triggers(), (True, False, False))
+        # Set lower limit to current temperature + 10°C (simulating temperature drop)
+        self.sensor.set_alert_crit_limit(temp + 10)
+        sleep_ms(10)
+        # Check if hardware alert is cleared
+        self.assertEqual(self.alert.value(), 1)
+        # Check if correct alert trigger bit is cleared
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False))
+
+    def test_irq_lower_alerts(self) -> None:
+        # Get current temperature
+        temp: float = self.sensor.get_temperature()
+        # Check if alert is disabled and in comparator mode
+        self.assertFalse(self.sensor.alert_ctrl)
+        self.assertFalse(self.sensor.alert_mode)
+        # Set and check alert mode to IRQ
+        self.sensor.set_alert_mode(irq=True)
+        self.assertTrue(self.sensor.alert_mode)
+        # Set lower limit to current temperature + 10°C
+        #     upper limit to 100°C
+        #     crit limit to 100°C
+        self.sensor.set_alert_lower_limit(temp + 10)
+        self.sensor.set_alert_upper_limit(100)
+        self.sensor.set_alert_crit_limit(100)
+        # Enable alerts
+        self.sensor.enable_alert()
+        sleep_ms(10)
+        # Check if hardware IRQ is triggered
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is set
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, True))
+        # Clear IRQ
+        self.sensor.irq_clear()
+        # Check if hardware IRQ is cleared
+        self.assertEqual(self.alert.value(), 1)
+        # Check if correct alert trigger bit is still set
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, True))
+        # Set lower limit to current temperature - 10°C (simulating temperature increase)
+        self.sensor.set_alert_lower_limit(temp - 10)
+        sleep_ms(10)
+        # Check if hardware IRQ is triggered
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is cleared
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False))
+        # Clear IRQ
+        self.sensor.irq_clear()
+        # Check if alert is cleared
+        self.assertEqual(self.alert.value(), 1)
+        # Check if correct alert trigger bit is cleared
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False))
+
+    def test_irq_upper_alerts(self) -> None:
+        # Get current temperature
+        temp: float = self.sensor.get_temperature()
+        # Check if alert is disabled and in comparator mode
+        self.assertFalse(self.sensor.alert_ctrl)
+        self.assertFalse(self.sensor.alert_mode)
+        # Set and check alert mode to IRQ
+        self.sensor.set_alert_mode(irq=True)
+        self.assertTrue(self.sensor.alert_mode)
+        # Set lower limit 0°C
+        #     upper limit to current temperature - 10°C
+        #     crit limit to 100°C
+        self.sensor.set_alert_lower_limit(0)
+        self.sensor.set_alert_upper_limit(temp - 10)
+        self.sensor.set_alert_crit_limit(100)
+        # Enable alerts
+        self.sensor.enable_alert()
+        sleep_ms(10)
+        # Check if hardware IRQ is triggered
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is set
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False))
+        # Clear IRQ
+        self.sensor.irq_clear()
+        # Check if hardware IRQ is cleared
+        self.assertEqual(self.alert.value(), 1)
+        # Check if correct alert trigger bit is still set
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False))
+        # Set upper limit to current temperature + 10°C (simulating temperature drop)
+        self.sensor.set_alert_upper_limit(temp + 10)
+        sleep_ms(10)
+        # Check if hardware IRQ is triggered
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is cleared
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False))
+        # Clear IRQ
+        self.sensor.irq_clear()
+        # Check if alert is cleared
+        self.assertEqual(self.alert.value(), 1)
+        # Check if correct alert trigger bit is cleared
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, False, False))
+
+    def test_irq_crit_alerts(self) -> None:
+        # Get current temperature
+        temp: float = self.sensor.get_temperature()
+        # Check if alert is disabled and in comparator mode
+        self.assertFalse(self.sensor.alert_ctrl)
+        self.assertFalse(self.sensor.alert_mode)
+        # Set and check alert mode to IRQ
+        self.sensor.set_alert_mode(irq=True)
+        self.assertTrue(self.sensor.alert_mode)
+        # Set lower limit 0°C
+        #     upper limit to current temperature - 10°C
+        #     crit limit to 100°C
+        self.sensor.set_alert_lower_limit(0)
+        self.sensor.set_alert_upper_limit(temp - 10)
+        self.sensor.set_alert_crit_limit(100)
+        # Enable alerts (first IRQ from upper limit)
+        self.sensor.enable_alert()
+        sleep_ms(10)
+        # Check if alert is triggered
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is set
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False))
+        # Clear IRQ
+        self.sensor.irq_clear()
+        # Check if alert is cleared
+        self.assertEqual(self.alert.value(), 1)
+        # Check if correct alert trigger bit is still set
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False))
+        # Set crit limit to current temperature - 5°C (to trigger second IRQ from crit limit)
+        self.sensor.set_alert_crit_limit(temp - 5)
+        sleep_ms(10)
+        # Check if alert is triggered (second IRQ from crit limit)
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is set
+        self.assertEqual(self.sensor.get_alert_triggers(), (True, True, False))
+        # Clear IRQ
+        self.sensor.irq_clear()
+        # Check if alert is NOT cleard
+        self.assertEqual(self.alert.value(), 0)
+        # Check if correct alert trigger bit is still set
+        self.assertEqual(self.sensor.get_alert_triggers(), (True, True, False))
+        # Set crit limit to current temperature + 5°C (simulating temperature drop)
+        self.sensor.set_alert_crit_limit(temp + 5)
+        sleep_ms(10)
+        # Check if alert is cleared
+        self.assertEqual(self.alert.value(), 1)
+        # Check if correct alert trigger bit is cleared
+        self.assertEqual(self.sensor.get_alert_triggers(), (False, True, False))
+
+
+if __name__ == "__main__":
+    unittest.main()