diff --git a/.flake8 b/.flake8 index 4e0bf2c..89638b4 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,7 @@ [flake8] +docstring_style=sphinx max-line-length = 127 -ignore = D, Q000, P101, W605, S311, PLW, PLC, PLR +ignore = D400, Q000, P101, W605, S311, PLW, PLC, PLR per-file-ignores = buildhat/__init__.py:F401 exclude = docs/conf.py, docs/sphinxcontrib/cmtinc-buildhat.py, docs/sphinx_selective_exclude/*.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 0ae4c92..8e72fc3 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -31,6 +31,7 @@ jobs: echo "PYTHONPATH=." >> $GITHUB_ENV - name: Lint with flake8 run: | + flake8 . --version # stop the build if there are Python syntax errors or undefined names flake8 . --count --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide diff --git a/buildhat/__init__.py b/buildhat/__init__.py index 2d76e9f..da3a147 100644 --- a/buildhat/__init__.py +++ b/buildhat/__init__.py @@ -1,3 +1,5 @@ +"""Provide all the classes we need for build HAT""" + from .color import ColorSensor from .colordistance import ColorDistanceSensor from .distance import DistanceSensor diff --git a/buildhat/color.py b/buildhat/color.py index 757a0d0..aacaaed 100644 --- a/buildhat/color.py +++ b/buildhat/color.py @@ -1,3 +1,5 @@ +"""Color sensor handling functionality""" + import math from collections import deque from threading import Condition @@ -11,7 +13,13 @@ class ColorSensor(Device): :param port: Port of device :raises DeviceError: Occurs if there is no color sensor attached to port """ + def __init__(self, port): + """ + Initialise color sensor + + :param port: Port of device + """ super().__init__(port) self.reverse() self.mode(6) @@ -19,8 +27,11 @@ def __init__(self, port): self._old_color = None def segment_color(self, r, g, b): - """Returns the color name from RGB + """Return the color name from RGB + :param r: Red + :param g: Green + :param b: Blue :return: Name of the color as a string :rtype: str """ @@ -46,6 +57,9 @@ def rgb_to_hsv(self, r, g, b): Based on https://www.rapidtables.com/convert/color/rgb-to-hsv.html algorithm + :param r: Red + :param g: Green + :param b: Blue :return: HSV representation of color :rtype: tuple """ @@ -69,7 +83,7 @@ def rgb_to_hsv(self, r, g, b): return int(h), int(s * 100), int(v * 100) def get_color(self): - """Returns the color + """Return the color :return: Name of the color as a string :rtype: str @@ -78,7 +92,7 @@ def get_color(self): return self.segment_color(r, g, b) def get_ambient_light(self): - """Returns the ambient light + """Return the ambient light :return: Ambient light :rtype: int @@ -90,7 +104,7 @@ def get_ambient_light(self): return int(sum(readings) / len(readings)) def get_reflected_light(self): - """Returns the reflected light + """Return the reflected light :return: Reflected light :rtype: int @@ -115,7 +129,7 @@ def _avgrgbi(self, reads): return rgbi def get_color_rgbi(self): - """Returns the color + """Return the color :return: RGBI representation :rtype: list @@ -127,7 +141,7 @@ def get_color_rgbi(self): return self._avgrgbi(reads) def get_color_hsv(self): - """Returns the color + """Return the color :return: HSV representation :rtype: tuple @@ -160,7 +174,7 @@ def _cb_handle(self, lst): self._cond.notify() def wait_until_color(self, color): - """Waits until specific color + """Wait until specific color :param color: Color to look for """ @@ -175,7 +189,10 @@ def wait_until_color(self, color): self.callback(None) def wait_for_new_color(self): - """Waits for new color or returns immediately if first call + """Wait for new color or returns immediately if first call + + :return: Name of the color as a string + :rtype: str """ self.mode(5) if self._old_color is None: @@ -192,7 +209,5 @@ def wait_for_new_color(self): return self._old_color def on(self): - """ - Turns on the sensor and LED - """ + """Turn on the sensor and LED""" self._write("port {} ; plimit 1 ; set -1\r".format(self.port)) diff --git a/buildhat/colordistance.py b/buildhat/colordistance.py index 7f7d52d..e6020eb 100644 --- a/buildhat/colordistance.py +++ b/buildhat/colordistance.py @@ -1,3 +1,5 @@ +"""Color distance sensor handling functionality""" + import math from collections import deque from threading import Condition @@ -11,7 +13,13 @@ class ColorDistanceSensor(Device): :param port: Port of device :raises DeviceError: Occurs if there is no colordistance sensor attached to port """ + def __init__(self, port): + """ + Initialise color distance sensor + + :param port: Port of device + """ super().__init__(port) self.on() self.mode(6) @@ -19,8 +27,11 @@ def __init__(self, port): self._old_color = None def segment_color(self, r, g, b): - """Returns the color name from HSV + """Return the color name from HSV + :param r: Red + :param g: Green + :param b: Blue :return: Name of the color as a string :rtype: str """ @@ -46,6 +57,9 @@ def rgb_to_hsv(self, r, g, b): Based on https://www.rapidtables.com/convert/color/rgb-to-hsv.html algorithm + :param r: Red + :param g: Green + :param b: Blue :return: HSV representation of color :rtype: tuple """ @@ -69,7 +83,7 @@ def rgb_to_hsv(self, r, g, b): return int(h), int(s * 100), int(v * 100) def get_color(self): - """Returns the color + """Return the color :return: Name of the color as a string :rtype: str @@ -78,7 +92,7 @@ def get_color(self): return self.segment_color(r, g, b) def get_ambient_light(self): - """Returns the ambient light + """Return the ambient light :return: Ambient light :rtype: int @@ -90,7 +104,7 @@ def get_ambient_light(self): return int(sum(readings) / len(readings)) def get_reflected_light(self): - """Returns the reflected light + """Return the reflected light :return: Reflected light :rtype: int @@ -117,7 +131,7 @@ def _avgrgb(self, reads): return rgb def get_color_rgb(self): - """Returns the color + """Return the color :return: RGBI representation :rtype: list @@ -139,7 +153,7 @@ def _cb_handle(self, lst): self._cond.notify() def wait_until_color(self, color): - """Waits until specific color + """Wait until specific color :param color: Color to look for """ @@ -154,7 +168,10 @@ def wait_until_color(self, color): self.callback(None) def wait_for_new_color(self): - """Waits for new color or returns immediately if first call + """Wait for new color or returns immediately if first call + + :return: Name of the color as a string + :rtype: str """ self.mode(6) if self._old_color is None: @@ -171,7 +188,5 @@ def wait_for_new_color(self): return self._old_color def on(self): - """ - Turns on the sensor and LED - """ + """Turn on the sensor and LED""" self._write("port {} ; plimit 1 ; set -1\r".format(self.port)) diff --git a/buildhat/devices.py b/buildhat/devices.py index 3aa2dca..0d141d7 100644 --- a/buildhat/devices.py +++ b/buildhat/devices.py @@ -1,3 +1,5 @@ +"""Functionality for handling Build HAT devices""" + import os import sys import weakref @@ -8,6 +10,7 @@ class Device: """Creates a single instance of the buildhat for all devices to use""" + _instance = None _started = 0 _device_names = {1: ("PassiveMotor", "PassiveMotor"), @@ -38,6 +41,11 @@ class Device: DISCONNECTED_DEVICE = "Disconnected" def __init__(self, port): + """Initialise device + + :param port: Port of device + :raises DeviceError: Occurs if incorrect port specified or port already used + """ if not isinstance(port, str) or len(port) != 1: raise DeviceError("Invalid port") p = ord(port) - ord('A') @@ -73,6 +81,7 @@ def _setup(device="/dev/serial0"): weakref.finalize(Device._instance, Device._instance.shutdown) def __del__(self): + """Handle deletion of device""" if hasattr(self, "port") and Device._used[self.port]: Device._used[self.port] = False self._conn.callit = None @@ -81,7 +90,11 @@ def __del__(self): @staticmethod def name_for_id(typeid): - """Translate integer type id to device name (python class)""" + """Translate integer type id to device name (python class) + + :param typeid: Type of device + :return: Name of device + """ if typeid in Device._device_names: return Device._device_names[typeid][0] else: @@ -89,7 +102,11 @@ def name_for_id(typeid): @staticmethod def desc_for_id(typeid): - """Translate integer type id to something more descriptive than the device name""" + """Translate integer type id to something more descriptive than the device name + + :param typeid: Type of device + :return: Description of device + """ if typeid in Device._device_names: return Device._device_names[typeid][1] else: @@ -101,23 +118,42 @@ def _conn(self): @property def connected(self): + """Whether device is connected or not + + :return: Connection status + """ return self._conn.connected @property def typeid(self): + """Type ID of device + + :return: Type ID + """ return self._typeid @property def typeidcur(self): + """Type ID currently present + + :return: Type ID + """ return self._conn.typeid @property def _hat(self): + """Hat instance + + :return: Hat instance + """ return Device._instance @property def name(self): - """Determines name of device on port""" + """Determine name of device on port + + :return: Device name + """ if not self.connected: return Device.DISCONNECTED_DEVICE elif self.typeidcur in self._device_names: @@ -127,7 +163,10 @@ def name(self): @property def description(self): - """Description of device on port""" + """Device on port info + + :return: Device description + """ if not self.connected: return Device.DISCONNECTED_DEVICE elif self.typeidcur in self._device_names: @@ -136,15 +175,25 @@ def description(self): return Device.UNKNOWN_DEVICE def isconnected(self): + """Whether it is connected or not + + :raises DeviceError: Occurs if device no longer the same + """ if not self.connected: raise DeviceError("No device found") if self.typeid != self.typeidcur: raise DeviceError("Device has changed") def reverse(self): + """Reverse polarity""" self._write("port {} ; plimit 1 ; set -1\r".format(self.port)) def get(self): + """Extract information from device + + :return: Data from device + :raises DeviceError: Occurs if device not in valid mode + """ self.isconnected() idx = -1 if self._simplemode != -1: @@ -160,6 +209,10 @@ def get(self): return self._conn.data def mode(self, modev): + """Set combimode or simple mode + + :param modev: List of tuples for a combimode, or integer for simple mode + """ self.isconnected() if isinstance(modev, list): self._combimode = 0 @@ -176,6 +229,10 @@ def mode(self, modev): self._simplemode = int(modev) def select(self): + """Request data from mode + + :raises DeviceError: Occurs if device not in valid mode + """ self.isconnected() if self._simplemode != -1: idx = self._simplemode @@ -186,18 +243,15 @@ def select(self): self._write("port {} ; select {}\r".format(self.port, idx)) def on(self): - """ - Turns on sensor - """ + """Turn on sensor""" self._write("port {} ; plimit 1 ; on\r".format(self.port)) def off(self): - """ - Turns off sensor - """ + """Turn off sensor""" self._write("port {} ; off\r".format(self.port)) def deselect(self): + """Unselect data from mode""" self._write("port {} ; select\r".format(self.port)) def _write(self, cmd): @@ -208,6 +262,10 @@ def _write1(self, data): self._write("port {} ; write1 {}\r".format(self.port, ' '.join('{:x}'.format(h) for h in data))) def callback(self, func): + """Set callback function + + :param func: Callback function + """ if func is not None: self.select() else: diff --git a/buildhat/distance.py b/buildhat/distance.py index 3267459..de221fa 100644 --- a/buildhat/distance.py +++ b/buildhat/distance.py @@ -1,3 +1,5 @@ +"""Distance sensor handling functionality""" + from threading import Condition from .devices import Device @@ -10,7 +12,14 @@ class DistanceSensor(Device): :param port: Port of device :raises DeviceError: Occurs if there is no distance sensor attached to port """ + def __init__(self, port, threshold_distance=100): + """ + Initialise distance sensor + + :param port: Port of device + :param threshold_distance: Optional + """ super().__init__(port) self.on() self.mode(0) @@ -41,15 +50,21 @@ def _intermediate(self, data): @property def distance(self): """ + Obtain previously stored distance + :getter: Returns distance + :return: Stored distance """ return self._distance @property def threshold_distance(self): """ + Threshold distance value + :getter: Returns threshold distance :setter: Sets threshold distance + :return: Threshold distance """ return self._threshold_distance @@ -59,7 +74,7 @@ def threshold_distance(self, value): def get_distance(self): """ - Returns the distance from ultrasonic sensor to object + Return the distance from ultrasonic sensor to object :return: Distance from ultrasonic sensor :rtype: int @@ -70,37 +85,45 @@ def get_distance(self): @property def when_in_range(self): """ - Handles motion events + Handle motion events :getter: Returns function to be called when in range :setter: Sets function to be called when in range + :return: In range callback """ return self._when_in_range @when_in_range.setter def when_in_range(self, value): - """Calls back, when distance in range""" + """Call back, when distance in range + + :param value: In range callback + """ self._when_in_range = value self.callback(self._intermediate) @property def when_out_of_range(self): """ - Handles motion events + Handle motion events :getter: Returns function to be called when out of range :setter: Sets function to be called when out of range + :return: Out of range callback """ return self._when_out_of_range @when_out_of_range.setter def when_out_of_range(self, value): - """Calls back, when distance out of range""" + """Call back, when distance out of range + + :param value: Out of range callback + """ self._when_out_of_range = value self.callback(self._intermediate) def wait_for_out_of_range(self, distance): - """Waits until object is farther than specified distance + """Wait until object is farther than specified distance :param distance: Distance """ @@ -111,7 +134,7 @@ def wait_for_out_of_range(self, distance): self._cond_data.wait() def wait_for_in_range(self, distance): - """Waits until object is closer than specified distance + """Wait until object is closer than specified distance :param distance: Distance """ @@ -124,9 +147,11 @@ def wait_for_in_range(self, distance): def eyes(self, *args): """ Brightness of LEDs on sensor + (Sensor Right Upper, Sensor Left Upper, Sensor Right Lower, Sensor Left Lower) - :param \*args: Four brightness arguments of 0 to 100 + :param args: Four brightness arguments of 0 to 100 + :raises DistanceSensorError: Occurs if invalid brightness passed """ out = [0xc5] if len(args) != 4: @@ -138,7 +163,5 @@ def eyes(self, *args): self._write1(out) def on(self): - """ - Turns on the sensor - """ + """Turn on the sensor""" self._write("port {} ; set -1\r".format(self.port)) diff --git a/buildhat/exc.py b/buildhat/exc.py index f6fcb08..2c0d3f9 100644 --- a/buildhat/exc.py +++ b/buildhat/exc.py @@ -1,22 +1,25 @@ +"""Exceptions for all build HAT classes""" + + class DistanceSensorError(Exception): - pass + """Error raised when invalid arguments passed to distance sensor functions""" class MatrixError(Exception): - pass + """Error raised when invalid arguments passed to matrix functions""" class LightError(Exception): - pass + """Error raised when invalid arguments passed to light functions""" class MotorError(Exception): - pass + """Error raised when invalid arguments passed to motor functions""" class BuildHATError(Exception): - pass + """Error raised when HAT not found""" class DeviceError(Exception): - pass + """Error raised when there is a Device issue""" diff --git a/buildhat/force.py b/buildhat/force.py index 77c66ea..c7378db 100644 --- a/buildhat/force.py +++ b/buildhat/force.py @@ -1,3 +1,5 @@ +"""Force sensor handling functionality""" + from threading import Condition from .devices import Device @@ -9,7 +11,13 @@ class ForceSensor(Device): :param port: Port of device :raises DeviceError: Occurs if there is no force sensor attached to port """ + def __init__(self, port, threshold_force=1): + """Initialise force sensor + + :param port: Port of device + :param threshold_force: Optional + """ super().__init__(port) self.mode([(0, 0), (1, 0), (3, 0)]) self._when_pressed = None @@ -36,9 +44,11 @@ def _intermediate(self, data): @property def threshold_force(self): - """ + """Threshold force + :getter: Returns threshold force :setter: Sets threshold force + :return: Threshold force """ return self._threshold_force @@ -47,7 +57,7 @@ def threshold_force(self, value): self._threshold_force = value def get_force(self): - """Returns the force in (N) + """Return the force in (N) :return: The force exerted on the button :rtype: int @@ -55,7 +65,8 @@ def get_force(self): return self.get()[0] def get_peak_force(self): - """Gets the maximum force registered since the sensor was reset + """Get the maximum force registered since the sensor was reset + (The sensor gets reset when the firmware is reloaded) :return: 0 - 100 @@ -64,7 +75,7 @@ def get_peak_force(self): return self.get()[2] def is_pressed(self): - """Gets whether the button is pressed + """Get whether the button is pressed :return: If button is pressed :rtype: bool @@ -73,36 +84,46 @@ def is_pressed(self): @property def when_pressed(self): - """Handles force events + """Handle force events :getter: Returns function to be called when pressed :setter: Sets function to be called when pressed + :return: Callback function """ return self._when_pressed @when_pressed.setter def when_pressed(self, value): - """Calls back, when button is has pressed""" + """Call back, when button is has pressed + + :param value: Callback function + """ self._when_pressed = value self.callback(self._intermediate) @property def when_released(self): - """Handles force events + """Handle force events :getter: Returns function to be called when released :setter: Sets function to be called when released + :return: Callback function """ return self._when_pressed @when_released.setter def when_released(self, value): - """Calls back, when button is has released""" + """Call back, when button is has released + + :param value: Callback function + """ self._when_released = value self.callback(self._intermediate) def wait_until_pressed(self, force=1): - """Waits until the button is pressed + """Wait until the button is pressed + + :param force: Optional """ self.callback(self._intermediate) with self._cond_force: @@ -111,7 +132,9 @@ def wait_until_pressed(self, force=1): self._cond_force.wait() def wait_until_released(self, force=0): - """Waits until the button is released + """Wait until the button is released + + :param force: Optional """ self.callback(self._intermediate) with self._cond_force: diff --git a/buildhat/hat.py b/buildhat/hat.py index 9de897c..d0e5738 100644 --- a/buildhat/hat.py +++ b/buildhat/hat.py @@ -1,10 +1,16 @@ +"""HAT handling functionality""" + from .devices import Device class Hat: - """Allows enumeration of devices which are connected to the hat - """ + """Allows enumeration of devices which are connected to the hat""" + def __init__(self, device=None): + """Hat + + :param device: Optional string containing path to Build HAT serial device + """ self.led_status = -1 if not device: Device._setup() @@ -12,7 +18,7 @@ def __init__(self, device=None): Device._setup(device) def get(self): - """Gets devices which are connected or disconnected + """Get devices which are connected or disconnected :return: Dictionary of devices :rtype: dict @@ -33,7 +39,7 @@ def get(self): return devices def get_vin(self): - """Gets the voltage present on the input power jack + """Get the voltage present on the input power jack :return: Voltage on the input power jack :rtype: float @@ -50,9 +56,11 @@ def _set_led(self, intmode): Device._instance.write("ledmode {}\r".format(intmode).encode()) def set_leds(self, color="voltage"): - """Sets the two LEDs on or off on the BuildHAT. By default - the color depends on the input voltage with green being nominal at around 8V + """Set the two LEDs on or off on the BuildHAT. + + By default the color depends on the input voltage with green being nominal at around 8V (The fastest time the LEDs can be perceptually toggled is around 0.025 seconds) + :param color: orange, green, both, off, or voltage (default) """ if color == "orange": @@ -70,6 +78,7 @@ def set_leds(self, color="voltage"): def orange_led(self, status=True): """Turn the BuildHAT's orange LED on or off + :param status: True to turn it on, False to turn it off """ if status: @@ -89,6 +98,7 @@ def orange_led(self, status=True): def green_led(self, status=True): """Turn the BuildHAT's green LED on or off + :param status: True to turn it on, False to turn it off """ if status: diff --git a/buildhat/light.py b/buildhat/light.py index 0e06e51..ad7cb0d 100644 --- a/buildhat/light.py +++ b/buildhat/light.py @@ -1,3 +1,5 @@ +"""Light device handling functionality""" + from .devices import Device from .exc import LightError @@ -10,7 +12,13 @@ class Light(Device): :param port: Port of device :raises DeviceError: Occurs if there is no light attached to port """ + def __init__(self, port): + """ + Initialise light + + :param port: Port of device + """ super().__init__(port) def brightness(self, brightness): @@ -18,6 +26,7 @@ def brightness(self, brightness): Brightness of LEDs :param brightness: Brightness argument 0 to 100 + :raises LightError: Occurs if invalid brightness passed """ if not (brightness >= 0 and brightness <= 100): raise LightError("Need brightness arg, of 0 to 100") diff --git a/buildhat/matrix.py b/buildhat/matrix.py index a3a4c3b..78a637b 100644 --- a/buildhat/matrix.py +++ b/buildhat/matrix.py @@ -1,3 +1,5 @@ +"""Matrix device handling functionality""" + from .devices import Device from .exc import MatrixError @@ -8,7 +10,12 @@ class Matrix(Device): :param port: Port of device :raises DeviceError: Occurs if there is no LED matrix attached to port """ + def __init__(self, port): + """Initialise matrix + + :param port: Port of device + """ super().__init__(port) self.on() self.mode(2) @@ -17,8 +24,9 @@ def __init__(self, port): def set_pixels(self, matrix, display=True): """Write pixel data to LED matrix - :param pixels: 3x3 list of tuples, with colour (0–10) and brightness (0–10) (see example for more detail) + :param matrix: 3x3 list of tuples, with colour (0–10) and brightness (0–10) (see example for more detail) :param display: Whether to update matrix or not + :raises MatrixError: Occurs if invalid matrix height/width provided """ if len(matrix) != 3: raise MatrixError("Incorrect matrix height") @@ -47,6 +55,7 @@ def strtocolor(colorstr): :param colorstr: str of a valid color :return: (0-10) representing the color :rtype: int + :raises MatrixError: Occurs if invalid color specified """ if colorstr == "pink": return 1 @@ -79,6 +88,7 @@ def normalize_pixel(pixel): :param pixel: tuple of colour (0–10) or string (ie:"red") and brightness (0–10) :return: (color, brightness) integers :rtype: tuple + :raises MatrixError: Occurs if invalid pixel specified """ if isinstance(pixel, tuple): c, brightness = pixel # pylint: disable=unpacking-non-sequence @@ -96,9 +106,10 @@ def normalize_pixel(pixel): @staticmethod def validate_coordinate(coord): - """"Validate an x,y coordinate for the 3x3 Matrix + """Validate an x,y coordinate for the 3x3 Matrix :param coord: tuple of 0-2 for the X coordinate and 0-2 for the Y coordinate + :raises MatrixError: Occurs if invalid coordinate specified """ # pylint: disable=unsubscriptable-object if isinstance(coord, tuple): @@ -122,15 +133,20 @@ def clear(self, pixel=None): self._output() def off(self): - # Never send the "off" command to the port a Matrix is connected to - # Instead, just turn all the pixels off + """Pretends to turn matrix off + + Never send the "off" command to the port a Matrix is connected to + Instead, just turn all the pixels off + """ self.clear() def level(self, level): """Use the matrix as a "level" meter from 0-9 + (The level meter is expressed in green which seems to be unchangeable) :param level: The height of the bar graph, 0-9 + :raises MatrixError: Occurs if invalid level specified """ if not isinstance(level, int): raise MatrixError("Invalid level, not integer") @@ -165,6 +181,7 @@ def set_transition(self, transition): fade which will cause the fade to "pop" in brightness. :param transition: Transition mode (0-2) + :raises MatrixError: Occurs if invalid transition """ if not isinstance(transition, int): raise MatrixError("Invalid transition, not integer") diff --git a/buildhat/motors.py b/buildhat/motors.py index c226baa..3dbd162 100644 --- a/buildhat/motors.py +++ b/buildhat/motors.py @@ -1,3 +1,5 @@ +"""Motor device handling functionality""" + import threading import time from collections import deque @@ -16,6 +18,10 @@ class PassiveMotor(Device): """ def __init__(self, port): + """Initialise motor + + :param port: Port of device + """ super().__init__(port) self._default_speed = 20 self._currentspeed = 0 @@ -23,9 +29,10 @@ def __init__(self, port): self.bias(0.3) def set_default_speed(self, default_speed): - """Sets the default speed of the motor + """Set the default speed of the motor :param default_speed: Speed ranging from -100 to 100 + :raises MotorError: Occurs if invalid speed passed """ if not (default_speed >= -100 and default_speed <= 100): raise MotorError("Invalid Speed") @@ -35,6 +42,7 @@ def start(self, speed=None): """Start motor :param speed: Speed ranging from -100 to 100 + :raises MotorError: Occurs if invalid speed passed """ if self._currentspeed == speed: # Already running at this speed, do nothing @@ -50,23 +58,35 @@ def start(self, speed=None): self._write(cmd) def stop(self): - """Stops motor""" + """Stop motor""" cmd = "port {} ; off\r".format(self.port) self._write(cmd) self._currentspeed = 0 def plimit(self, plimit): + """Limit power + + :param plimit: Value 0 to 1 + :raises MotorError: Occurs if invalid plimit value passed + """ if not (plimit >= 0 and plimit <= 1): raise MotorError("plimit should be 0 to 1") self._write("port {} ; plimit {}\r".format(self.port, plimit)) def bias(self, bias): + """Bias motor + + :param bias: Value 0 to 1 + :raises MotorError: Occurs if invalid bias value passed + """ if not (bias >= 0 and bias <= 1): raise MotorError("bias should be 0 to 1") self._write("port {} ; bias {}\r".format(self.port, bias)) class MotorRunmode(Enum): + """Current mode motor is in""" + NONE = 0 FREE = 1 DEGREES = 2 @@ -81,6 +101,10 @@ class Motor(Device): """ def __init__(self, port): + """Initialise motor + + :param port: Port of device + """ super().__init__(port) self.default_speed = 20 self._currentspeed = 0 @@ -95,19 +119,22 @@ def __init__(self, port): self._runmode = MotorRunmode.NONE def set_default_speed(self, default_speed): - """Sets the default speed of the motor + """Set the default speed of the motor :param default_speed: Speed ranging from -100 to 100 + :raises MotorError: Occurs if invalid speed passed """ if not (default_speed >= -100 and default_speed <= 100): raise MotorError("Invalid Speed") self.default_speed = default_speed def run_for_rotations(self, rotations, speed=None, blocking=True): - """Runs motor for N rotations + """Run motor for N rotations :param rotations: Number of rotations :param speed: Speed ranging from -100 to 100 + :param blocking: Whether call should block till finished + :raises MotorError: Occurs if invalid speed passed """ self._runmode = MotorRunmode.DEGREES if speed is None: @@ -156,7 +183,8 @@ def _run_to_position(self, degrees, speed, direction): self._runmode = MotorRunmode.NONE def _run_positional_ramp(self, pos, newpos, speed): - """ + """Ramp motor + :param pos: Current motor position in decimal rotations (from preset position) :param newpos: New motor postion in decimal rotations (from preset position) :param speed: -100 to 100 @@ -174,12 +202,14 @@ def _run_positional_ramp(self, pos, newpos, speed): self.coast() def run_for_degrees(self, degrees, speed=None, blocking=True): - """Runs motor for N degrees + """Run motor for N degrees Speed of 1 means 1 revolution / second :param degrees: Number of degrees to rotate :param speed: Speed ranging from -100 to 100 + :param blocking: Whether call should block till finished + :raises MotorError: Occurs if invalid speed passed """ self._runmode = MotorRunmode.DEGREES if speed is None: @@ -194,10 +224,13 @@ def run_for_degrees(self, degrees, speed=None, blocking=True): self._run_for_degrees(degrees, speed) def run_to_position(self, degrees, speed=None, blocking=True, direction="shortest"): - """Runs motor to position (in degrees) + """Run motor to position (in degrees) :param degrees: Position in degrees from -180 to 180 :param speed: Speed ranging from 0 to 100 + :param blocking: Whether call should block till finished + :param direction: shortest (default)/clockwise/anticlockwise + :raises MotorError: Occurs if invalid speed or angle passed """ self._runmode = MotorRunmode.DEGREES if speed is None: @@ -225,10 +258,12 @@ def _run_for_seconds(self, seconds, speed): self._runmode = MotorRunmode.NONE def run_for_seconds(self, seconds, speed=None, blocking=True): - """Runs motor for N seconds + """Run motor for N seconds :param seconds: Time in seconds :param speed: Speed ranging from -100 to 100 + :param blocking: Whether call should block till finished + :raises MotorError: Occurs when invalid speed specified """ self._runmode = MotorRunmode.SECONDS if speed is None: @@ -246,6 +281,7 @@ def start(self, speed=None): """Start motor :param speed: Speed ranging from -100 to 100 + :raises MotorError: Occurs when invalid speed specified """ if self._runmode == MotorRunmode.FREE: if self._currentspeed == speed: @@ -269,13 +305,13 @@ def start(self, speed=None): self._write(cmd) def stop(self): - """Stops motor""" + """Stop motor""" self._runmode = MotorRunmode.NONE self._currentspeed = 0 self.coast() def get_position(self): - """Gets position of motor with relation to preset position (can be negative or positive) + """Get position of motor with relation to preset position (can be negative or positive) :return: Position of motor in degrees from preset position :rtype: int @@ -283,7 +319,7 @@ def get_position(self): return self.get()[1] def get_aposition(self): - """Gets absolute position of motor + """Get absolute position of motor :return: Absolute position of motor from -180 to 180 :rtype: int @@ -291,7 +327,7 @@ def get_aposition(self): return self.get()[2] def get_speed(self): - """Gets speed of motor + """Get speed of motor :return: Speed of motor :rtype: int @@ -301,10 +337,11 @@ def get_speed(self): @property def when_rotated(self): """ - Handles rotation events + Handle rotation events :getter: Returns function to be called when rotated :setter: Sets function to be called when rotated + :return: Callback function """ return self._when_rotated @@ -320,29 +357,49 @@ def _intermediate(self, data): @when_rotated.setter def when_rotated(self, value): - """Calls back, when motor has been rotated""" + """Call back, when motor has been rotated + + :param value: Callback function + """ self._when_rotated = value self.callback(self._intermediate) def plimit(self, plimit): + """Limit power + + :param plimit: Value 0 to 1 + :raises MotorError: Occurs if invalid plimit value passed + """ if not (plimit >= 0 and plimit <= 1): raise MotorError("plimit should be 0 to 1") self._write("port {} ; plimit {}\r".format(self.port, plimit)) def bias(self, bias): + """Bias motor + + :param bias: Value 0 to 1 + :raises MotorError: Occurs if invalid bias value passed + """ if not (bias >= 0 and bias <= 1): raise MotorError("bias should be 0 to 1") self._write("port {} ; bias {}\r".format(self.port, bias)) def pwm(self, pwmv): + """PWM motor + + :param pwmv: Value -1 to 1 + :raises MotorError: Occurs if invalid pwm value passed + """ if not (pwmv >= -1 and pwmv <= 1): raise MotorError("pwm should be -1 to 1") self._write("port {} ; pwm ; set {}\r".format(self.port, pwmv)) def coast(self): + """Coast motor""" self._write("port {} ; coast\r".format(self.port)) def float(self): + """Float motor""" self.pwm(0) @@ -353,21 +410,27 @@ class MotorPair: :param motorb: Other motor in pair to drive :raises DeviceError: Occurs if there is no motor attached to port """ + def __init__(self, leftport, rightport): + """Initialise pair of motors + + :param leftport: Left motor port + :param rightport: Right motor port + """ super().__init__() self._leftmotor = Motor(leftport) self._rightmotor = Motor(rightport) self.default_speed = 20 def set_default_speed(self, default_speed): - """Sets the default speed of the motor + """Set the default speed of the motor :param default_speed: Speed ranging from -100 to 100 """ self.default_speed = default_speed def run_for_rotations(self, rotations, speedl=None, speedr=None): - """Runs pair of motors for N rotations + """Run pair of motors for N rotations :param rotations: Number of rotations :param speedl: Speed ranging from -100 to 100 @@ -380,7 +443,7 @@ def run_for_rotations(self, rotations, speedl=None, speedr=None): self.run_for_degrees(int(rotations * 360), speedl, speedr) def run_for_degrees(self, degrees, speedl=None, speedr=None): - """Runs pair of motors for degrees + """Run pair of motors for degrees :param degrees: Number of degrees :param speedl: Speed ranging from -100 to 100 @@ -400,7 +463,7 @@ def run_for_degrees(self, degrees, speedl=None, speedr=None): th2.join() def run_for_seconds(self, seconds, speedl=None, speedr=None): - """Runs pair for N seconds + """Run pair for N seconds :param seconds: Time in seconds :param speedl: Speed ranging from -100 to 100 @@ -422,7 +485,8 @@ def run_for_seconds(self, seconds, speedl=None, speedr=None): def start(self, speedl=None, speedr=None): """Start motors - :param speed: Speed ranging from -100 to 100 + :param speedl: Speed ranging from -100 to 100 + :param speedr: Speed ranging from -100 to 100 """ if speedl is None: speedl = self.default_speed @@ -437,11 +501,12 @@ def stop(self): self._rightmotor.stop() def run_to_position(self, degreesl, degreesr, speed=None, direction="shortest"): - """Runs pair to position (in degrees) + """Run pair to position (in degrees) - :param degreesl: Position in degrees for left motor - :param degreesr: Position in degrees for right motor - :param speed: Speed ranging from -100 to 100 + :param degreesl: Position in degrees for left motor + :param degreesr: Position in degrees for right motor + :param speed: Speed ranging from -100 to 100 + :param direction: shortest (default)/clockwise/anticlockwise """ if speed is None: th1 = threading.Thread(target=self._leftmotor._run_to_position, args=(degreesl, self.default_speed, direction)) diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index d1819da..a0db7b9 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -1,3 +1,5 @@ +"""Build HAT handling functionality""" + import queue import threading import time @@ -11,6 +13,8 @@ class HatState(Enum): + """Current state that hat is in""" + OTHER = 0 FIRMWARE = 1 NEEDNEWFIRMWARE = 2 @@ -18,22 +22,39 @@ class HatState(Enum): class Connection: + """Connection information for a port""" + def __init__(self): + """Initialise connection""" self.typeid = -1 self.connected = False self.callit = None def update(self, typeid, connected, callit=None): + """Update connection information for port + + :param typeid: Type ID of device on port + :param connected: Whether device is connected or not + :param callit: Callback function + """ self.typeid = typeid self.connected = connected self.callit = callit def cmp(str1, str2): + """Look for str2 in str1 + + :param str1: String to look in + :param str2: String to look for + :return: Whether str2 exists + """ return str1[:len(str2)] == str2 class BuildHAT: + """Interacts with Build HAT via UART interface""" + CONNECTED = ": connected to active ID" CONNECTEDPASSIVE = ": connected to passive ID" DISCONNECTED = ": disconnected" @@ -49,6 +70,14 @@ class BuildHAT: BOOT0_GPIO_NUMBER = 22 def __init__(self, firmware, signature, version, device="/dev/serial0"): + """Interact with Build HAT + + :param firmware: Firmware file + :param signature: Signature file + :param version: Firmware version + :param device: Serial device to use + :raises BuildHATError: Occurs if can't find HAT + """ self.cond = Condition() self.state = HatState.OTHER self.connections = [] @@ -129,6 +158,7 @@ def __init__(self, firmware, signature, version, device="/dev/serial0"): self.cond.wait() def resethat(self): + """Reset the HAT""" reset = DigitalOutputDevice(BuildHAT.RESET_GPIO_NUMBER) boot0 = DigitalOutputDevice(BuildHAT.BOOT0_GPIO_NUMBER) boot0.off() @@ -141,6 +171,11 @@ def resethat(self): time.sleep(0.5) def loadfirmware(self, firmware, signature): + """Load firmware + + :param firmware: Firmware to load + :param signature: Signature to load + """ with open(firmware, "rb") as f: firm = f.read() with open(signature, "rb") as f: @@ -161,7 +196,10 @@ def loadfirmware(self, firmware, signature): self.getprompt() def getprompt(self): - # Need to decide what we will do, when no prompt + """Loop until prompt is found + + Need to decide what we will do, when no prompt + """ while True: line = b"" try: @@ -172,6 +210,11 @@ def getprompt(self): break def checksum(self, data): + """Calculate checksum from data + + :param data: Data to calculate the checksum from + :return: Checksum that has been calculated + """ u = 1 for i in range(0, len(data)): if (u & 0x80000000) != 0: @@ -182,9 +225,14 @@ def checksum(self, data): return u def write(self, data): + """Write data to the serial port of Build HAT + + :param data: Data to write to Build HAT + """ self.ser.write(data) def shutdown(self): + """Turn off the Build HAT devices""" if not self.fin: self.fin = True self.running = False @@ -203,6 +251,10 @@ def shutdown(self): self.write(b"port 0 ; select ; port 1 ; select ; port 2 ; select ; port 3 ; select ; echo 0\r") def callbackloop(self, q): + """Event handling for callbacks + + :param q: Queue of callback events + """ while self.running: cb = q.get() # Test for empty tuple, which should only be passed when @@ -215,6 +267,12 @@ def callbackloop(self, q): q.task_done() def loop(self, cond, uselist, q): + """Event handling for Build HAT + + :param cond: Condition used to block user's script till we're ready + :param uselist: Whether we're using the HATs 'list' function or not + :param q: Queue for callback events + """ count = 0 while self.running: line = b"" diff --git a/buildhat/wedo.py b/buildhat/wedo.py index 42c518c..df99dd6 100644 --- a/buildhat/wedo.py +++ b/buildhat/wedo.py @@ -1,3 +1,5 @@ +"""WeDo sensor handling functionality""" + from .devices import Device @@ -7,13 +9,19 @@ class TiltSensor(Device): :param port: Port of device :raises DeviceError: Occurs if there is no tilt sensor attached to port """ + def __init__(self, port): + """ + Initialise tilt sensor + + :param port: Port of device + """ super().__init__(port) self.mode(0) def get_tilt(self): """ - Returns the tilt from tilt sensor to object + Return the tilt from tilt sensor :return: Tilt from tilt sensor :rtype: tuple @@ -27,13 +35,19 @@ class MotionSensor(Device): :param port: Port of device :raises DeviceError: Occurs if there is no motion sensor attached to port """ + def __init__(self, port): + """ + Initialise motion sensor + + :param port: Port of device + """ super().__init__(port) self.mode(0) def get_distance(self): """ - Returns the distance from motion sensor to object + Return the distance from motion sensor :return: Distance from motion sensor :rtype: int diff --git a/docs/buildhat/color.py b/docs/buildhat/color.py index 913cc2d..5f63812 100755 --- a/docs/buildhat/color.py +++ b/docs/buildhat/color.py @@ -1,3 +1,5 @@ +"""Example getting color values from color sensor""" + from buildhat import ColorSensor color = ColorSensor('C') diff --git a/docs/buildhat/colordistance.py b/docs/buildhat/colordistance.py index cf783cd..8a73964 100755 --- a/docs/buildhat/colordistance.py +++ b/docs/buildhat/colordistance.py @@ -1,3 +1,5 @@ +"""Example for color distance sensor""" + from buildhat import ColorDistanceSensor color = ColorDistanceSensor('C') diff --git a/docs/buildhat/distance.py b/docs/buildhat/distance.py index e75a9c6..19f1963 100755 --- a/docs/buildhat/distance.py +++ b/docs/buildhat/distance.py @@ -1,3 +1,5 @@ +"""Example for distance sensor""" + from signal import pause from buildhat import DistanceSensor, Motor @@ -15,10 +17,18 @@ def handle_in(distance): + """Within range + + :param distance: Distance + """ print("in range", distance) def handle_out(distance): + """Out of range + + :param distance: Distance + """ print("out of range", distance) diff --git a/docs/buildhat/force.py b/docs/buildhat/force.py index 3e9acd1..1c8f3cd 100755 --- a/docs/buildhat/force.py +++ b/docs/buildhat/force.py @@ -1,3 +1,5 @@ +"""Example using force sensor""" + from signal import pause from buildhat import ForceSensor, Motor @@ -19,10 +21,18 @@ def handle_pressed(force): + """Force sensor pressed + + :param force: Force value + """ print("pressed", force) def handle_released(force): + """Force sensor released + + :param force: Force value + """ print("released", force) diff --git a/docs/buildhat/hat.py b/docs/buildhat/hat.py index 4a67ed8..2f95ad2 100755 --- a/docs/buildhat/hat.py +++ b/docs/buildhat/hat.py @@ -1,3 +1,5 @@ +"""Example to print devices attached to hat""" + from buildhat import Hat hat = Hat() diff --git a/docs/buildhat/light.py b/docs/buildhat/light.py index e995302..7a0e5bc 100644 --- a/docs/buildhat/light.py +++ b/docs/buildhat/light.py @@ -1,3 +1,5 @@ +"""Example turning on/off LED lights""" + from time import sleep from buildhat import Light diff --git a/docs/buildhat/matrix.py b/docs/buildhat/matrix.py index ce522db..63277e5 100755 --- a/docs/buildhat/matrix.py +++ b/docs/buildhat/matrix.py @@ -1,3 +1,5 @@ +"""Example driving LED matrix""" + import random import time diff --git a/docs/buildhat/motor.py b/docs/buildhat/motor.py index 5f96535..8cab2e8 100755 --- a/docs/buildhat/motor.py +++ b/docs/buildhat/motor.py @@ -1,3 +1,5 @@ +"""Example driving motors""" + import time from buildhat import Motor @@ -7,6 +9,12 @@ def handle_motor(speed, pos, apos): + """Motor data + + :param speed: Speed of motor + :param pos: Position of motor + :param apos: Absolute position of motor + """ print("Motor", speed, pos, apos) diff --git a/docs/buildhat/motorpair.py b/docs/buildhat/motorpair.py index e5a8858..d392cbf 100755 --- a/docs/buildhat/motorpair.py +++ b/docs/buildhat/motorpair.py @@ -1,3 +1,5 @@ +"""Example for pair of motors""" + from buildhat import MotorPair pair = MotorPair('C', 'D') diff --git a/docs/buildhat/passivemotor.py b/docs/buildhat/passivemotor.py index 35272d3..68d0f26 100755 --- a/docs/buildhat/passivemotor.py +++ b/docs/buildhat/passivemotor.py @@ -1,3 +1,5 @@ +"""Passive motor example""" + import time from buildhat import PassiveMotor diff --git a/requirements-test.txt b/requirements-test.txt index d9a623c..b6d3acc 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,17 @@ flake8 +flake8-bandit +flake8-broken-line +flake8-bugbear +flake8-commas +flake8-comprehensions +flake8-debugger +flake8-docstrings +flake8-eradicate +flake8-isort +flake8-isort flake8-pylint -pyserial +flake8-quotes +flake8-rst-docstrings +flake8-string-format gpiozero +pyserial diff --git a/setup.py b/setup.py index 450ca86..372ba3f 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ #! /usr/bin/env python3 +"""Setup file""" + # Copyright (c) 2020-2021 Raspberry Pi Foundation # # SPDX-License-Identifier: MIT diff --git a/test/distance.py b/test/distance.py index 1120955..3348314 100644 --- a/test/distance.py +++ b/test/distance.py @@ -1,3 +1,5 @@ +"""Test distance sensor""" + import unittest from buildhat import DistanceSensor @@ -5,25 +7,31 @@ class TestDistance(unittest.TestCase): + """Test distance sensor""" def test_properties(self): + """Test properties of sensor""" d = DistanceSensor('A') self.assertIsInstance(d.distance, int) self.assertIsInstance(d.threshold_distance, int) def test_distance(self): + """Test obtaining distance""" d = DistanceSensor('A') self.assertIsInstance(d.get_distance(), int) def test_eyes(self): + """Test lighting LEDs on sensor""" d = DistanceSensor('A') d.eyes(100, 100, 100, 100) def test_duplicate_port(self): + """Test using same port""" d = DistanceSensor('A') # noqa: F841 self.assertRaises(DeviceError, DistanceSensor, 'A') def test_del(self): + """Test deleting sensor""" d = DistanceSensor('A') del d DistanceSensor('A') diff --git a/test/hat.py b/test/hat.py index 877aed7..9538ede 100644 --- a/test/hat.py +++ b/test/hat.py @@ -1,21 +1,27 @@ +"""Test hat functionality""" + import unittest from buildhat import Hat class TestHat(unittest.TestCase): + """Test hat functions""" def test_vin(self): + """Test voltage measure function""" h = Hat() vin = h.get_vin() self.assertGreaterEqual(vin, 7.2) self.assertLessEqual(vin, 8.5) def test_get(self): + """Test getting list of devices""" h = Hat() self.assertIsInstance(h.get(), dict) def test_serial(self): + """Test setting serial device""" Hat(device="/dev/serial0") diff --git a/test/motors.py b/test/motors.py index d35ee82..4d3275f 100644 --- a/test/motors.py +++ b/test/motors.py @@ -1,3 +1,5 @@ +"""Test motors""" + import time import unittest @@ -6,8 +8,10 @@ class TestMotor(unittest.TestCase): + """Test motors""" def test_rotations(self): + """Test motor rotating""" m = Motor('A') pos1 = m.get_position() m.run_for_rotations(2) @@ -16,6 +20,7 @@ def test_rotations(self): self.assertLess(abs(rotated - 2), 0.5) def test_position(self): + """Test motor goes to desired position""" m = Motor('A') m.run_to_position(0) pos1 = m.get_aposition() @@ -28,6 +33,7 @@ def test_position(self): self.assertLess(diff, 10) def test_time(self): + """Test motor runs for correct duration""" m = Motor('A') t1 = time.time() m.run_for_seconds(5) @@ -35,24 +41,28 @@ def test_time(self): self.assertEqual(int(t2 - t1), 5) def test_speed(self): + """Test setting motor speed""" m = Motor('A') m.set_default_speed(50) self.assertRaises(MotorError, m.set_default_speed, -101) self.assertRaises(MotorError, m.set_default_speed, 101) def test_plimit(self): + """Test altering power limit of motor""" m = Motor('A') m.plimit(0.5) self.assertRaises(MotorError, m.plimit, -1) self.assertRaises(MotorError, m.plimit, 2) def test_bias(self): + """Test setting motor bias""" m = Motor('A') m.bias(0.5) self.assertRaises(MotorError, m.bias, -1) self.assertRaises(MotorError, m.bias, 2) def test_pwm(self): + """Test PWMing motor""" m = Motor('A') m.pwm(0.3) time.sleep(0.5) @@ -61,6 +71,7 @@ def test_pwm(self): self.assertRaises(MotorError, m.pwm, 2) def test_callback(self): + """Test setting callback""" m = Motor('A') def handle_motor(speed, pos, apos): @@ -71,6 +82,7 @@ def handle_motor(speed, pos, apos): self.assertGreater(handle_motor.evt, 0) def test_none_callback(self): + """Test setting empty callback""" m = Motor('A') m.when_rotated = None m.start() @@ -78,27 +90,32 @@ def test_none_callback(self): m.stop() def test_duplicate_port(self): + """Test using same port for motor""" m1 = Motor('A') # noqa: F841 self.assertRaises(DeviceError, Motor, 'A') def test_del(self): + """Test deleting motor""" m1 = Motor('A') del m1 Motor('A') def test_continuous_start(self): + """Test starting motor for 5mins""" t = time.time() + (60 * 5) m = Motor('A') while time.time() < t: m.start(0) def test_continuous_degrees(self): + """Test setting degrees for 5mins""" t = time.time() + (60 * 5) m = Motor('A') while time.time() < t: m.run_for_degrees(0) def test_continuous_position(self): + """Test setting position of motor for 5mins""" t = time.time() + (60 * 5) m = Motor('A') while time.time() < t: