Skip to content
Merged
78 changes: 52 additions & 26 deletions examples/change_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
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")
9 changes: 5 additions & 4 deletions examples/movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions run_examples.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/modulino/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/modulino/distance.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from time import sleep_ms, ticks_ms, ticks_diff
from .modulino import Modulino
from .lib.vl53l4cd import VL53L4CD

Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/modulino/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
45 changes: 34 additions & 11 deletions src/modulino/modulino.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
0x4: "Latch Relay"
}

_BOOTLOADER_ADDRESS = const(0x64)

class _I2CHelper:
"""
A helper class for interacting with I2C devices on supported boards.
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]:
"""
Expand All @@ -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

Expand Down
28 changes: 25 additions & 3 deletions src/modulino/movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -38,12 +38,34 @@ 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.
These values can be accessed as .x, .y, and .z properties
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])
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