""" This module contains the top-level code for the RoseBot robot library. CSSE 120 students should use this module, NOT the rosebot_low_level module that contains lower-level implementations. """ import rosebot_low_level as rbll import collections from enum import Enum, unique import time SIGNAL = collections.namedtuple('SIGNAL', ['pin_type', 'pin_number']) LED_PIN = SIGNAL(rbll.RoseBotDigitalOutput, 13) HIGH = 1 LOW = 0 SIMULATE = False DC = None class SENSOR_PINS(object): pins = {'left_bump_sensor': SIGNAL(rbll.RoseBotDigitalInput, 3), 'right_bump_sensor': SIGNAL(rbll.RoseBotDigitalInput, 10), 'left_proximity_sensor': SIGNAL(rbll.RoseBotAnalogInput, 0), 'front_proximity_sensor': SIGNAL(rbll.RoseBotAnalogInput, 4), 'right_proximity_sensor': SIGNAL(rbll.RoseBotAnalogInput, 5), 'left_reflectance_sensor': SIGNAL(rbll.RoseBotAnalogInput, 3), 'middle_reflectance_sensor': SIGNAL(rbll.RoseBotAnalogInput, 6), 'right_reflectance_sensor': SIGNAL(rbll.RoseBotAnalogInput, 7), } @unique class MOTORS_ENCODERS(Enum): left_wheel = 1 right_wheel = 2 class RoseBot(object): def __init__(self): """ Initializes a RoseBot that has: -- self.connection: for Python-to-robot communication -- self.differential_drive: for making the robot move -- self.camera: for doing things with the Pixy camera -- self.buzzer: for making noises -- self.led: for turning the LED on and off -- Sensor objects for sensing its environment: -- self.left_bump_sensor -- self.right_bump_sensor -- self.left_reflectance_sensor -- self.middle_reflectance_sensor -- self.right_reflectance_sensor -- self.left_proximity_sensor -- self.front_proximity_sensor -- self.right_proximity_sensor -- self.left_encoder for how many "ticks" -- self.right_encoder a wheel has turned """ self.connection = None # Established via connect_wifly self.differential_drive = DifferentialDrive() self.camera = None # PixyCamera() established after connecting self.buzzer = None # Buzzer() established after connecting self.led = LED() self.left_bump_sensor = BumpSensor('left') self.right_bump_sensor = BumpSensor('right') self.left_proximity_sensor = ProximitySensor('left') self.front_proximity_sensor = ProximitySensor('front') self.right_proximity_sensor = ProximitySensor('right') self.left_reflectance_sensor = ReflectanceSensor('left') self.middle_reflectance_sensor = ReflectanceSensor('middle') self.right_reflectance_sensor = ReflectanceSensor('right') self.left_encoder = Encoder(MOTORS_ENCODERS.left_wheel) self.right_encoder = Encoder(MOTORS_ENCODERS.right_wheel) def connect_wifly(self, robot_number): """ What comes in: An integer that is the number written on the WiFly on the phyical RoseBot. What goes out: Nothing (i.e., None). Side effects: -- Establishes a Connection that is used "under the hood" to: -- Send commands from the Python program to the robot. -- Receive information from the robot. -- Sets this RoseBot's connection instance variable to that Connection. Raises a ConnectionFailed exception if the connection cannot be establish (e.g. if the robot is not turned on). Example: robot = rb.RoseBot() robot.connect_wifly(23) Note: This method accepts its argument in any of several equivalent forms, per these examples: robot.connect_wifly(23) robot.connect_wifly('23') robot.connect_wifly('r23') robot.connect_wifly('r23.wlan.rose-hulman.edu') """ self.connection = Connection(robot_number) print('Connected! Robot is ready to run!') if SIMULATE: print('Running in SIMULATION mode') self._establish_connections_for_subcomponents() def connect_wired(self, com_port=None): """ Similar to connect_wifly, except a wired connection. """ self.connection = Connection(None, com_port) print('Connected! Robot is ready to run!') if SIMULATE: print('Running in SIMULATION mode') self._establish_connections_for_subcomponents() def _establish_connections_for_subcomponents(self): """ Private method for establishing connections to the lower-level objects that do the heavy lifting. """ if SIMULATE: board = None else: board = self.connection._low_level_connection self.camera = PixyCamera(board) self.buzzer = Buzzer(board) if SIMULATE: return lower_level_led = LED_PIN.pin_type(board, LED_PIN.pin_number) self.led._connect_to_board(lower_level_led) for sensor_name, pin in SENSOR_PINS.pins.items(): sensor = self.__getattribute__(sensor_name) lower_level_sensor = pin.pin_type(board, pin.pin_number) sensor.connect_to_board(lower_level_sensor) class Connection(object): """ Handles communication between the Python program and the robot with which it is communicating. """ def __init__(self, robot_number=None, com_port=None, use_log_file=False): """ Establishes the PyMata3 object that is used "under the hood" to: -- Send commands from the Python program to the robot. -- Receive information from the robot. Raises a ConnectionFailed exception if the connection cannot be establish (e.g. if the robot is not turned on). """ if robot_number is not None: ip_address = str(robot_number) if len(ip_address) == 1: ip_address = '0' + ip_address if not ip_address.startswith('r'): ip_address = 'r' + ip_address if not ip_address.endswith('.wlan.rose-hulman.edu'): ip_address = ip_address + '.wlan.rose-hulman.edu' elif com_port is not None: ip_address = None com_port = str(com_port) if not com_port.startswith('COM'): com_port = 'COM' + com_port else: ip_address = None if SIMULATE: return connection = rbll.Connection(ip_address, com_port, use_log_file=use_log_file) self._low_level_connection = connection.board def sleep(self, seconds): """ What comes in: A positive number. What goes out: Nothing (i.e., None). Side effects: Pauses the program for the given number of seconds. Example (assuming connection is a Connection for a RoseBot): connection.sleep(2.5) [pauses the program for 2.5 seconds] Note that: 1. The ROBOT continues doing whatever it was doing (e.g. moving). 2. A Connection maintains a "heartbeat" to the robot. In part because of that, you must use the robot's connection.sleep method and NOT the time.sleep method to make the program pause. """ if SIMULATE: # time.sleep(seconds) for _ in range(int(seconds / 0.01)): time.sleep(0.01) DC.root.update() else: self._low_level_connection.sleep(seconds) def shutdown(self): """ Shuts down the robot, leaving it in a "clean" state. """ if SIMULATE: print('Robot is shut down (in simulation mode)') return self._low_level_connection.shutdown() class DifferentialDrive(object): """ A DifferentialDrive controls all robot movement. """ def __init__(self): """ Establishes a connection to the low-level object that does the heavy lifting. """ if SIMULATE: return self._lower_level_differential_drive = rbll.DifferentialDrive() def drive_pwm(self, left_wheel_power, right_wheel_power): """ What comes in: Two integers, each between -255 and 255. What goes out: Nothing (i.e., None). Side effects: Makes the robot move at the given power levels, where -200 is full-speed backward and 200 is full-speed forward. Examples (where drive is a DifferentialDrive object for a RoseBot that has established a Connection): drive.drive_pwm(200, 200) [full speed forward] drive.drive_pwm(100, -100) [spin clockwise in place] drive.drive_pwm(-50, -50) [backwards, slowly] drive.drive_pwm(50, 180) [forwards, veering to the left] Type hints: :type left_wheel_power: int :type right_wheel_power: int """ if SIMULATE: print('Robot is moving in simulation mode:') fstring = ' Left and right wheel speeds are: {:4} {:4}' print(fstring.format(left_wheel_power, right_wheel_power)) DC.draw_line(0, 0, 100, 100) return self._lower_level_differential_drive.drive_pwm(left_wheel_power, right_wheel_power) def stop(self): """ What comes in: Nothing (i.e., no arguments). What goes out: Nothing (i.e., None). Side effects: Makes the robot stop. Example (where drive is a DifferentialDrive object for a RoseBot that has established a Connection): robot.stop() """ if SIMULATE: print('Robot is stopped (in simulation mode)') return self.drive_pwm(0, 0) class LED(object): def on(self): """ Turns the LED fully ON. """ if SIMULATE: print('LED is ON (in simulation mode)') return self._lower_level_led.digital_write(HIGH) def off(self): """ Turns the LED fully OFF. """ if SIMULATE: print('LED is OFF (in simulation mode)') return self._lower_level_led.digital_write(LOW) def _connect_to_board(self, lower_level_led): """ Private method that establishes a connection to the low-level object that does the heavy lifting. """ self._lower_level_led = lower_level_led class Encoder(object): """ An Encoder measures the rate (and hence also the distance) at which its associated Motor spins. It uses "ticks" as its units of distance, where "ticks" is a motor/encoder-dependent unit. The WheelSystem that includes this Encoder along with its associated Motor and the actual wheel itself could convert from "ticks" to centimeters per second that the wheel itself turned / traveled. """ def __init__(self, which_encoder): self.which_encoder = which_encoder if which_encoder == MOTORS_ENCODERS.left_wheel: self.low_level_encoder = rbll.LeftWheelEncoder() else: self.low_level_encoder = rbll.RightWheelEncoder() def get_ticks(self): """ Returns the number of "ticks" that this Encoder's associated Motor has spun since this Encoder was last reset. """ if SIMULATE: print('The get_ticks method is not implemented in simulation mode') return 1 return self.low_level_encoder.get_ticks() def reset(self): """ Resets this Encoder for purposes of further reporting by self.get_ticks() """ if SIMULATE: print('Encoder is reset to 0 (in simulation mode)') return return self.low_level_encoder.reset() def read(self): """ A synonym for get_ticks. """ return self.get_ticks() class Sensor(object): """ A Sensor can return a reading (i.e., current value of the Sensor). """ def __init__(self, position_on_robot, type_of_sensor): self.position_on_robot = position_on_robot self.name = (position_on_robot + '_' + type_of_sensor + '_sensor') def connect_to_board(self, lower_level_sensor): if SIMULATE: return self.lower_level_sensor = lower_level_sensor def read(self): """ Returns the current value of this Sensor. """ if SIMULATE: print('The read method is not implemented in simulation mode') return 1 return self.lower_level_sensor.read() class BumpSensor(Sensor): """ A BumpSensor can be bumped (1) or not bumped (0). """ def __init__(self, position_on_robot): super().__init__(position_on_robot, 'bump') def is_pressed(self): """ Returns True if this Bump Sensor is pressed, else returns False. """ return self.read() == 0 class ProximitySensor(Sensor): """ A ProximitySensor returns distance: 0 (far) to 4095 (close). """ def __init__(self, position_on_robot): super().__init__(position_on_robot, 'proximity') def distance_to_object_seen(self): """ Returns a number from 0 to 4095 that indicates the distance that the nearest object detected by this Proximity Sensor is from this Proximity Sensor. small -> far distance (i.e., the object is far from this Proximity Sensor) big -> close distance (i.e., the object is close to this Proximity Sensor) The readings depend on many factors including the physical characteristics of the sensor (no two are exactly alike), the ambient light, and more. """ return self.read() class ReflectanceSensor(Sensor): """ A ReflectanceSensor returns light: 0 (low) to 4095 (lots). """ def __init__(self, position_on_robot): super().__init__(position_on_robot, 'reflectance') def reflectance_reading(self): """ Returns a number from 0 to 4095 that indicates the amount of light that is bouncing back to this Reflectance Sensor. 0 -> very little light is bouncing back. 2048 -> lots of light is bouncing back. The readings depend on many factors including the physical characteristics of the sensor (no two are exactly alike), the ambient light, and more. """ return self.read() class PixyCamera(rbll.RoseBotPixy): """ Methods include: get_block() and get_blocks(). They return a PixyBlock and list of PixyBlocks, respectively. A PixyBlock has instance variables: x, y, width, height, plus a method size(). """ def __init__(self, board): if SIMULATE: return super().__init__(board) class Buzzer(rbll.RoseBotBuzzer): """ Methods include: - play_tone(n) plays tone n (try 220, 440 et al). - stop() """ def __init__(self, board): if SIMULATE: return super().__init__(board) def play_tone(self, note, duration_s=None): if SIMULATE: fstring = 'Robot is playing note {}' print(fstring.format(note)) return super().play_tone(note, duration_s=duration_s) def stop(self): if SIMULATE: print('Robot has stopped buzzing') return super().stop() class __FreezeClass__ (type): """ Students: IGNORE this class! It just works behind the scenes to help you learn to use the DataContainer below. """ def __setattr__(self, name, _): # Value argument is unused. err = "You tried to set the instance variable '" + name + "'\n" err += "on the CLASS '" + self.__name__ + "'.\n" err += "You probably meant to set that instance variable\n" err += "on an INSTANCE of that CLASS. Did you forget\n" err += "the () after to the word '" + self.__name__ + "',\n" err += "on the line where you CONSTRUCTED the instance?" raise SyntaxError(err)