From b05f91909f0259569aa220258079265828ee4951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Van=C4=9Bk?= <linuxtardis@gmail.com> Date: Fri, 26 May 2023 00:37:21 +0200 Subject: [PATCH] Add API docs for Python EV3 sensors --- lib/ev3/color.py | 69 +++++++++++++++++++++++++++++++++++++++++++++--- lib/ev3/gyro.py | 45 +++++++++++++++++++++++++++++++ lib/ev3/infra.py | 16 +++++++++++ lib/ev3/sonic.py | 13 +++++++++ lib/ev3/touch.py | 7 +++++ lib/ev3/uart.py | 14 ++++++++++ 6 files changed, 161 insertions(+), 3 deletions(-) diff --git a/lib/ev3/color.py b/lib/ev3/color.py index 56655e3..ab90c39 100644 --- a/lib/ev3/color.py +++ b/lib/ev3/color.py @@ -19,7 +19,19 @@ COLOR_SENSOR_ID = const(29) class ColorSensorCalibration: + """ + Calibration constants used for offset and gain correction for RGB mode measurements. + """ def __init__(self, max_r: float, max_g: float, max_b: float, min_r: float, min_g: float, min_b: float): + """ + Convert observed min/max RGB values into scaling constants. + :param max_r: Maximum measured red ADC value + :param max_g: Maximum measured green ADC value + :param max_b: Maximum measured blue ADC value + :param min_r: Minimum measured red ADC value + :param min_g: Minimum measured green ADC value + :param min_b: Minimum measured blue ADC value + """ self.r_scale = 100 / (max_r - min_r) self.g_scale = 100 / (max_g - min_g) self.b_scale = 100 / (max_b - min_b) @@ -46,26 +58,63 @@ class ColorSensor(EV3UartSensor): PYBRICKS_RGB_CALIBRATION = ColorSensorCalibration(388.8, 360.0, 198.3, 1.2, 2.9, 7.1) def __init__(self, port: int, wait_ms: int | None = None): + """ + Initialize a new EV3 color sensor object at the given port. + :param port: Port to which the sensor is attached. Use one of Port.S1~Port.S4. + :param wait_ms: Wait for up to this time for the sensor to start communicating. + If not provided, the constructor will return immediately. + """ super().__init__(port, wait_ms, COLOR_SENSOR_ID) self.rgb_calibration = ColorSensor.DEFAULT_RGB_CALIBRATION def reflection(self) -> int: + """ + Measure surfare reflectivity for red light. The values + are calibrated within the sensor itself. + :return: Reflectivity in % + """ return self.read_raw_mode(ColorSensor.MODE_REFLECT)[0] def reflection_raw(self) -> int: + """ + Measure surfare reflectivity for red light. The values + are uncalibrated. Code using this function must thus perform + range scaling/shifting manually. + + This method will likely provide higher resolution than + reflection() and therefore it might be advantageous + to use this function for line-following applications. + :return: Reflectivity in sensor ADC units + """ items = self.read_raw_mode(ColorSensor.MODE_REFLECT_RAW) led_on = items[0] led_off = items[1] return max(led_off - led_on, 0) def ambient(self) -> int: + """ + Measure ambient light intensity. The values are calibrated + within the sensor itself. + :return: Light intensity in percent of something + """ return self.read_raw_mode(ColorSensor.MODE_AMBIENT)[0] def rgb_raw(self) -> tuple: + """ + Measure surface reflectivity for red, green and blue light. + Returned values are not calibrated. + :return: Uncalibrated reflectivity for all colors in sensor ADC units. + """ values = self.read_raw_mode(ColorSensor.MODE_RGB_RAW) return values[0], values[1], values[2] def rgb(self) -> tuple: + """ + Measure surface reflectivity for red, green and blue light. + Returned values are calibrated on Open-Cube according to + currently configured `rgb_calibration`. + :return: Reflectivity for all colors in % of maximum observed by the sensor + """ r, g, b = self.rgb_raw() cal = self.rgb_calibration @@ -76,13 +125,21 @@ class ColorSensor(EV3UartSensor): ) def hsv(self) -> tuple: + """ + Measure surface reflectivity for red, green and blue light. + Then, convert it to the HSV colorspace. + Returned values are calibrated on Open-Cube according to + currently configured `rgb_calibration`. + :return: Triplet (h,s,v) representing the surface color in the HSV colorspace. + Hue is in degrees, saturation and value are in percent. + """ # See https://dl.acm.org/doi/epdf/10.1145/965139.807361 R, G, B = self.rgb() R = R / 100 G = G / 100 B = B / 100 - V = max(R, G, B) # value [0-1] + V = max(R, G, B) # value [0-1] X = min(R, G, B) if V < 0.001: @@ -90,7 +147,7 @@ class ColorSensor(EV3UartSensor): return 0, 0, V * 100 span = (V - X) - S = span / V # saturation [0-1] + S = span / V # saturation [0-1] if S < 0.001: # gray, define hue as zero @@ -119,8 +176,14 @@ class ColorSensor(EV3UartSensor): return H * 60, S * 100, V * 100 - def color(self) -> int | None: + """ + Measure the color of a LEGO brick. + The measurement is provided by the sensor itself. + If this works poorly for your use case, hsv() might be a better choice. + :return: None if the sensor cannot identify a color, otherwise + one of lib.ev3.Color constants + """ code = self.read_raw_mode(ColorSensor.MODE_COLOR)[0] if code != 0: return code diff --git a/lib/ev3/gyro.py b/lib/ev3/gyro.py index 174e569..cd92b86 100644 --- a/lib/ev3/gyro.py +++ b/lib/ev3/gyro.py @@ -21,21 +21,40 @@ class GyroSensor(EV3UartSensor): MODE_TILT_ANGLE = const(6) def __init__(self, port: int, wait_ms: int | None = None): + """ + Initialize a new EV3 gyro sensor object at the given port. + :param port: Port to which the sensor is attached. Use one of Port.S1~Port.S4. + :param wait_ms: Wait for up to this time for the sensor to start communicating. + If not provided, the constructor will return immediately. + """ super().__init__(port, wait_ms, GYRO_SENSOR_ID) self.offset_value = 0 self.offset_mode = OFFSET_ANGLE # sensor boots up in GYRO-ANG def angle(self) -> int: + """ + Measure current sensor orientation. + :return: Sensor angle in degrees + """ if self.offset_mode != OFFSET_ANGLE: self.offset_mode = OFFSET_ANGLE self.offset_value = 0 return self.read_raw_mode(GyroSensor.MODE_ANGLE)[0] - self.offset_value def speed(self) -> int: + """ + Measure current sensor rate of rotation. + :return: Sensor angular speed in deg/s + """ self.offset_mode = OFFSET_INVALID return self.read_raw_mode(GyroSensor.MODE_RATE)[0] def reset_angle(self, angle: int): + """ + Adjust Open-Cube maintained zero point of angle()/tilst_angle()/angle_and_rate() readings. + :param angle: New value that the reading functions will return + (assuming that the sensor will not move) + """ if self.offset_mode == OFFSET_ANGLERATE: self.offset_value = self.read_raw_mode(GyroSensor.MODE_ANGLE_AND_RATE)[0] - angle elif self.offset_mode == OFFSET_TILTANGLE: @@ -45,6 +64,14 @@ class GyroSensor(EV3UartSensor): self.offset_mode = OFFSET_ANGLE def angle_and_speed(self): + """ + Measure both the sensor angle and rate of rotation at once. + + This is better than angle()+speed() as it uses a dedicated + sensor mode for measuring both at once. + + :return: (angle, rate) tuple in (deg/s, deg). + """ if self.offset_mode != OFFSET_ANGLERATE: self.offset_mode = OFFSET_ANGLERATE self.offset_value = 0 @@ -52,14 +79,32 @@ class GyroSensor(EV3UartSensor): return values[0] - self.offset_value, values[1] def coarse_speed(self): + """ + Measure current sensor rate of rotation with lower resolution, but higher range. + :return: Sensor angular speed in deg/s + """ self.offset_mode = OFFSET_INVALID return self.read_raw_mode(GyroSensor.MODE_FAST_RATE)[0] def tilt_speed(self): + """ + Measure current sensor rate of rotation along a second axis. + + Beware: not all sensors provide two-axis measurements. + + :return: Sensor angular speed in deg/s + """ self.offset_mode = OFFSET_INVALID return self.read_raw_mode(GyroSensor.MODE_TILT_RATE)[0] def tilt_angle(self): + """ + Measure current sensor orientation along a second axis. + + Beware: not all sensors provide two-axis measurements. + + :return: Sensor angle in degrees + """ if self.offset_mode != OFFSET_TILTANGLE: self.offset_mode = OFFSET_TILTANGLE self.offset_value = 0 diff --git a/lib/ev3/infra.py b/lib/ev3/infra.py index 952aa12..e1bacf8 100644 --- a/lib/ev3/infra.py +++ b/lib/ev3/infra.py @@ -13,12 +13,28 @@ class InfraredSensor(EV3UartSensor): MODE_REMOTE = const(2) def __init__(self, port: int, wait_ms: int | None = None): + """ + Initialize a new EV3 infrared sensor object at the given port. + :param port: Port to which the sensor is attached. Use one of Port.S1~Port.S4. + :param wait_ms: Wait for up to this time for the sensor to start communicating. + If not provided, the constructor will return immediately. + """ super().__init__(port, wait_ms, INFRA_SENSOR_ID) def distance(self) -> int: + """ + Measure the relative proximity to the nearest surface in percent. + :return: Proximity in percent + """ return self.read_raw_mode(InfraredSensor.MODE_PROXIMITY)[0] def seeker(self, channel: int) -> tuple: + """ + Determine where the remote controller for the given channel exists. + See also http://docs.ev3dev.org/projects/lego-linux-drivers/en/ev3dev-buster/sensor_data.html#lego-ev3-ir + :param channel: Which channel to check (1-4). + :return: Tuple (heading, distance). Heading ranges from -25 to +25, distance is 0-100. + """ items = self.read_raw_mode(InfraredSensor.MODE_SEEK) pair_offset = 2 * (channel - 1) return items[pair_offset + 0], items[pair_offset + 1] diff --git a/lib/ev3/sonic.py b/lib/ev3/sonic.py index 2fffb1e..218fb8a 100644 --- a/lib/ev3/sonic.py +++ b/lib/ev3/sonic.py @@ -16,15 +16,28 @@ class UltrasonicSensor(EV3UartSensor): MODE_SINGLE_CM = const(3) def __init__(self, port: int, wait_ms: int | None = None): + """ + Initialize a new EV3 ultrasonic sensor object at the given port. + :param port: Port to which the sensor is attached. Use one of Port.S1~Port.S4. + :param wait_ms: Wait for up to this time for the sensor to start communicating. + If not provided, the constructor will return immediately. + """ super().__init__(port, wait_ms, SONIC_SENSOR_ID) def distance(self, silent=False) -> int: + """ + Measure the distance to the nearest obstacle in front of the sensor. + :return: Distance in millimeters or a magic value of 2550 if the measurement fails. + """ cont_value = self.read_raw_mode(UltrasonicSensor.MODE_CONTINUOUS_CM)[0] if silent: self.start_set_mode(UltrasonicSensor.MODE_SINGLE_CM) return cont_value def presence(self) -> bool: + """ + Check if another ultrasonic sensor is active within the hearing distance of this sensor. + """ return self.read_raw_mode(UltrasonicSensor.MODE_LISTEN)[0] > 0 # this is likely not needed! distance(silent=True) seems to work OK-ish diff --git a/lib/ev3/touch.py b/lib/ev3/touch.py index 8e81c5f..7eee0ef 100644 --- a/lib/ev3/touch.py +++ b/lib/ev3/touch.py @@ -5,6 +5,10 @@ from lib.hw_defs.ports import SENSOR_PORT_PINS class TouchSensor: def __init__(self, port: int): + """ + Initialize a new EV3 touch sensor object at the given port. + :param port: Port to which the sensor is attached. Use one of Port.S1~Port.S4. + """ if port < 0 or port > 3: raise ValueError(f"Sensor port index {port} is out of range") pins = SENSOR_PORT_PINS[port] @@ -12,4 +16,7 @@ class TouchSensor: self.btn_pin = Pin(self.btn_pin_no, mode=Pin.IN) def pressed(self) -> bool: + """ + Checks if the button is pressed. + """ return self.btn_pin.value() == 1 diff --git a/lib/ev3/uart.py b/lib/ev3/uart.py index 720bc41..bcf9b3e 100644 --- a/lib/ev3/uart.py +++ b/lib/ev3/uart.py @@ -2,11 +2,21 @@ import ev3_sensor_ll class SensorMismatchError(RuntimeError): + """ + Incorrect sensor type has been connected to Open-Cube. + """ pass class EV3UartSensor(ev3_sensor_ll.EV3Sensor): def __init__(self, port: int, wait_ms: int | None = None, proper_id: int | None = None): + """ + Initialize a new generic EV3 sensor object at the given port. + :param port: Port to which the sensor is attached. Use one of Port.S1~Port.S4. + :param wait_ms: Wait for up to this time for the sensor to start communicating. + If not provided, the constructor will return immediately. + :param proper_id: If set, check that the connected EV3 sensor is using the given ID. + """ super().__init__(port) self._port = port self._proper_id = proper_id @@ -18,6 +28,10 @@ class EV3UartSensor(ev3_sensor_ll.EV3Sensor): raise def wait_for_connection(self, wait_ms: int = 3000): + """ + Wait for up to wait_ms milliseconds for the sensor + to start communicating. + """ super().wait_for_connection(wait_ms) if self._proper_id is not None and self.sensor_id() != self._proper_id: raise SensorMismatchError(f"Wrong EV3 sensor is plugged into port {self._port + 1}") -- GitLab