diff --git a/examples/change_address.py b/examples/change_address.py index ec49216..3f422b0 100644 --- a/examples/change_address.py +++ b/examples/change_address.py @@ -11,38 +11,64 @@ from time import sleep from modulino import Modulino -print() -bus = None # Change this to the I2C bus you are using on 3rd party host boards -devices = Modulino.available_devices(bus) +def main(): + print() + bus = None # Change this to the I2C bus you are using on 3rd party host boards + devices = Modulino.available_devices(bus) -if len(devices) == 0: - print("No devices found on the bus. Try resetting the board.") - exit(1) + if len(devices) == 0: + print("No devices found on the bus. Try resetting the board.") + return -print("The following devices were found on the bus:") + print("The following devices were found on the bus:") -for index, device in enumerate(devices): - print(f"{index + 1}) {device.device_type} at {hex(device.address)}") + for index, device in enumerate(devices): + dev_type = device.device_type if device.device_type is not None else "Unknown Device" + print(f"{index + 1}) {dev_type} at {hex(device.address)}") -choice = int(input("\nEnter the device number for which you want to change the address: ")) + choice_is_valid = False + while not choice_is_valid: + try: + choice = int(input("\nEnter the device number for which you want to change the address: ")) + except ValueError: + print("Invalid input. Please enter a valid device number.") + continue + + if choice < 1 or choice > len(devices): + print("Invalid choice. Please select a valid device number.") + else: + choice_is_valid = True -if choice < 1 or choice > len(devices): - print("Invalid choice. Please select a valid device number.") - exit(1) + selected_device = devices[choice - 1] -selected_device = devices[choice - 1] -new_address = int(input("Enter the new address (hexadecimal or decimal): "), 0) -if new_address < 0 or new_address > 127: - print("Invalid address. Address must be between 0 and 127") - exit(1) + new_address_is_valid = False + while not new_address_is_valid: + try: + new_address = int(input("Enter the new address (hexadecimal or decimal): "), 0) + except ValueError: + print("Invalid input. Please enter a valid hexadecimal (e.g., 0x2A) or decimal (e.g., 42) address.") + continue -print(f"Changing address of device at {hex(selected_device.address)} to {hex(new_address)}...") -selected_device.change_address(new_address) -sleep(1) # Give the device time to reset + if new_address < 1 or new_address > 127: + print("Invalid address. Address must be between 1 and 127") + elif new_address == 100: + print("The address 0x64 (100) is reserved for bootloader mode. Please choose a different address.") + else: + new_address_is_valid = True -# Check if the address was successfully changed -if selected_device.connected: - print(f"✅ Address changed successfully to {hex(new_address)}") -else: - print("❌ Failed to change address. Please try again.") \ No newline at end of file + print(f"Changing address of device at {hex(selected_device.address)} to {hex(new_address)}...") + selected_device.change_address(new_address) + sleep(1) # Give the device time to reset + + # Check if the address was successfully changed + if selected_device.connected: + print(f"✅ Address changed successfully to {hex(new_address)}") + else: + print("❌ Failed to change address. Please try again.") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\Aborted by user") \ No newline at end of file diff --git a/examples/movement.py b/examples/movement.py index 5f10213..fcb2502 100644 --- a/examples/movement.py +++ b/examples/movement.py @@ -11,10 +11,11 @@ movement = ModulinoMovement() while True: - acc = movement.accelerometer - gyro = movement.gyro + acc = movement.acceleration + gyro = movement.angular_velocity - print(f"🏃 Accelerometer: x:{acc.x:>8.3f} y:{acc.y:>8.3f} z:{acc.z:>8.3f}") - print(f"🌐 Gyroscope: x:{gyro.x:>8.3f} y:{gyro.y:>8.3f} z:{gyro.z:>8.3f}") + print(f"🏃 Acceleration: x:{acc.x:>8.3f} y:{acc.y:>8.3f} z:{acc.z:>8.3f}") + print(f"💪 Acceleration Magnitude: {movement.acceleration_magnitude:>8.3f} g") + print(f"🌐 Angular Velocity: x:{gyro.x:>8.3f} y:{gyro.y:>8.3f} z:{gyro.z:>8.3f}") print("") sleep_ms(100) diff --git a/run_examples.py b/run_examples.py old mode 100644 new mode 100755 index e479c7a..17eacef --- a/run_examples.py +++ b/run_examples.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # This script will list the examples in the examples folder and run the selected example using mpremote. # The user can select the example using the arrow keys. # To run the script, use the following command: diff --git a/src/modulino/__init__.py b/src/modulino/__init__.py index c9ac73c..949a57c 100644 --- a/src/modulino/__init__.py +++ b/src/modulino/__init__.py @@ -4,7 +4,7 @@ __maintainer__ = "Arduino" # Import core classes and/or functions to expose them at the package level -from .helpers import map_value, map_value_int +from .helpers import map_value, map_value_int, constrain from .modulino import Modulino from .pixels import ModulinoPixels, ModulinoColor from .thermo import ModulinoThermo diff --git a/src/modulino/distance.py b/src/modulino/distance.py index 673f303..5d4e414 100644 --- a/src/modulino/distance.py +++ b/src/modulino/distance.py @@ -1,3 +1,4 @@ +from time import sleep_ms, ticks_ms, ticks_diff from .modulino import Modulino from .lib.vl53l4cd import VL53L4CD @@ -25,16 +26,19 @@ def __init__(self, i2c_bus = None, address: int | None = None) -> None: self.sensor.start_ranging() @property - def _distance_raw(self) -> int | None: + def _distance_raw(self, timeout = 1000) -> int | None: """ Reads the raw distance value from the sensor and clears the interrupt. Returns: int: The distance in centimeters. """ - try: + try: + start = ticks_ms() while not self.sensor.data_ready: - pass + if ticks_diff(ticks_ms(), start) > timeout: + raise OSError("Timeout waiting for sensor data") + sleep_ms(1) self.sensor.clear_interrupt() sensor_value = self.sensor.distance return sensor_value diff --git a/src/modulino/helpers.py b/src/modulino/helpers.py index ef5a068..e896a21 100644 --- a/src/modulino/helpers.py +++ b/src/modulino/helpers.py @@ -29,3 +29,17 @@ def map_value_int(x: float | int, in_min: float | int, in_max: float | int, out_ The mapped value as an integer. """ return int(map_value(x, in_min, in_max, out_min, out_max)) + +def constrain(value: float | int, min_value: float | int, max_value: float | int) -> float | int: + """ + Constrains a value to be within a specified range. + + Args: + value: The value to constrain. + min_value: The minimum allowable value. + max_value: The maximum allowable value. + + Returns: + The constrained value. + """ + return max(min_value, min(value, max_value)) \ No newline at end of file diff --git a/src/modulino/modulino.py b/src/modulino/modulino.py index 42601fd..c37ba78 100644 --- a/src/modulino/modulino.py +++ b/src/modulino/modulino.py @@ -25,6 +25,8 @@ 0x4: "Latch Relay" } +_BOOTLOADER_ADDRESS = const(0x64) + class _I2CHelper: """ A helper class for interacting with I2C devices on supported boards. @@ -131,7 +133,7 @@ class Modulino: This class variable needs to be overridden in derived classes. """ - def __init__(self, i2c_bus: I2C = None, address: int = None, name: str = None): + def __init__(self, i2c_bus: I2C = None, address: int = None, name: str = None, check_connection: bool = True) -> None: """ Initializes the Modulino object with the given i2c bus and address. If the address is not provided, the device will try to auto discover it. @@ -143,6 +145,7 @@ def __init__(self, i2c_bus: I2C = None, address: int = None, name: str = None): i2c_bus (I2C): The I2C bus to use. If not provided, the default I2C bus will be used. address (int): The address of the device. If not provided, the device will try to auto discover it. name (str): The name of the device. + check_connection (bool): Whether to check if the device is connected to the bus. """ if i2c_bus is None: @@ -166,7 +169,7 @@ def __init__(self, i2c_bus: I2C = None, address: int = None, name: str = None): if self.address is None: raise RuntimeError(f"Couldn't find the {self.name} device on the bus. Try resetting the board.") - elif not self.connected: + elif check_connection and not self.connected: raise RuntimeError(f"Couldn't find a {self.name} device with address {hex(self.address)} on the bus. Try resetting the board.") def discover(self, default_addresses: list[int]) -> int | None: @@ -181,11 +184,9 @@ def discover(self, default_addresses: list[int]) -> int | None: if len(default_addresses) == 0: return None - devices_on_bus = self.i2c_bus.scan() - for addr in default_addresses: - if addr in devices_on_bus: - return addr - + devices_on_bus = Modulino.scan(self.i2c_bus, default_addresses) + if len(devices_on_bus) > 0: + return devices_on_bus[0] return None def __bool__(self) -> bool: @@ -205,7 +206,12 @@ def connected(self) -> bool: """ if not bool(self): return False - return self.address in self.i2c_bus.scan() + + try: + self.i2c_bus.writeto(self.address, b'') + return True + except OSError: + return False @property def pin_strap_address(self) -> int | None: @@ -267,7 +273,7 @@ def enter_bootloader(self): sleep(0.25) # Wait for the device to reset return True except OSError as e: - # ENODEV (e.errno == 19) can be thrown if either the device reset while writing out the buffer + # ENODEV (e.errno == 19) can be thrown if the device resets while writing out the buffer return False def read(self, amount_of_bytes: int) -> bytes | None: @@ -312,6 +318,20 @@ def has_default_address(self) -> bool: """ return self.address in self.default_addresses + @staticmethod + def scan(bus: I2C, target_addresses = None) -> list[int]: + addresses = bytearray() # Use 8bit data type + # General call address (0x00) is skipped in default range + candidates = target_addresses if target_addresses is not None else range(1,128) + + for address in candidates: + try: + bus.writeto(address, b'') + addresses.append(address) + except OSError: + pass + return list(addresses) + @staticmethod def available_devices(bus: I2C = None) -> list[Modulino]: """ @@ -325,10 +345,13 @@ def available_devices(bus: I2C = None) -> list[Modulino]: """ if bus is None: bus = _I2CHelper.get_interface() - device_addresses = bus.scan() + device_addresses = Modulino.scan(bus) devices = [] for address in device_addresses: - device = Modulino(i2c_bus=bus, address=address) + if address == _BOOTLOADER_ADDRESS: + # Skip bootloader address + continue + device = Modulino(i2c_bus=bus, address=address, check_connection=False) devices.append(device) return devices diff --git a/src/modulino/movement.py b/src/modulino/movement.py index 3b1c88a..2086234 100644 --- a/src/modulino/movement.py +++ b/src/modulino/movement.py @@ -27,7 +27,7 @@ def __init__(self, i2c_bus = None, address: int | None = None) -> None: self.sensor = LSM6DSOX(self.i2c_bus, address=self.address) @property - def accelerometer(self) -> MovementValues: + def acceleration(self) -> MovementValues: """ Returns: MovementValues: The acceleration values in the x, y, and z axes. @@ -38,7 +38,17 @@ def accelerometer(self) -> MovementValues: return MovementValues(sensor_values[0], sensor_values[1], sensor_values[2]) @property - def gyro(self) -> MovementValues: + def acceleration_magnitude(self) -> float: + """ + Returns: + float: The magnitude of the acceleration vector in g. + When the Modulino is at rest (on planet earth), this value should be approximately 1.0g due to gravity. + """ + x, y, z = self.accelerometer + return (x**2 + y**2 + z**2) ** 0.5 + + @property + def angular_velocity(self) -> MovementValues: """ Returns: MovementValues: The gyroscope values in the x, y, and z axes. @@ -46,4 +56,16 @@ def gyro(self) -> MovementValues: or by using the index operator for tuple unpacking. """ sensor_values = self.sensor.gyro() - return MovementValues(sensor_values[0], sensor_values[1], sensor_values[2]) \ No newline at end of file + return MovementValues(sensor_values[0], sensor_values[1], sensor_values[2]) + + @property + def gyro(self) -> MovementValues: + """ + Alias for angular_velocity property. + + Returns: + MovementValues: The gyroscope values in the x, y, and z axes. + These values can be accessed as .x, .y, and .z properties + or by using the index operator for tuple unpacking. + """ + return self.angular_velocity \ No newline at end of file