import struct from numbers import Real from typing import Optional # Major and minor version of required firmware _REQUIRED_FIRMWARE_VERSION = (0, 4) class FirmwareVersionMismatch(Exception): pass class WhoAmIMismatch(Exception): pass class Controller: I2C_ADDR = 0x57 FIRMWARE_VERSION = 0x08 WHO_AM_I = 0x0F PWM_FREQUENCY = 0x10 MAX_MOTOR_PERCENTAGE = 0x11 MOTOR_SHUTDOWN_TIMEOUT = 0x28 MOTOR_SPEED = 0x30 AUTOMATIC_MOTOR_SPEED = 0x31 ENCODER_TICKS = 0x32 STATUS = 0x36 RESET = 0xE0 RESET_TO_BOOTLOADER = 0xE1 DEVICE_ID = 0xF0 def __init__(self, i2c_bus=8): import smbus self.i2c_bus = i2c_bus self.i2c = smbus.SMBus(self.i2c_bus) self.check_who_am_i() self.check_firmware_version() def _read(self, command, n, unpack_spec) -> tuple: return struct.unpack( unpack_spec, bytes(self.i2c.read_i2c_block_data(self.I2C_ADDR, command, n)), ) def _write(self, command: int, data: list[int]): self.i2c.write_i2c_block_data(self.I2C_ADDR, command, data) def who_am_i(self) -> int: """Check that the motors controller board is present. This should return the same value as Controller.I2C_ADDR.""" return self._read(self.WHO_AM_I, 1, "B")[0] def check_who_am_i(self): """Check that the device answers to WHO_AM_I is correct.""" w = self.who_am_i() if w != self.I2C_ADDR: error = ( f"WHO_AM_I returns {w:#04x} " f"instead of the expected {self.I2C_ADDR:#04x}" ) raise WhoAmIMismatch(error) def set_max_percentage(self, percent: float): """Set the maximum percentage of power which will be used (between 1 and 100) for further speed instructions. This has no effect on an running command.""" if percent <= 0 or percent > 100: raise ValueError("percentage must be between 0 and 100") self._write(self.MAX_MOTOR_PERCENTAGE, [round(percent)]) def get_max_percentage(self) -> int: """Get the maximum percentage of power which will be used (between 1 and 100).""" return self._read(self.MAX_MOTOR_PERCENTAGE, 1, "B")[0] def set_motor_speed(self, left: Optional[float], right: Optional[float]): """Set the motor speed between -100 and 100. None means not to change the motor value. Using None for both motors will put the controller board in standby mode and motors will stop.""" def convert(v: Optional[float], arg: str): if v is None: return -128 if not isinstance(v, Real) or v < -100 or v > 100: raise ValueError( f"{arg} motor speed " "must be a number between -100 and 100, or None" ) return round(v) self._write( self.MOTOR_SPEED, list(struct.pack("bb", convert(left, "left"), convert(right, "right"))), ) def get_motor_speed(self) -> Optional[tuple[int, int]]: """Get the left and right motor speed as a tuple, or None if in standby.""" (left, right) = self._read(self.MOTOR_SPEED, 2, "bb") return (left, right) if left != 0x80 and right != 0x80 else None def set_automatic_motor_speed(self, left: int, right: int): """Set the motor speed in ticks by 10th of seconds.""" def check(v: int, arg: str): if v < -32767 or v > 32767: raise ValueError( f"{arg} automatic motor speed must be a number " + "between -32767 and 32767" ) return v self._write( self.AUTOMATIC_MOTOR_SPEED, list(struct.pack("<hh", check(left, "left"), check(right, "right"))), ) def get_automatic_motor_speed(self): """Get the left and right automatic motor speed as a tuple, or None if the system is not in automatic mode.""" (left, right) = self._read(self.AUTOMATIC_MOTOR_SPEED, 4, "<hh") return (left, right) if left != -32768 else None def set_left_motor_speed(self, speed: float): """Set the left motor speed between -100 and 100.""" self.set_motor_speed(speed, None) def set_right_motor_speed(self, speed: float): """Set the right motor speed between -100 and 100.""" self.set_motor_speed(None, speed) def standby(self): """Stop the motors by putting the controller board in standby mode.""" self.set_motor_speed(None, None) def get_encoder_ticks(self) -> tuple[int, int]: """Retrieve the encoder ticks since the last time it was queried. The ticks must be retrieved before they overflow a 2 byte signed integer (-32768..32767) or the result will make no sense. Return a pair with left and right data.""" return self._read(self.ENCODER_TICKS, 4, "hh") def get_status(self) -> dict[str, bool]: """Return a dict with status fields: - "moving": True if at least one motor is moving, False otherwise - "automatic": True if the motors are in automatic mode, False otherwise""" (status,) = self._read(self.STATUS, 1, "B") return {"moving": (status & 1) != 0, "automatic": (status & 2) != 0} def set_motor_shutdown_timeout(self, duration: float): """Set the duration in seconds after which the motors will shut down if no valid command is received. The minimum is 0.1 seconds, the maximum is 10 seconds.""" if duration < 0.1 or duration > 10.0: raise ValueError self._write(self.MOTOR_SHUTDOWN_TIMEOUT, [round(duration * 10)]) def get_motor_shutdown_timeout(self) -> float: """Get the duration in seconds after which the motors will shut down if no valid command is received.""" return self._read(self.MOTOR_SHUTDOWN_TIMEOUT, 1, "B")[0] / 10 def get_firmware_version(self) -> tuple[int, int, int]: """Get the firmware version (major, minor, patch).""" return self._read(self.FIRMWARE_VERSION, 3, "BBB") def check_firmware_version(self): """Check that the firmware uses a version compatible with this library.""" version = self.get_firmware_version() Controller._check_firmware_version_consistency( _REQUIRED_FIRMWARE_VERSION, version ) def _check_firmware_version_consistency( required: tuple[int, int], version: tuple[int, int, int] ): (MAJOR, MINOR) = required (major, minor, patch) = version error = None if major != MAJOR or minor < MINOR: version = f"{major}.{minor}.{patch}" VERSION = f"{MAJOR}.{MINOR}.*" error = ( f"Hardware runs firmware version {version} which " f"is not compatible with this library version ({VERSION})" ) raise FirmwareVersionMismatch(error) def set_pwm_frequency(self, freq: int): """Set the PWM frequency in Hz, between 1 and 100000.""" if freq < 1 or freq > 100000: raise ValueError(f"PWM frequency is out of [1, 100000] range: {freq}") self._write( self.PWM_FREQUENCY, [freq & 0xFF, (freq >> 8) & 0xFF, (freq >> 16) & 0xFF] ) def get_pwm_frequency(self): """Return the PWM frequency in Hz.""" a, b, c = self._read(self.PWM_FREQUENCY, 3, "BBB") return a | (b << 8) | (c << 16) def reset(self): """Reset the device. Used mainly for testing.""" self._write(self.RESET, []) def reset_to_bootloader(self): """Reset the device to bootloader mode. Used for reprogramming.""" self._write(self.RESET_TO_BOOTLOADER, []) def get_device_id(self): """Return the 8 bytes composing the device id.""" return list(self._read(self.DEVICE_ID, 8, "BBBBBBBB"))