Repository: leaphy-robotics/leaphy-micropython
Pull request #67: decorator for i2c check
Author: login: MartenPostma
Created: 2025-04-09T17:44:02Z | Merged: 2025-04-22T17:36:03Z

decorator to check if i2c is used and alive


Diff:
diff --git a/leaphymicropython/sensors/tof.py b/leaphymicropython/sensors/tof.py
index d19cf38..3d4c0a6 100644
--- a/leaphymicropython/sensors/tof.py
+++ b/leaphymicropython/sensors/tof.py
@@ -1,16 +1,11 @@
-"""This module provides time-of-flight related calculations."""  # Single-line docstring
+"""This module provides time-of-flight related calculations."""
 
-from machine import I2C, Pin  # pylint: disable=import-error
-from leaphymicropython.sensors.vl53l0x import VL53L0X  # pylint: disable=import-error
-from leaphymicropython.utils.i2c_helper import (  # pylint: disable=import-error
-    select_channel,
-)
-from leaphymicropython.utils.i2c_address_finder import (  # pylint: disable=import-error
-    is_device_address_visible,
-)
+from leaphymicropython.sensors.vl53l0x import VL53L0X
+from leaphymicropython.utils.i2c_helper import I2CDevice
+from leaphymicropython.utils.i2c_helper import handle_i2c_errors
 
 
-class TimeOfFlight:  # pylint: disable=too-many-instance-attributes
+class TimeOfFlight(I2CDevice):
     """
     Initializes the TimeOfFlight object.
 
@@ -40,8 +35,8 @@ class TimeOfFlight:  # pylint: disable=too-many-instance-attributes
         show_warnings (bool,optional): if True, show warnings
     """
 
-    MULTIPLEXER_ADDRESS = 0x70
-    TOF_ADDRESS = 0x29
+    # the following attribute is used by decorator handle_i2c_errors
+    ADDRESS = 0x29
 
     def __init__(
         self,
@@ -50,7 +45,7 @@ def __init__(
         scl_gpio_pin=13,
         bus_id=0,
         show_warnings=True,
-    ):  # pylint: disable=too-many-arguments
+    ):
         """
         Initializes the TimeOfFlight object.
 
@@ -60,70 +55,28 @@ def __init__(
             sda_gpio_pin (int, optional): The GPIO pin connected to the SDA line. Defaults to 12.
             scl_gpio_pin (int, optional): The GPIO pin connected to the SCL line. Defaults to 13.
             bus_id (int, optional): identifies a particular I2C peripheral, for example bus 0 or 1.
+            show_warnings (bool, optional): if True, show warning about device address not found
         """
-        self.channel = channel
-        self.sda_gpio_pin = sda_gpio_pin
-        self.scl_gpio_pin = scl_gpio_pin
-        self.bus_id = bus_id
-        self.reinitialize = True
-        self.show_warnings = show_warnings
-        self.i2c = None
+        super().__init__(channel, sda_gpio_pin, scl_gpio_pin, bus_id, show_warnings)
         self.tof = None
-        self.mugs_used = None
+        self.find_device(show_warnings=self.show_warnings)
 
-    def initialize_tof(self):
+    def initialize_device(self):
         """
         initialize external library
         """
-        self.i2c = I2C(
-            id=self.bus_id, scl=Pin(self.scl_gpio_pin), sda=Pin(self.sda_gpio_pin)
-        )
-        self.mugs_used = is_device_address_visible(
-            i2c=self.i2c, target_address=self.MULTIPLEXER_ADDRESS
-        )
-        if self.mugs_used:
-            select_channel(self.i2c, self.MULTIPLEXER_ADDRESS, self.channel)
-        sensor_visible = is_device_address_visible(
-            i2c=self.i2c, target_address=self.TOF_ADDRESS
-        )
-        if not sensor_visible:
-            if self.show_warnings:
-                print(
-                    f"can not find tof sensor (address should be {hex(self.TOF_ADDRESS)})"
-                )
-        return VL53L0X(self.i2c)
+        super().initialize_device()
+        self.tof = VL53L0X(self.i2c)
 
+    @handle_i2c_errors
     def get_distance(self):
         """
         Retrieves the distance measurement from the VL53L0X sensor.
 
-        If a multiplexer channel is specified (channel != 255),
+        If a multiplexer channel is specified,
         it selects the appropriate channel before reading from the sensor.
 
         Returns:
             int: The measured distance in millimeters.
         """
