Source code for devices

""" 
devices.py contains the Motor and Sensor classes.
Each sensor type has its own class, derived from the base Sensor class.
Motor and sensor classes implement Direct Commands in a way that simplifies 
the interaction with the EV3 devices.

Author: Eduardo Nigro
    rev 0.0.3
    2021-12-17
"""
import numpy as np
import time

[docs]class Motor: """ The class to represent the EV3 motor. Set up a motor on output port ``'B'`` >>> from pyev3.brick import LegoEV3 >>> from pyev3.devices import Motor >>> myev3 = LegoEV3(commtype='usb') >>> mymotor = Motor(myev3, port='B') Set up a motor on output port ``'A'`` of the first brick and port ``'B'`` of the second brick >>> mymotor1 = Motor(myev3, layer=1, port='A') >>> mymotor2 = Motor(myev3, layer=2, port='B') :param ev3handle: ``LegoEV3`` instance representing the EV3 brick. :type ev3handle: object :param layer: The layer of the brick ``1`` or ``2`` in a daisy-chain configuration. :type layer: int :param port: The brick output port connected to the motor. Possible values are ``'A'``, ``'B'``, ``'C'``, or ``'D'``. :type port: str .. note:: 1. If EV3 bricks are connected in a daisy-chain configuration, set the input parameter `layer` to the appropriate number. """ def __init__(self, ev3handle, layer=1, port='A'): """ Class constructor. """ # Assigning input attributes self._ev3handle = ev3handle self._layer = layer self._outputport = port # Assigning default attributes self._motorstopped = True self._outputmode = 'power' self._output = 0 # GET/SET METHODS (PUBLIC PROPERTIES) @property def outputmode(self): """ Contains the motor output mode. It can be either ``'speed'`` or ``'power'`` (`read/write`). The motor has to be stopped before switching modes. """ return self._outputmode @outputmode.setter def outputmode(self, value): if self._motorstopped: if value.lower() == 'speed': self._outputmode = 'speed' elif value.lower() == 'power': self._outputmode = 'power' else: raise NameError('Invalid output type.') else: print('Stop motor to change output mode.') @property def output(self): """ Contains the motor output level as a percentage. Values can be between ``-100`` or ``100`` (`read/write`). """ return self._output @output.setter def output(self, value): # Limiting value or changing data type if value > 100: value = 100 elif value < -100: value = -100 else: value = np.int8(value) # Sending appropriate output level command to EV3 brick if self.outputmode == 'power': self._ev3handle._set_outputpower( layer=self._layer, port=self._outputport, power=value) elif self.outputmode == 'speed': self._ev3handle._set_outputspeed( layer=self._layer, port=self._outputport, speed=value) # Updating Output property self._output = value @property def angle(self): """ Contains the angular position of the motor in degrees (`read only`). """ return self._ev3handle._get_outputcount( layer=self._layer, port=self._outputport) @angle.setter def angle(self, _): print('"angle" is a read only property.') # MOTOR CONTROL METHODS
[docs] def start(self): """ Start the EV3 motor. >>> mymotor.start() """ # Making sure current output mode is sent to EV3 brick if self.outputmode == 'power': self._ev3handle._set_outputpower( layer=self._layer, port=self._outputport, power=self.output) elif self.outputmode == 'speed': self._ev3handle._set_outputspeed( layer=self._layer, port=self._outputport, speed=self.output) # Changing motor stopped status flag self._motorstopped = False # Starting motor self._ev3handle._start_motor( layer=self._layer, port=self._outputport)
[docs] def stop(self, brake='off'): """ Stop the EV3 motor. :param brake: The brake option of the motor ``'on'`` or ``'off'``. Can be used to hold the motor position. :type brake: str >>> mymotor.stop(brake='on') """ # Changing motor stopped status flag self._motorstopped = True # Stopping motor with appropriate brake option self._ev3handle._stop_motor( layer=self._layer, port=self._outputport, brake=brake)
[docs] def reset_angle(self): """ Reset the encoder output angle of the EV3 motor. >>> mymotor.reset_angle() """ self._ev3handle._clear_outputcount( layer=self._layer, port=self._outputport)
class Sensor: """ The Sensor class implements common methods for all the different types of EV3 sensors. The following properties and methods are inherited by the derived sensor-specific classes. .. note:: 1. The sensor input mode can be changed at any time, even after its creation. """ def __init__(self, ev3handle, layer=1, portnum=1): """ Class constructor. :param ev3handle: ``LegoEV3`` instance representing the EV3 brick. :type ev3handle: object :param layer: The layer of the brick ``1`` or ``2`` in adaisy-chain configuration. :type layer: int :param portnum: The brick input port connected to the sensor. Possible values are ``1``, ``2``, ``3``, or ``4``. :type portnum: int """ # Assigning input attributes self._ev3handle = ev3handle self._layer = layer self._inputport = portnum # Assigning sensor specific attributes self._sensortype, self.activemode = self._ev3handle._read_inputdevicetypemode( layer=self._layer, portnum=self._inputport) # Getting sensor info and separating name from mode nameaux = self._ev3handle._read_inputdevicename( layer=self._layer, portnum=self._inputport) indexaux = nameaux.find('-') # Assigning sensor name and active mode attributes if indexaux < 0: self._sensorname = 'TOUCH' self._activemodename = nameaux else: self._sensorname = nameaux[0:indexaux] self._activemodename = nameaux[indexaux+1::] # Creating default generic sensor attributes self._inputmode = None self._outputformat = None self._mode = None self._dataset = None # Creating sensor info dictionary self._info = { 'TOUCH': ['Touch', ['touch', 'bump']], 'COL': ['Color', ['reflected', 'ambient', 'color', 'rgb']], 'RGB': ['Color', ['reflected', 'ambient', 'color', 'rgb']], 'US': ['Ultrasonic', ['distance', 'listen']], 'IR': ['Infrared', ['proximity', 'seeker', 'remote']], 'GYRO': ['Gyro', ['angle', 'rate', 'angle&rate']] } # GET/SET METHOD INPUTMODE @property def inputmode(self): """ Contains the sensor input mode. Use this to change the mode on the fly (`read/write`). """ return self._inputmode @inputmode.setter def inputmode(self, value): if value in list(self.modepar.keys()): self._inputmode = value self._outputformat = self.modepar[value][0] self._mode = self.modepar[value][1] self._dataset = self.modepar[value][2] # Forcing mode change _ = self._read_output() else: nameaux = self._info[self._sensorname][0] raise NameError('Undefined input mode for ' + nameaux + ' sensor.') # SENSOR BASE CLASS METHODS def display_info(self): """ Displays a summary with the sensor information. """ print('____________________________________________________________') print('Sensor Name : ' + self._info[self._sensorname][0]) print('Current Mode : ' + self.inputmode) print('Available Modes : ' + str(self._info[self._sensorname][1])[1:-1]) print('Layer : ' + str(self._layer)) print('Port Number : ' + str(self._inputport)) print('____________________________________________________________') def _read_output(self): """ Read generic sensor output value. """ # Assigning sensor output reader method if self._outputformat == 1: reader = self._ev3handle._read_inputdevicereadySI elif self._outputformat == 2: reader = self._ev3handle._read_inputdevicereadyPCT elif self._outputformat == 3: reader = self._ev3handle._read_inputdevicereadyRAW # Reading sensor output result = reader( layer=self._layer, portnum=self._inputport, mode=self._mode, dataset=self._dataset) while result is None: time.sleep(0.01) result = reader( layer=self._layer, portnum=self._inputport, mode=self._mode, dataset=self._dataset) return result def _clear_changes(self): self._ev3handle._clear_inputdevicechanges( layer=self._layer, portnum=self._inputport)
[docs]class Touch(Sensor): """ The class to represent the EV3 touch sensor. Set up a touch sensor on port number 1 >>> from pyev3.brick import LegoEV3 >>> from pyev3.devices import Touch >>> myev3 = LegoEV3(commtype='usb') >>> mysensor = Touch(myev3, portnum=1, inputmode='bump') :param ev3handle: ``LegoEV3`` instance representing the EV3 brick. :type ev3handle: object :param layer: The layer of the brick ``1`` or ``2`` in a daisy-chain configuration. :type layer: int :param portnum: The brick input port connected to the sensor. Possible values are ``1``, ``2``, ``3``, or ``4``. :type portnum: int :param inputmode: * ``'touch'`` to check the button state (`pressed or released`) * ``'bump'`` to count the number of press/release events :type inputmode: str """ def __init__(self, ev3handle, layer=1, portnum=1, inputmode='touch'): """ Class constructor. """ # Defining touch sensor mode parameters # [outputformat, mode, dataset] self.modepar = { 'touch': [1, 0, 1], 'bump': [1, 1, 1] } # Instantiating base class Sensor super(Touch, self).__init__(ev3handle, layer=layer, portnum=portnum) # Checking for valid sensor if self._info[self._sensorname][0] != 'Touch': raise NameError('Incorrect sensor type on port : ' + str(portnum)) # Assining sensor specific attributes self.inputmode = inputmode @property def output(self): """ Contains the sensor output based on the `inputmode` (`read only`). * ``1`` or ``0`` (`inputmode='touch'`) * ``int`` (`inputmode='bump'`) """ data = self._read_output() result = int(data[0]) return result @output.setter def output(self, _): print('"output" is a read only property.')
[docs] def reset_count(self): """ Reset the touch sensor event counter. >>> mysensor.reset_count() """ return self._clear_changes()
[docs]class Color(Sensor): """ The class to represent the EV3 color sensor. Set up a color sensor on port number 2 >>> from pyev3.brick import LegoEV3 >>> from pyev3.devices import Color >>> myev3 = LegoEV3(commtype='usb') >>> mysensor = Color(myev3, portnum=2, inputmode='rgb') :param ev3handle: ``LegoEV3`` instance representing the EV3 brick. :type ev3handle: object :param layer: The layer of the brick ``1`` or ``2`` in a daisy-chain configuration. :type layer: int :param portnum: The brick input port connected to the sensor. Possible values are ``1``, ``2``, ``3``, or ``4``. :type portnum: int :param inputmode: * ``'ambient'`` ambient light intensity * ``'reflected'`` reflected light intensity * ``'color'`` surface color * ``'rgb'`` surface color RGB components :type inputmode: str """ def __init__(self, ev3handle, layer=1, portnum=1, inputmode='ambient'): """ Class constructor. """ # Defining color sensor mode parameters # [outputformat, mode, dataset] self.modepar = { 'reflected': [2, 0, 1], 'ambient': [2, 1, 1], 'color': [1, 2, 1], 'rgb': [3, 4, 3] } # Instantiating base class Sensor super(Color, self).__init__(ev3handle, layer=layer, portnum=portnum) # Checking for valid sensor if self._info[self._sensorname][0] != 'Color': raise NameError('Incorrect sensor type on port : ' + str(portnum)) # Assining sensor specific attributes self.inputmode = inputmode # Defining color list self._color = [ 'unknown', 'black', 'blue', 'green', 'yellow', 'red', 'white', 'brown' ] @property def output(self): """ Contains the sensor output based on the `inputmode` (`read only`). * ``0`` to ``100`` (`inputmode='ambient'`) * ``0`` to ``100`` (`inputmode='reflected'`) * ``'blue'``, ``'green'``, ``'yellow'``, ``'red'``, ``'white'``, ``'brown'``, ``'unknown'`` (`inputmode='color'`) * tuple of integers (``0`` to ``255``, ``0`` to ``255``, ``0`` to ``255``) (`inputmode='rgb'`) """ data = self._read_output() if self._inputmode == 'reflected': result = int(data[0]) elif self._inputmode == 'ambient': result = int(data[0]) elif self._inputmode == 'color': result = self._color[int(data[0])] elif self._inputmode == 'rgb': result = tuple([min(255, int(value)) for value in data]) return result @output.setter def output(self, _): print('"output" is a read only property.')
[docs]class Ultrasonic(Sensor): """ The class to represent the EV3 ultrasonic sensor. Set up an ultrasonic sensor on port number 3 >>> from pyev3.brick import LegoEV3 >>> from pyev3.devices import Ultrasonic >>> myev3 = LegoEV3(commtype='usb') >>> mysensor = Ultrasonic(myev3, portnum=3, inputmode='distance') :param ev3handle: ``LegoEV3`` instance representing the EV3 brick. :type ev3handle: object :param layer: The layer of the brick ``1`` or ``2`` in a daisy-chain configuration. :type layer: int :param portnum: The brick input port connected to the sensor. Possible values are ``1``, ``2``, ``3``, or ``4``. :type portnum: int :param inputmode: * ``'distance'`` measure distance to an object in cm * ``'listen'`` detect presence of other ultrasound source :type inputmode: str """ def __init__(self, ev3handle, layer=1, portnum=1, inputmode=None): """ Class constructor. """ # Defining ultrasonic sensor mode parameters # [outputformat, mode, dataset] self.modepar = { 'distance': [1, 0, 1], 'listen': [1, 2, 1] } # Instantiating base class Sensor super(Ultrasonic, self).__init__(ev3handle, layer=layer, portnum=portnum) # Checking for valid sensor if self._info[self._sensorname][0] != 'Ultrasonic': raise NameError('Incorrect sensor type on port : ' + str(portnum)) # Assining sensor specific attributes self.inputmode = inputmode @property def output(self): """ Contains the sensor output based on the `inputmode` (`read only`). * ``0`` to ``250`` (`inputmode='distance'`) * ``0`` or ``1`` (`inputmode='listen'`) """ data = self._read_output() if self._inputmode == 'distance': result = np.float32(data[0]) elif self._inputmode == 'listen': result = int(data[0]) return result @output.setter def output(self, _): print('"output" is a read only property.')
[docs]class Infrared(Sensor): """ The class to represent the EV3 infrared sensor. Set up an infrared sensor on port number 4 >>> from pyev3.brick import LegoEV3 >>> from pyev3.devices import Infrared >>> myev3 = LegoEV3(commtype='usb') >>> mysensor = Infrared(myev3, portnum=4, inputmode='remote') :param ev3handle: ``LegoEV3`` instance representing the EV3 brick. :type ev3handle: object :param layer: The layer of the brick ``1`` or ``2`` in a daisy-chain configuration. :type layer: int :param portnum: The brick input port connected to the sensor. Possible values are ``1``, ``2``, ``3``, or ``4``. :type portnum: int :param inputmode: * ``'proximity'`` detect proximity to an object * ``'seeker'`` searches beacon (`requires channel 1 and beacon on`) * ``'remote'`` takes remote control input :type inputmode: str """ def __init__(self, ev3handle, layer=1, portnum=1, inputmode='proximity'): """ Class constructor. """ # Defining infrared sensor mode parameters # [outputformat, mode, dataset] self.modepar = { 'proximity': [1, 0, 1], 'seeker': [1, 1, 2], 'remote': [1, 2, 4] } # Instantiating base class Sensor super(Infrared, self).__init__(ev3handle, layer=layer, portnum=portnum) # Checking for valid sensor if self._info[self._sensorname][0] != 'Infrared': raise NameError('Incorrect sensor type on port : ' + str(portnum)) # Assining sensor specific attributes self.inputmode = inputmode @property def output(self): """ Contains the sensor output based on the `inputmode` (`read only`). * ``0`` to ``100`` (`inputmode='proximity'`) * tuple of integers (``azymuth``, ``proximity``) (`inputmode='seeker'`) * tuple of integers (``channel``, ``buttoncode``) (`inputmode='remote'`) * channel ``1``, ``2``, ``3``, ``4`` * buttoncode * ``0`` = No button * ``1`` = Button 1 * ``2`` = Button 2 * ``3`` = Button 3 * ``4`` = Button 4 * ``5`` = Buttons 1 and 3 * ``6`` = Buttons 1 and 4 * ``7`` = Buttons 2 and 3 * ``8`` = Buttons 2 and 4 * ``9`` = Beacon Mode is on * ``10`` = Buttons 1 and 2 * ``11`` = Buttons 3 and 4 """ data = self._read_output() if self._inputmode == 'proximity': result = int(data[0]) elif self._inputmode == 'seeker': if not np.isnan(data[1]): result = (int(data[0]), int(data[1])) else: raise NameError('Incorrect remote channel or beacon is off.') elif self._inputmode == 'remote': index = np.nonzero(data)[0] if len(index) > 0: channel = index[0]+1 button = int(data[index[0]]) result = (channel, button) else: result = (0, 0) return result @output.setter def output(self, _): print('"output" is a read only property.')
[docs]class Gyro(Sensor): """ The class to represent the EV3 gyro sensor. Set up a gyro sensor on port number 1 >>> from pyev3.brick import LegoEV3 >>> from pyev3.devices import Gyro >>> myev3 = LegoEV3(commtype='usb') >>> mysensor = Gyro(myev3, portnum=1, inputmode='rate') :param ev3handle: ``LegoEV3`` instance representing the EV3 brick. :type ev3handle: object :param layer: The layer of the brick ``1`` or ``2`` in a daisy-chain configuration. :type layer: int :param portnum: The brick input port connected to the sensor. Possible values are ``1``, ``2``, ``3``, or ``4``. :type portnum: int :param inputmode: * ``'angle'`` angular position in deg * ``'rate'`` angular speed up to 440 deg/s * ``'angle&rate'`` angular position and rate :type inputmode: str """ def __init__(self, ev3handle, layer=1, portnum=1, inputmode='angle'): """ Class constructor. """ # Defining gyro sensor mode parameters # [outputformat, mode, dataset] self.modepar = { 'angle': [1, 0, 1], 'rate': [1, 1, 1], 'angle&rate': [1, 3, 2] } # Instantiating base class Sensor super(Gyro, self).__init__(ev3handle, layer=layer, portnum=portnum) # Checking for valid sensor if self._info[self._sensorname][0] != 'Gyro': raise NameError('Incorrect sensor type on port : ' + str(portnum)) # Assining sensor specific attributes self.inputmode = inputmode @property def output(self): """ Contains the sensor output based on the `inputmode` (`read only`). * integer ``angle`` (`inputmode='angle'`) * integer ``rate`` (`inputmode='rate'`) * tuple of integers (``angle``, ``rate``) (`inputmode='angle&rate'`) """ data = self._read_output() if self._inputmode == 'angle': result = int(data[0]) elif self._inputmode == 'rate': result = int(data[0]) elif self._inputmode == 'angle&rate': result = (int(data[0]), int(data[1])) return result @output.setter def output(self, _): print('"output" is a read only property.')