Skip to content

Commit 587c97b

Browse files
authored
Merge pull request #16 from arduino/improvements
Various Improvements
2 parents 09d4e8c + 36e3a21 commit 587c97b

File tree

8 files changed

+139
-48
lines changed

8 files changed

+139
-48
lines changed

examples/change_address.py

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,64 @@
1111
from time import sleep
1212
from modulino import Modulino
1313

14-
print()
15-
bus = None # Change this to the I2C bus you are using on 3rd party host boards
16-
devices = Modulino.available_devices(bus)
14+
def main():
15+
print()
16+
bus = None # Change this to the I2C bus you are using on 3rd party host boards
17+
devices = Modulino.available_devices(bus)
1718

18-
if len(devices) == 0:
19-
print("No devices found on the bus. Try resetting the board.")
20-
exit(1)
19+
if len(devices) == 0:
20+
print("No devices found on the bus. Try resetting the board.")
21+
return
2122

22-
print("The following devices were found on the bus:")
23+
print("The following devices were found on the bus:")
2324

24-
for index, device in enumerate(devices):
25-
print(f"{index + 1}) {device.device_type} at {hex(device.address)}")
25+
for index, device in enumerate(devices):
26+
dev_type = device.device_type if device.device_type is not None else "Unknown Device"
27+
print(f"{index + 1}) {dev_type} at {hex(device.address)}")
2628

27-
choice = int(input("\nEnter the device number for which you want to change the address: "))
29+
choice_is_valid = False
30+
while not choice_is_valid:
31+
try:
32+
choice = int(input("\nEnter the device number for which you want to change the address: "))
33+
except ValueError:
34+
print("Invalid input. Please enter a valid device number.")
35+
continue
36+
37+
if choice < 1 or choice > len(devices):
38+
print("Invalid choice. Please select a valid device number.")
39+
else:
40+
choice_is_valid = True
2841

29-
if choice < 1 or choice > len(devices):
30-
print("Invalid choice. Please select a valid device number.")
31-
exit(1)
42+
selected_device = devices[choice - 1]
3243

33-
selected_device = devices[choice - 1]
34-
new_address = int(input("Enter the new address (hexadecimal or decimal): "), 0)
3544

36-
if new_address < 0 or new_address > 127:
37-
print("Invalid address. Address must be between 0 and 127")
38-
exit(1)
45+
new_address_is_valid = False
46+
while not new_address_is_valid:
47+
try:
48+
new_address = int(input("Enter the new address (hexadecimal or decimal): "), 0)
49+
except ValueError:
50+
print("Invalid input. Please enter a valid hexadecimal (e.g., 0x2A) or decimal (e.g., 42) address.")
51+
continue
3952

40-
print(f"Changing address of device at {hex(selected_device.address)} to {hex(new_address)}...")
41-
selected_device.change_address(new_address)
42-
sleep(1) # Give the device time to reset
53+
if new_address < 1 or new_address > 127:
54+
print("Invalid address. Address must be between 1 and 127")
55+
elif new_address == 100:
56+
print("The address 0x64 (100) is reserved for bootloader mode. Please choose a different address.")
57+
else:
58+
new_address_is_valid = True
4359

44-
# Check if the address was successfully changed
45-
if selected_device.connected:
46-
print(f"✅ Address changed successfully to {hex(new_address)}")
47-
else:
48-
print("❌ Failed to change address. Please try again.")
60+
print(f"Changing address of device at {hex(selected_device.address)} to {hex(new_address)}...")
61+
selected_device.change_address(new_address)
62+
sleep(1) # Give the device time to reset
63+
64+
# Check if the address was successfully changed
65+
if selected_device.connected:
66+
print(f"✅ Address changed successfully to {hex(new_address)}")
67+
else:
68+
print("❌ Failed to change address. Please try again.")
69+
70+
if __name__ == "__main__":
71+
try:
72+
main()
73+
except KeyboardInterrupt:
74+
print("\Aborted by user")

examples/movement.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
movement = ModulinoMovement()
1212

1313
while True:
14-
acc = movement.accelerometer
15-
gyro = movement.gyro
14+
acc = movement.acceleration
15+
gyro = movement.angular_velocity
1616