-        if self.reinitialize:
-            try:
-                self.tof = self.initialize_tof()
-                self.reinitialize = False
-            except OSError as ex:
-                if ex.errno == 5:
-                    value = None
-                else:
-                    raise ex
-
-        if self.reinitialize is False:
-            try:
-                if self.mugs_used:
-                    select_channel(self.i2c, self.MULTIPLEXER_ADDRESS, self.channel)
-                value = self.tof.ping()
-                self.reinitialize = False
-            except OSError as ex:
-                if ex.errno == 5:
-                    self.reinitialize = True
-                    value = None
-                else:
-                    raise ex
-
-        return value
+        return self.tof.ping()
diff --git a/leaphymicropython/utils/i2c_helper.py b/leaphymicropython/utils/i2c_helper.py
index e75dd36..af372a6 100644
--- a/leaphymicropython/utils/i2c_helper.py
+++ b/leaphymicropython/utils/i2c_helper.py
@@ -1,7 +1,62 @@
+"""This module provides helper functions for i2c."""
+
 import struct
+from machine import Pin, I2C
+from leaphymicropython.utils.i2c_address_finder import is_device_address_visible
 
 
-def select_channel(i2c, multiplexer_address, channel_number):
+def handle_i2c_errors(func):
+    """
+    Decorator to handle I2C errors.
+
+    This decorator wraps a function that interacts with I2C devices.
+    It checks if the I2C connection is alive and attempts to reinitialize
+    the connection if necessary. It also handles specific OSError exceptions
+    that may occur during I2C communication.
+
+    Args:
+        func: The function to be wrapped.
+
+    Returns:
+        The wrapped function.
+    """
+
+    def wrapper(*args, **kwargs):
+        instance = args[0]
+        # check if the class instance is a subclass of I2CDevice
+        if not isinstance(instance, I2CDevice):
+            return func(*args, **kwargs)
+        # if i2c is used, check if connection is alive
+        result = None
+        if instance.reinitialize:
+            try:
+                instance.initialize_i2c()
+                instance.find_device(show_warnings=instance.show_warnings)
+                instance.initialize_device()
+                instance.reinitialize = False
+            except OSError as ex:
+                if ex.errno == 5:
+                    result = None
+                else:
+                    raise ex
+
+        if not instance.reinitialize:
+            try:
+                instance.select_channel()
+                result = func(*args, **kwargs)
+                instance.reinitialize = False
+            except OSError as ex:
+                if ex.errno == 5:
+                    instance.reinitialize = True
+                    result = None
+                else:
+                    raise ex
+        return result
+
+    return wrapper
+
+
+def select_channel(i2c, multiplexer_address, channel_number) -> None:
     """
     Selects a channel on an I2C multiplexer.
 
@@ -26,12 +81,101 @@ def select_channel(i2c, multiplexer_address, channel_number):
         print("Invalid channel number. Please select a channel between 0 and 7 or 255.")
 
 
+class I2CDevice:  # pylint: disable=too-many-instance-attributes
+    """
+    Base class for I2C sensors and actuators.
+
+    This class provides a common interface and functionality for interacting with
+    I2C devices. It includes methods for initializing the I2C bus, finding the
+    device on the bus, and selecting a channel on an I2C multiplexer if one is used.
+    """
+
+    MULTIPLEXER_ADDRESS = 0x70
+    ADDRESS = None
+
+    def __init__(
+        self,
+        channel: int = 255,
+        sda_gpio_pin: int = 12,
+        scl_gpio_pin: int = 13,
+        bus_id: int = 0,
+        show_warnings: bool = True,
+    ):
+        self.reinitialize: bool = True
+        self.i2c = None
+        self.channel: int = channel
+        self.bus_id: int = bus_id
+        self.scl_gpio_pin: int = scl_gpio_pin
+        self.sda_gpio_pin: int = sda_gpio_pin
+        self.show_warnings: bool = show_warnings
+        self._mux_used = None
+        self.initialize_i2c()
+
+    def initialize_i2c(self) -> None:
+        """
+        Initializes the I2C bus.
+
+        This method sets up the I2C bus with the specified ID, SCL pin, and SDA pin.
+        It is called during the initialization of the I2C device.
+        """
+        self.i2c = I2C(
+            id=self.bus_id, scl=Pin(self.scl_gpio_pin), sda=Pin(self.sda_gpio_pin)
+        )
+        self._mux_used = None
+
+    def initialize_device(self) -> None:
+        """
+        Abstract method. Initializes the I2C device attached to the bus.
+        """
+
+    def find_device(self, show_warnings=True) -> None:
+        """Finds the I2C device on the bus.
+
+        This method checks if the device is visible on the I2C bus.
+        If a multiplexer is used, it selects the appropriate channel before
+        checking for the device. If the device is not found, it prints a warning
+        message if show_warnings is True.
+
+        Args:
+            show_warnings (bool, optional): If True, show a warning if the device is not found.
+            Defaults to True.
+        """
+        if self.is_mux_used():
+            select_channel(self.i2c, self.MULTIPLEXER_ADDRESS, self.channel)
+        device_visible = is_device_address_visible(
+            i2c=self.i2c, target_address=self.ADDRESS
+        )
+        if not device_visible and show_warnings:
+            print(f"can not find device (address should be {hex(self.ADDRESS)})")
+
+    def is_mux_used(self) -> bool:
+        """
+        Checks if a multiplexer is used.
+
+        This method checks (once!) if a multiplexer is used by checking if its address is visible on the I2C bus.
+        """
+        if self._mux_used is None:
+            self._mux_used = is_device_address_visible(
+                i2c=self.i2c, target_address=self.MULTIPLEXER_ADDRESS
+            )
+        return self._mux_used
+
+    def select_channel(self) -> None:
+        """
+        Selects the appropriate channel on the I2C multiplexer.
+
+        If a multiplexer is used, this method selects the specified channel.
+        """
+        if self.is_mux_used():
+            select_channel(self.i2c, self.MULTIPLEXER_ADDRESS, self.channel)
+
+
 class CBits:
     """
     Changes bits from a byte register
     """
 
-    def __init__(
+    def __init__(  # pylint: disable=too-many-arguments
         self,
         num_bits: int,
         register_address: int,


Reviews:
user: login: rmoesbergen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-13T17:01:35Z
user: login: rmoesbergen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-13T17:06:46Z
user: login: rmoesbergen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-13T17:07:56Z
user: login: rmoesbergen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T12:02:34Z
user: login: rmoesbergen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T12:03:52Z
user: login: rmoesbergen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T12:06:02Z
user: login: rmoesbergen
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-22T15:46:57Z

Review comments:
diff_hunk: @@ -1,6 +1,48 @@
+"""This module provides helper functions for i2c."""  # Single-line docstring
+
 import struct
 
 
+def i2c_is_it_used_and_alive(func):
path: leaphymicropython/utils/i2c_helper.py
user: login: rmoesbergen
body: zullen we hem `handle_i2c_errors` noemen?
created_at: 2025-04-13T17:01:35Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,48 @@
+"""This module provides helper functions for i2c."""  # Single-line docstring
+
 import struct
 
 
+def i2c_is_it_used_and_alive(func):
+    """check if i2c connection is used and if so, if it is alive before using it"""
+
+    def wrapper(*args, **kwargs):
+        instance = args[0]
+        # check if i2c is used
+        i2c_used = hasattr(instance, "i2c")
path: leaphymicropython/utils/i2c_helper.py
user: login: rmoesbergen
body: Dit kunnen we mooier oplossen met een abstracte "interface" class en een issubclass() check. Daar kunnen dan ook de benodigde methods in zoals initialize() etc.
created_at: 2025-04-13T17:06:46Z
author_association: MEMBER
diff_hunk: @@ -8,6 +8,9 @@
 from leaphymicropython.utils.i2c_address_finder import (  # pylint: disable=import-error
     is_device_address_visible,
 )
+from leaphymicropython.utils.i2c_helper import (  # pylint: disable=import-error
path: leaphymicropython/sensors/tof.py
user: login: rmoesbergen
body: die import-error moet wel oplosbaar zijn, misschien door in CI de workdir aan het PYTHONPATH toe te voegen 🤔 
created_at: 2025-04-13T17:07:56Z
author_association: MEMBER
diff_hunk: @@ -1,19 +1,11 @@
 """This module provides time-of-flight related calculations."""  # Single-line docstring
 
-from machine import I2C, Pin  # pylint: disable=import-error
-from leaphymicropython.sensors.vl53l0x import VL53L0X  # pylint: disable=import-error
-from leaphymicropython.utils.i2c_helper import (  # pylint: disable=import-error
-    select_channel,
-)
-from leaphymicropython.utils.i2c_address_finder import (  # pylint: disable=import-error
-    is_device_address_visible,
-)
-from leaphymicropython.utils.i2c_helper import (  # pylint: disable=import-error
-    i2c_is_it_used_and_alive,
-)
+from leaphymicropython.sensors.vl53l0x import VL53L0X
+from leaphymicropython.utils.i2c_helper import I2CSensorOrActuator
+from leaphymicropython.utils.i2c_helper import handle_i2c_errors
 
 
-class TimeOfFlight:  # pylint: disable=too-many-instance-attributes
+class TimeOfFlight(I2CSensorOrActuator):  # pylint: disable=too-many-instance-attributes
path: leaphymicropython/sensors/tof.py
user: login: rmoesbergen
body: Wat vind je van I2Device ipv. I2CSensorOrActuator?
created_at: 2025-04-18T12:02:34Z
author_association: MEMBER
diff_hunk: @@ -1,22 +1,37 @@
 """This module provides helper functions for i2c."""  # Single-line docstring
 
 import struct
+from machine import Pin, I2C
+from leaphymicropython.utils.i2c_address_finder import is_device_address_visible
 
 
-def i2c_is_it_used_and_alive(func):
-    """check if i2c connection is used and if so, if it is alive before using it"""
+def handle_i2c_errors(func):
+    """
+    Decorator to handle I2C errors.
+
+    This decorator wraps a function that interacts with I2C devices.
+    It checks if the I2C connection is alive and attempts to reinitialize
+    the connection if necessary. It also handles specific OSError exceptions
+    that may occur during I2C communication.
 
