import smbus import struct from typing import Any, Optional class Controller: I2C_ADDR = 0x57 WHO_AM_I = 0x0F MAX_MOTOR_PERCENTAGE = 0x11 MOTOR_SHUTDOWN_TIMEOUT = 0x28 MOTOR_SPEED = 0x30 ENCODER_TICKS = 0x32 STATUS = 0x36 def __init__(self, i2cbus=8): self.I2C_BUS = i2cbus self.i2c = smbus.SMBus(self.I2C_BUS) 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") def set_max_percentage(self, percent: int): """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 self._write(self.MAX_MOTOR_PERCENTAGE, [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[int], right: Optional[int]): """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[int], arg: str): if v is None: return -128 if not isinstance(v, int) or v < -100 or v > 100: raise ValueError( f"{arg} must be an integer between -100 and 100 or None" ) return v self._write( self.MOTOR_SPEED, list( struct.pack( "bb", convert(left, "left"), convert(right, "right") ) ), ) def get_motor_speed(self) -> tuple[int, int]: """Get the left and right motor speed as a tuple.""" return self._read(self.MOTOR_SPEED, 2, "bb") def set_left_motor_speed(self, speed: int): """Set the left motor speed between -127 and 127.""" self.set_motor_speed(speed, None) def set_right_motor_speed(self, speed: int): """Set the right motor speed between -127 and 127.""" 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, Any]: """Return a dict with various status fields: - "moving": True if at least one motor is moving, False otherwise""" (status,) = self._read(self.STATUS, 1, "?") return {"moving": (status & 1) != 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