17-
print(f"🏃 Accelerometer: x:{acc.x:>8.3f} y:{acc.y:>8.3f} z:{acc.z:>8.3f}")
18-
print(f"🌐 Gyroscope: x:{gyro.x:>8.3f} y:{gyro.y:>8.3f} z:{gyro.z:>8.3f}")
17+
print(f"🏃 Acceleration: x:{acc.x:>8.3f} y:{acc.y:>8.3f} z:{acc.z:>8.3f}")
18+
print(f"💪 Acceleration Magnitude: {movement.acceleration_magnitude:>8.3f} g")
19+
print(f"🌐 Angular Velocity: x:{gyro.x:>8.3f} y:{gyro.y:>8.3f} z:{gyro.z:>8.3f}")
1920
print("")
2021
sleep_ms(100)

run_examples.py

100644100755
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#!/usr/bin/env python3
12
# This script will list the examples in the examples folder and run the selected example using mpremote.
23
# The user can select the example using the arrow keys.
34
# To run the script, use the following command:

src/modulino/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
__maintainer__ = "Arduino"
55

66
# Import core classes and/or functions to expose them at the package level
7-
from .helpers import map_value, map_value_int
7+
from .helpers import map_value, map_value_int, constrain
88
from .modulino import Modulino
99
from .pixels import ModulinoPixels, ModulinoColor
1010
from .thermo import ModulinoThermo

src/modulino/distance.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from time import sleep_ms, ticks_ms, ticks_diff
12
from .modulino import Modulino
23
from .lib.vl53l4cd import VL53L4CD
34

@@ -25,16 +26,19 @@ def __init__(self, i2c_bus = None, address: int | None = None) -> None:
2526
self.sensor.start_ranging()
2627

2728
@property
28-
def _distance_raw(self) -> int | None:
29+
def _distance_raw(self, timeout = 1000) -> int | None:
2930
"""
3031
Reads the raw distance value from the sensor and clears the interrupt.
3132
3233
Returns:
3334
int: The distance in centimeters.
3435
"""
35-
try:
36+
try:
37+
start = ticks_ms()
3638
while not self.sensor.data_ready:
37-
pass
39+
if ticks_diff(ticks_ms(), start) > timeout:
40+
raise OSError("Timeout waiting for sensor data")
41+
sleep_ms(1)
3842
self.sensor.clear_interrupt()
3943
sensor_value = self.sensor.distance
4044
return sensor_value

src/modulino/helpers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,17 @@ def map_value_int(x: float | int, in_min: float | int, in_max: float | int, out_
2929
The mapped value as an integer.
3030
"""
3131
return int(map_value(x, in_min, in_max, out_min, out_max))
32+
33+
def constrain(value: float | int, min_value: float | int, max_value: float | int) -> float | int:
34+
"""
35+
Constrains a value to be within a specified range.
36+
37+
Args:
38+
value: The value to constrain.
39+
min_value: The minimum allowable value.
40+
max_value: The maximum allowable value.
41+
42+
Returns:
43+
The constrained value.
44+
"""
45+
return max(min_value, min(value, max_value))

src/modulino/modulino.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
0x4: "Latch Relay"
2626
}
2727

28+
_BOOTLOADER_ADDRESS = const(0x64)
29+
2830
class _I2CHelper:
2931
"""
3032
A helper class for interacting with I2C devices on supported boards.
@@ -131,7 +133,7 @@ class Modulino:
131133
This class variable needs to be overridden in derived classes.
132134
"""
133135

134-
def __init__(self, i2c_bus: I2C = None, address: int = None, name: str = None):
136+
def __init__(self, i2c_bus: I2C = None, address: int = None, name: str = None, check_connection: bool = True) -> None:
135137
"""
136138
Initializes the Modulino object with the given i2c bus and address.
137139
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):
143145
i2c_bus (I2C): The I2C bus to use. If not provided, the default I2C bus will be used.
144146
address (int): The address of the device. If not provided, the device will try to auto discover it.
145147
name (str): The name of the device.
148+
check_connection (bool): Whether to check if the device is connected to the bus.
146149
"""
147150

148151
if i2c_bus is None:
@@ -166,7 +169,7 @@ def __init__(self, i2c_bus: I2C = None, address: int = None, name: str = None):
166169

167170
if self.address is None:
168171
raise RuntimeError(f"Couldn't find the {self.name} device on the bus. Try resetting the board.")
169-
elif not self.connected:
172+
elif check_connection and not self.connected:
170173
raise RuntimeError(f"Couldn't find a {self.name} device with address {hex(self.address)} on the bus. Try resetting the board.")
171174

172175
def discover(self, default_addresses: list[int]) -> int | None:
@@ -181,11 +184,9 @@ def discover(self, default_addresses: list[int]) -> int | None:
181184
if len(default_addresses) == 0:
182185
return None
183186

184-
devices_on_bus = self.i2c_bus.scan()
185-
for addr in default_addresses:
186-
if addr in devices_on_bus:
187-
return addr
188-
187+
devices_on_bus = Modulino.scan(self.i2c_bus, default_addresses)
188+
if len(devices_on_bus) > 0:
189+
return devices_on_bus[0]
189190
return None
190191

191192
def __bool__(self) -> bool:
@@ -205,7 +206,12 @@ def connected(self) -> bool:
205206
"""
206207
if not bool(self):
207208
return False
208-
return self.address in self.i2c_bus.scan()
209+
210+
try:
211+
self.i2c_bus.writeto(self.address, b'')
212+
return True
213+
except OSError:
214+
return False
209215