+    Args:
+        func: The function to be wrapped.
+
+    Returns:
+        The wrapped function.
+    """
     def wrapper(*args, **kwargs):
         instance = args[0]
-        # check if i2c is used
-        i2c_used = hasattr(instance, "i2c")
-        if not i2c_used:
+        # check if the class instance is a subclass of I2CSensorOrActuator
+        if not isinstance(instance, I2CSensorOrActuator):
             result = func(*args, **kwargs)
path: leaphymicropython/utils/i2c_helper.py
user: login: rmoesbergen
body: ```suggestion
            return func(*args, **kwargs)
```

De else: kan dan weg, scheel 1 "level" en maakt het wat beter leesbaar.
created_at: 2025-04-18T12:03:52Z
author_association: MEMBER
diff_hunk: @@ -68,6 +81,84 @@ def select_channel(i2c, multiplexer_address, channel_number):
         print("Invalid channel number. Please select a channel between 0 and 7 or 255.")
 
 
+class I2CSensorOrActuator():
+    """
+    Base class for I2C sensors and actuators.
+
+    This class provides a common interface and functionality for interacting with
+    I2C devices. It includes methods for initializing the I2C bus, finding the
+    device on the bus, and selecting a channel on an I2C multiplexer if one is used.
+    """
+    MULTIPLEXER_ADDRESS = None
+    ADDRESS = None
+
+    def __init__(self):
+        self.reinitialize = None
+        self.mugs_used = None
+        self.i2c = None
+        self.channel = None
+        self.bus_id = None
+        self.scl_gpio_pin = None
+        self.sda_gpio_pin = None
+
+    def initialize_i2c(self):
+        """
+        Initializes the I2C bus.
+
+        This method sets up the I2C bus with the specified ID, SCL pin, and SDA pin.
+        It is called during the initialization of the I2C device.
+        """
+        self.i2c = I2C(
+            id=self.bus_id, scl=Pin(self.scl_gpio_pin), sda=Pin(self.sda_gpio_pin)
+        )
+
+    def find_device(self, show_warnings=True):
+        """Finds the I2C device on the bus.
+
+        This method checks if the device is visible on the I2C bus.
+        If a multiplexer is used, it selects the appropriate channel before
+        checking for the device. If the device is not found, it prints a warning
+        message if show_warnings is True.
+
+        Args:
+            show_warnings (bool, optional): If True, show a warning if the device is not found.
+            Defaults to True.
+        """
+        self.is_mugs_used()
path: leaphymicropython/utils/i2c_helper.py
user: login: rmoesbergen
body: van een functie die met is_ begint verwacht ik dat hij een boolean returned, maar in dit geval is er een 'side effect'. Kan deze functie een bool returnen zodat je if is_mugs_used() kunt doen?

P.S. mugs -> mux (gebruikelijke afkorting voor multiplexer)

created_at: 2025-04-18T12:06:02Z
author_association: MEMBER

PR comments:
user: login: MartenPostma
created_at: 2025-04-16T19:44:45Z
body: poging 3
author_association: CONTRIBUTOR
user: login: rmoesbergen
created_at: 2025-04-18T12:07:30Z
body: > poging 3

Veel beter zo! Nog een paar kleine dingetjes...
author_association: MEMBER
user: login: rmoesbergen
created_at: 2025-04-22T15:47:14Z
body: 🥳 
author_association: MEMBER