210216
@property
211217
def pin_strap_address(self) -> int | None:
@@ -267,7 +273,7 @@ def enter_bootloader(self):
267273
sleep(0.25) # Wait for the device to reset
268274
return True
269275
except OSError as e:
270-
# ENODEV (e.errno == 19) can be thrown if either the device reset while writing out the buffer
276+
# ENODEV (e.errno == 19) can be thrown if the device resets while writing out the buffer
271277
return False
272278

273279
def read(self, amount_of_bytes: int) -> bytes | None:
@@ -312,6 +318,20 @@ def has_default_address(self) -> bool:
312318
"""
313319
return self.address in self.default_addresses
314320

321+
@staticmethod
322+
def scan(bus: I2C, target_addresses = None) -> list[int]:
323+
addresses = bytearray() # Use 8bit data type
324+
# General call address (0x00) is skipped in default range
325+
candidates = target_addresses if target_addresses is not None else range(1,128)
326+
327+
for address in candidates:
328+
try:
329+
bus.writeto(address, b'')
330+
addresses.append(address)
331+
except OSError:
332+
pass
333+
return list(addresses)
334+
315335
@staticmethod
316336
def available_devices(bus: I2C = None) -> list[Modulino]:
317337
"""
@@ -325,10 +345,13 @@ def available_devices(bus: I2C = None) -> list[Modulino]:
325345
"""
326346
if bus is None:
327347
bus = _I2CHelper.get_interface()
328-
device_addresses = bus.scan()
348+
device_addresses = Modulino.scan(bus)
329349
devices = []
330350
for address in device_addresses:
331-
device = Modulino(i2c_bus=bus, address=address)
351+
if address == _BOOTLOADER_ADDRESS:
352+
# Skip bootloader address
353+
continue
354+
device = Modulino(i2c_bus=bus, address=address, check_connection=False)
332355
devices.append(device)
333356
return devices
334357

src/modulino/movement.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(self, i2c_bus = None, address: int | None = None) -> None:
2727
self.sensor = LSM6DSOX(self.i2c_bus, address=self.address)
2828

2929
@property
30-
def accelerometer(self) -> MovementValues:
30+
def acceleration(self) -> MovementValues:
3131
"""
3232
Returns:
3333
MovementValues: The acceleration values in the x, y, and z axes.
@@ -38,12 +38,34 @@ def accelerometer(self) -> MovementValues:
3838
return MovementValues(sensor_values[0], sensor_values[1], sensor_values[2])
3939

4040
@property
41-
def gyro(self) -> MovementValues:
41+
def acceleration_magnitude(self) -> float:
42+
"""
43+
Returns:
44+
float: The magnitude of the acceleration vector in g.
45+
When the Modulino is at rest (on planet earth), this value should be approximately 1.0g due to gravity.
46+
"""
47+
x, y, z = self.accelerometer
48+
return (x**2 + y**2 + z**2) ** 0.5
49+
50+
@property
51+
def angular_velocity(self) -> MovementValues:
4252
"""
4353
Returns:
4454
MovementValues: The gyroscope values in the x, y, and z axes.
4555
These values can be accessed as .x, .y, and .z properties
4656
or by using the index operator for tuple unpacking.
4757
"""
4858
sensor_values = self.sensor.gyro()
49-
return MovementValues(sensor_values[0], sensor_values[1], sensor_values[2])
59+
return MovementValues(sensor_values[0], sensor_values[1], sensor_values[2])
60+
61+
@property
62+
def gyro(self) -> MovementValues:
63+
"""
64+
Alias for angular_velocity property.
65+
66+
Returns:
67+
MovementValues: The gyroscope values in the x, y, and z axes.
68+
These values can be accessed as .x, .y, and .z properties
69+
or by using the index operator for tuple unpacking.
70+
"""
71+
return self.angular_velocity

0 commit comments

Comments
 (0)