From 9d04b5278a625e48dc9d0ded9e5dcd14854908e5 Mon Sep 17 00:00:00 2001 From: Dan Trickey Date: Sun, 9 Jun 2019 21:19:37 +0100 Subject: [PATCH 01/23] Refactor serial logic into AbstractSerialBackend --- j5/backends/hardware/j5/serial.py | 80 +++++++++++++++++++++++ j5/backends/hardware/sr/v4/motor_board.py | 60 ++++------------- 2 files changed, 94 insertions(+), 46 deletions(-) create mode 100644 j5/backends/hardware/j5/serial.py diff --git a/j5/backends/hardware/j5/serial.py b/j5/backends/hardware/j5/serial.py new file mode 100644 index 00000000..b8e8d548 --- /dev/null +++ b/j5/backends/hardware/j5/serial.py @@ -0,0 +1,80 @@ +"""Abstract hardware backend implementation provided by j5 for serial comms.""" +from abc import abstractmethod +from functools import wraps +from typing import Callable, Optional, Set, Type, TypeVar + +from serial import Serial, SerialException, SerialTimeoutException + +from j5.backends import BackendMeta, CommunicationError, Environment +from j5.boards import Board + +RT = TypeVar("RT") # pragma: nocover + + +def handle_serial_error(func: Callable[..., RT]) -> Callable[..., RT]: # type: ignore + """ + Wrap functions that use the serial port, and rethrow the errors. + + This is a decorator that should be used to wrap any functions that call the serial + interface. It will catch and rethrow the errors as a CommunicationError, so that it + is more explicit what is going wrong. + """ + @wraps(func) + def catch_exceptions(*args, **kwargs): # type: ignore + try: + return func(*args, **kwargs) + except SerialTimeoutException as e: + raise CommunicationError(f"Serial Timeout Error: {e}") + except SerialException as e: + raise CommunicationError(f"Serial Error: {e}") + return catch_exceptions + + +class SerialHardwareBackend(metaclass=BackendMeta): + """An abstract class for creating backends that use Raw USB communication.""" + + @handle_serial_error + def __init__( + self, + serial_port: str, + serial_class: Type[Serial] = Serial, + baud: int = 115200, + timeout: float = 0.25, + ) -> None: + self._serial = serial_class( + port=serial_port, + baudrate=baud, + timeout=timeout, + ) + + @classmethod + @abstractmethod + def discover(cls) -> Set[Board]: + """Discover boards that this backend can control.""" + raise NotImplementedError # pragma: no cover + + @property + @abstractmethod + def environment(self) -> Environment: + """Environment the backend belongs too.""" + raise NotImplementedError # pragma: no cover + + @property + @abstractmethod + def firmware_version(self) -> Optional[str]: + """The firmware version of the board.""" + raise NotImplementedError # pragma: no cover + + @handle_serial_error + def read_serial_line(self) -> str: + """Read a line from the serial interface.""" + bdata = self._serial.readline() + + if len(bdata) == 0: + raise CommunicationError( + "Unable to communicate with motor board. ", + "Is it correctly powered?", + ) + + ldata = bdata.decode('utf-8') + return ldata.rstrip() diff --git a/j5/backends/hardware/sr/v4/motor_board.py b/j5/backends/hardware/sr/v4/motor_board.py index da9c28a5..970fc7fe 100644 --- a/j5/backends/hardware/sr/v4/motor_board.py +++ b/j5/backends/hardware/sr/v4/motor_board.py @@ -1,13 +1,16 @@ """Hardware Backend for the SR v4 motor board.""" -from functools import wraps -from typing import Callable, List, Optional, Set, Type, TypeVar, cast +from typing import Callable, List, Optional, Set, Type, cast -from serial import Serial, SerialException, SerialTimeoutException +from serial import Serial from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo -from j5.backends import Backend, CommunicationError +from j5.backends import CommunicationError from j5.backends.hardware.env import HardwareEnvironment +from j5.backends.hardware.j5.serial import ( + SerialHardwareBackend, + handle_serial_error, +) from j5.boards import Board from j5.boards.sr.v4.motor_board import MotorBoard from j5.components.motor import MotorInterface, MotorSpecialState, MotorState @@ -20,27 +23,6 @@ SPEED_COAST = 1 SPEED_BRAKE = 2 -RT = TypeVar("RT") # pragma: nocover - - -def handle_serial_error(func: Callable[..., RT]) -> Callable[..., RT]: # type: ignore - """ - Wrap functions that use the serial port, and rethrow the errors. - - This is a decorator that should be used to wrap any functions that call the serial - interface. It will catch and rethrow the errors as a CommunicationError, so that it - is more explicit what is going wrong. - """ - @wraps(func) - def catch_exceptions(*args, **kwargs): # type: ignore - try: - return func(*args, **kwargs) - except SerialTimeoutException as e: - raise CommunicationError(f"Serial Timeout Error: {e}") - except SerialException as e: - raise CommunicationError(f"Serial Error: {e}") - return catch_exceptions - def is_motor_board(port: ListPortInfo) -> bool: """Check if a ListPortInfo represents a MCV4B.""" @@ -50,7 +32,7 @@ def is_motor_board(port: ListPortInfo) -> bool: class SRV4MotorBoardHardwareBackend( MotorInterface, - Backend, + SerialHardwareBackend, ): """The hardware implementation of the SR v4 motor board.""" @@ -81,18 +63,18 @@ def discover( @handle_serial_error def __init__(self, serial_port: str, serial_class: Type[Serial] = Serial) -> None: + super(SRV4MotorBoardHardwareBackend, self).__init__( + serial_port=serial_port, + serial_class=serial_class, + baud=1000000, + ) + # Initialise our stored values for the state. self._state: List[MotorState] = [ MotorSpecialState.BRAKE for _ in range(0, 2) ] - self._serial = serial_class( - port=serial_port, - baudrate=1000000, - timeout=0.25, - ) - # Check we have the correct firmware version. version = self.firmware_version if version != "3": @@ -125,20 +107,6 @@ def send_command(self, command: int, data: Optional[int] = None) -> None: "Mismatch in command bytes written to serial interface.", ) - @handle_serial_error - def read_serial_line(self) -> str: - """Read a line from the serial interface.""" - bdata = self._serial.readline() - - if len(bdata) == 0: - raise CommunicationError( - "Unable to communicate with motor board. ", - "Is it correctly powered?", - ) - - ldata = bdata.decode('utf-8') - return ldata.rstrip() - @property def firmware_version(self) -> Optional[str]: """The firmware version of the board.""" From 0effdf6a5f093c0d46f7e36a35c6f8a6d169e4ca Mon Sep 17 00:00:00 2001 From: Dan Trickey Date: Wed, 12 Jun 2019 14:03:56 +0100 Subject: [PATCH 02/23] Refactor raw_usb backend to match the serial backend. --- .../hardware/j5/{raw_usb => }/raw_usb.py | 8 ++++++- j5/backends/hardware/j5/raw_usb/__init__.py | 23 ------------------- 2 files changed, 7 insertions(+), 24 deletions(-) rename j5/backends/hardware/j5/{raw_usb => }/raw_usb.py (91%) delete mode 100644 j5/backends/hardware/j5/raw_usb/__init__.py diff --git a/j5/backends/hardware/j5/raw_usb/raw_usb.py b/j5/backends/hardware/j5/raw_usb.py similarity index 91% rename from j5/backends/hardware/j5/raw_usb/raw_usb.py rename to j5/backends/hardware/j5/raw_usb.py index 6a87b11e..43e9fa22 100644 --- a/j5/backends/hardware/j5/raw_usb/raw_usb.py +++ b/j5/backends/hardware/j5/raw_usb.py @@ -1,4 +1,10 @@ -"""Abstract hardware backend implemention provided by j5 for Raw USB communication.""" +""" +Abstract hardware backend implemention provided by j5 for Raw USB communication. + +This has been written to reduce code duplication between backends for boards that +communicate very similarly. It has been written such that it could potentially be +distributed separately in the future, to remove the PyUSB dependency from the j5 core. +""" from abc import abstractmethod from functools import wraps diff --git a/j5/backends/hardware/j5/raw_usb/__init__.py b/j5/backends/hardware/j5/raw_usb/__init__.py deleted file mode 100644 index 5a56c738..00000000 --- a/j5/backends/hardware/j5/raw_usb/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Abstract hardware backend implemention provided by j5 for Raw USB communication. - -This has been written to reduce code duplication between backends for boards that -communicate very similarly. It has been written such that it could potentially be -distributed separately in the future, to remove the PyUSB dependency from the j5 core. -""" - -from .raw_usb import ( - RawUSBHardwareBackend, - ReadCommand, - USBCommunicationError, - WriteCommand, - handle_usb_error, -) - -__all__ = [ - "RawUSBHardwareBackend", - "ReadCommand", - "USBCommunicationError", - "WriteCommand", - "handle_usb_error", -] From 765f8755816c0ac710388f39a6e32d84c250e3a4 Mon Sep 17 00:00:00 2001 From: Dan Trickey Date: Wed, 12 Jun 2019 15:02:41 +0100 Subject: [PATCH 03/23] Implement discovery of Arduino --- j5/backends/hardware/arduino/__init__.py | 1 + j5/backends/hardware/arduino/uno.py | 88 ++++++++++++++++++++++++ j5/backends/hardware/j5/serial.py | 4 +- 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 j5/backends/hardware/arduino/__init__.py create mode 100644 j5/backends/hardware/arduino/uno.py diff --git a/j5/backends/hardware/arduino/__init__.py b/j5/backends/hardware/arduino/__init__.py new file mode 100644 index 00000000..aa5a8e61 --- /dev/null +++ b/j5/backends/hardware/arduino/__init__.py @@ -0,0 +1 @@ +"""Hardware backends for Arduino Boards.""" diff --git a/j5/backends/hardware/arduino/uno.py b/j5/backends/hardware/arduino/uno.py new file mode 100644 index 00000000..9e5473e3 --- /dev/null +++ b/j5/backends/hardware/arduino/uno.py @@ -0,0 +1,88 @@ +"""Arduino Uno Hardware Implementation.""" + +from typing import Callable, List, Optional, Set, Tuple, Type + +from serial import Serial +from serial.tools.list_ports import comports +from serial.tools.list_ports_common import ListPortInfo + +from j5.backends import CommunicationError +from j5.backends.hardware.env import HardwareEnvironment +from j5.backends.hardware.j5.serial import ( + SerialHardwareBackend, + handle_serial_error, +) +from j5.boards import Board +from j5.boards.arduino.uno import ArduinoUnoBoard + +USB_ID: Set[Tuple[int, int]] = { + (0x2341, 0x0043), # Fake Uno + (0x2a03, 0x0043), # Fake Uno + (0x1a86, 0x7523), # Real Uno +} + + +def is_arduino_uno(port: ListPortInfo) -> bool: + """Check if a ListPortInfo represents an Arduino Uno.""" + return (port.vid, port.pid) in USB_ID + + +class ArduinoUnoHardwareBackend( + SerialHardwareBackend, +): + """ + Hardware Backend for the Arduino Uno. + + Currently only for the SourceBots Arduino Firmware. + """ + + environment = HardwareEnvironment + board = ArduinoUnoBoard + + @classmethod + def discover( + cls, + find: Callable = comports, + serial_class: Type[Serial] = Serial, + ) -> Set[Board]: + """Discover all connected motor boards.""" + # Find all serial ports. + ports: List[ListPortInfo] = find() + + # Get a list of boards from the ports. + boards: Set[Board] = set() + for port in filter(is_arduino_uno, ports): + boards.add( + ArduinoUnoBoard( + "unknown", + cls(port.device, serial_class), + ), + ) + + return boards + + @handle_serial_error + def __init__(self, serial_port: str, serial_class: Type[Serial] = Serial) -> None: + super(ArduinoUnoHardwareBackend, self).__init__( + serial_port=serial_port, + serial_class=serial_class, + baud=115200, + ) + + count = 0 + + line = self.read_serial_line(empty=True) + while len(line) == 0: + line = self.read_serial_line(empty=True) + count += 1 + if count > 25: + raise CommunicationError(f"Arduino ({serial_port}) is not responding.") + + if line != "# Booted": + raise CommunicationError("Arduino Boot Error.") + self._version_line = self.read_serial_line() + + @property + def firmware_version(self) -> Optional[str]: + """The firmware version of the board.""" + return self._version_line diff --git a/j5/backends/hardware/j5/serial.py b/j5/backends/hardware/j5/serial.py index b8e8d548..27c2a2a1 100644 --- a/j5/backends/hardware/j5/serial.py +++ b/j5/backends/hardware/j5/serial.py @@ -66,11 +66,13 @@ def firmware_version(self) -> Optional[str]: raise NotImplementedError # pragma: no cover @handle_serial_error - def read_serial_line(self) -> str: + def read_serial_line(self, empty: bool = False) -> str: """Read a line from the serial interface.""" bdata = self._serial.readline() if len(bdata) == 0: + if empty: + return "" raise CommunicationError( "Unable to communicate with motor board. ", "Is it correctly powered?", From aa5318f78292eb9d6467a9a12a159bbb620851ff Mon Sep 17 00:00:00 2001 From: Dan Trickey Date: Wed, 12 Jun 2019 15:54:40 +0100 Subject: [PATCH 04/23] Check the firmware version and get the serial num --- j5/backends/hardware/arduino/uno.py | 70 +++++++++++++++++++++++++++-- j5/boards/arduino/uno.py | 2 +- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/j5/backends/hardware/arduino/uno.py b/j5/backends/hardware/arduino/uno.py index 9e5473e3..459c45cc 100644 --- a/j5/backends/hardware/arduino/uno.py +++ b/j5/backends/hardware/arduino/uno.py @@ -14,6 +14,7 @@ ) from j5.boards import Board from j5.boards.arduino.uno import ArduinoUnoBoard +from j5.components import GPIOPinInterface, GPIOPinMode, LEDInterface USB_ID: Set[Tuple[int, int]] = { (0x2341, 0x0043), # Fake Uno @@ -28,6 +29,8 @@ def is_arduino_uno(port: ListPortInfo) -> bool: class ArduinoUnoHardwareBackend( + LEDInterface, + GPIOPinInterface, SerialHardwareBackend, ): """ @@ -54,7 +57,7 @@ def discover( for port in filter(is_arduino_uno, ports): boards.add( ArduinoUnoBoard( - "unknown", + port.serial_number, cls(port.device, serial_class), ), ) @@ -69,8 +72,14 @@ def __init__(self, serial_port: str, serial_class: Type[Serial] = Serial) -> Non baud=115200, ) - count = 0 + self._pins = { + i: GPIOPinMode.DIGITAL_INPUT + for i in range(2, 20) + # Digital 2 - 13 + # Analogue 14 - 19 + } + count = 0 line = self.read_serial_line(empty=True) while len(line) == 0: line = self.read_serial_line(empty=True) @@ -80,9 +89,64 @@ def __init__(self, serial_port: str, serial_class: Type[Serial] = Serial) -> Non if line != "# Booted": raise CommunicationError("Arduino Boot Error.") + self._version_line = self.read_serial_line() + if self.firmware_version is not None: + version_ids = self.firmware_version.split(".") + else: + version_ids = ["0", "0", "0"] + + if int(version_ids[0]) < 2019 or int(version_ids[1]) < 6: + raise CommunicationError( + f"Unexpected firmware version: {self.firmware_version},", + f" expected at least: \"2019.6.0\".", + ) + @property def firmware_version(self) -> Optional[str]: """The firmware version of the board.""" - return self._version_line + return self._version_line.split("v")[1] + + def _command(self, command: str, params: List[str]) -> str: + """Send a command to the board.""" + + def set_gpio_pin_mode(self, identifier: int, pin_mode: GPIOPinMode) -> None: + """Set the hardware mode of a GPIO pin.""" + self._pins[identifier] = pin_mode + + def get_gpio_pin_mode(self, identifier: int) -> GPIOPinMode: + """Get the hardware mode of a GPIO pin.""" + return self._pins[identifier] + + def write_gpio_pin_digital_state(self, identifier: int, state: bool) -> None: + """Write to the digital state of a GPIO pin.""" + + def get_gpio_pin_digital_state(self, identifier: int) -> bool: + """Get the last written state of the GPIO pin.""" + return False + + def read_gpio_pin_digital_state(self, identifier: int) -> bool: + """Read the digital state of the GPIO pin.""" + return False + + def read_gpio_pin_analogue_value(self, identifier: int) -> float: + """Read the scaled analogue value of the GPIO pin.""" + return 0.0 + + def write_gpio_pin_dac_value(self, identifier: int, scaled_value: float) -> None: + """Write a scaled analogue value to the DAC on the GPIO pin.""" + # Uno doesn't have any of these. + raise NotImplementedError + + def write_gpio_pin_pwm_value(self, identifier: int, duty_cycle: float) -> None: + """Write a scaled analogue value to the PWM on the GPIO pin.""" + # Not implemented on ArduinoUnoBoard yet. + raise NotImplementedError + + def get_led_state(self, identifier: int) -> bool: + """Get the state of an LED.""" + return False + + def set_led_state(self, identifier: int, state: bool) -> None: + """Set the state of an LED.""" diff --git a/j5/boards/arduino/uno.py b/j5/boards/arduino/uno.py index b23d2e05..87ec8512 100644 --- a/j5/boards/arduino/uno.py +++ b/j5/boards/arduino/uno.py @@ -85,7 +85,7 @@ def serial(self) -> str: @property def firmware_version(self) -> Optional[str]: """Get the firmware version of the board.""" - return None + return self._backend.firmware_version @property def pins(self) -> Mapping[PinNumber, GPIOPin]: From f7ba1d26b21abe99e2ea5856ac93294d78c03fe7 Mon Sep 17 00:00:00 2001 From: Dan Trickey Date: Wed, 26 Jun 2019 20:54:02 +0100 Subject: [PATCH 05/23] Apply suggestions from code review Co-Authored-By: Kier Davis --- j5/backends/hardware/arduino/uno.py | 4 ++-- j5/backends/hardware/j5/serial.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/j5/backends/hardware/arduino/uno.py b/j5/backends/hardware/arduino/uno.py index 459c45cc..eac2b620 100644 --- a/j5/backends/hardware/arduino/uno.py +++ b/j5/backends/hardware/arduino/uno.py @@ -16,7 +16,7 @@ from j5.boards.arduino.uno import ArduinoUnoBoard from j5.components import GPIOPinInterface, GPIOPinMode, LEDInterface -USB_ID: Set[Tuple[int, int]] = { +USB_IDS: Set[Tuple[int, int]] = { (0x2341, 0x0043), # Fake Uno (0x2a03, 0x0043), # Fake Uno (0x1a86, 0x7523), # Real Uno @@ -25,7 +25,7 @@ def is_arduino_uno(port: ListPortInfo) -> bool: """Check if a ListPortInfo represents an Arduino Uno.""" - return (port.vid, port.pid) in USB_ID + return (port.vid, port.pid) in USB_IDS class ArduinoUnoHardwareBackend( diff --git a/j5/backends/hardware/j5/serial.py b/j5/backends/hardware/j5/serial.py index 27c2a2a1..b467e047 100644 --- a/j5/backends/hardware/j5/serial.py +++ b/j5/backends/hardware/j5/serial.py @@ -31,7 +31,7 @@ def catch_exceptions(*args, **kwargs): # type: ignore class SerialHardwareBackend(metaclass=BackendMeta): - """An abstract class for creating backends that use Raw USB communication.""" + """An abstract class for creating backends that use USB serial communication.""" @handle_serial_error def __init__( @@ -74,7 +74,7 @@ def read_serial_line(self, empty: bool = False) -> str: if empty: return "" raise CommunicationError( - "Unable to communicate with motor board. ", + "Unable to communicate with board. ", "Is it correctly powered?", ) From 6768cd7e12cc1ddf15c2073ea612a4fab8d4bb5d Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 13:33:13 +0100 Subject: [PATCH 06/23] Use same name for mocked-out arguments --- j5/backends/hardware/arduino/uno.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/j5/backends/hardware/arduino/uno.py b/j5/backends/hardware/arduino/uno.py index eac2b620..0ffea912 100644 --- a/j5/backends/hardware/arduino/uno.py +++ b/j5/backends/hardware/arduino/uno.py @@ -45,12 +45,12 @@ class ArduinoUnoHardwareBackend( @classmethod def discover( cls, - find: Callable = comports, + comports: Callable = comports, serial_class: Type[Serial] = Serial, ) -> Set[Board]: """Discover all connected motor boards.""" # Find all serial ports. - ports: List[ListPortInfo] = find() + ports: List[ListPortInfo] = comports() # Get a list of boards from the ports. boards: Set[Board] = set() From 9074abf8b687fc9e20dfd79bb799134dfdec2900 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 14:10:17 +0100 Subject: [PATCH 07/23] Add test for creating a ArduinoUnoHardwareBackend This requires moving MockSerial out into its own file so it can be shared between test_motor_board.py and test_uno.py. Some rework is required since the two boards have different expected baud rates. --- tests/backends/hardware/arduino/__init__.py | 1 + tests/backends/hardware/arduino/test_uno.py | 36 ++++++ tests/backends/hardware/j5/mock_serial.py | 82 ++++++++++++ .../hardware/sr/v4/test_motor_board.py | 120 ++++-------------- 4 files changed, 141 insertions(+), 98 deletions(-) create mode 100644 tests/backends/hardware/arduino/__init__.py create mode 100644 tests/backends/hardware/arduino/test_uno.py create mode 100644 tests/backends/hardware/j5/mock_serial.py diff --git a/tests/backends/hardware/arduino/__init__.py b/tests/backends/hardware/arduino/__init__.py new file mode 100644 index 00000000..501d89b2 --- /dev/null +++ b/tests/backends/hardware/arduino/__init__.py @@ -0,0 +1 @@ +"""Tests for arduino hardware implementations.""" diff --git a/tests/backends/hardware/arduino/test_uno.py b/tests/backends/hardware/arduino/test_uno.py new file mode 100644 index 00000000..fc4d3d93 --- /dev/null +++ b/tests/backends/hardware/arduino/test_uno.py @@ -0,0 +1,36 @@ +"""Tests for the Arduino Uno hardware implementation.""" + +from typing import Optional + +from tests.backends.hardware.j5.mock_serial import MockSerial + +from j5.backends.hardware.arduino.uno import ArduinoUnoHardwareBackend + + +class UnoSerial(MockSerial): + """UnoSerial is the same as MockSerial, but includes data we expect to receive.""" + + def __init__(self, + port: Optional[str] = None, + baudrate: int = 9600, + bytesize: int = 8, + parity: str = 'N', + stopbits: float = 1, + timeout: Optional[float] = None): + super().__init__( + port=port, + baudrate=baudrate, + bytesize=bytesize, + parity=parity, + stopbits=stopbits, + timeout=timeout, + expects=b"", + expected_baudrate=115200, + ) + + +def test_backend_initialisation() -> None: + """Test that we can initialise a ArduinoUnoHardwareBackend.""" + backend = ArduinoUnoHardwareBackend("/dev/ttyUSB1", UnoSerial) + assert type(backend) is ArduinoUnoHardwareBackend + assert type(backend._serial) is UnoSerial diff --git a/tests/backends/hardware/j5/mock_serial.py b/tests/backends/hardware/j5/mock_serial.py new file mode 100644 index 00000000..a9fa098d --- /dev/null +++ b/tests/backends/hardware/j5/mock_serial.py @@ -0,0 +1,82 @@ +"""A class that mocks serial.Serial.""" + +from typing import Optional + + +class MockSerial: + """This class mocks the behaviour of serial.Serial.""" + + def __init__(self, + port: Optional[str] = None, + baudrate: int = 9600, + bytesize: int = 8, + parity: str = 'N', + stopbits: float = 1, + timeout: Optional[float] = None, + expects: bytes = b'', + expected_baudrate: int = 9600, + ): + self._is_open: bool = True + self._buffer: bytes = b'' + self.port = port + self._expects = expects + + assert baudrate == expected_baudrate + assert bytesize == 8 + assert parity == 'N' + assert stopbits == 1 + assert timeout is not None + assert 0.1 <= timeout <= 0.3 # Acceptable range of timeouts + + def close(self) -> None: + """Close the serial port.""" + assert self._is_open # Check the port is open first. + self._is_open = False + + def flush(self) -> None: + """Flush the buffer on the serial port.""" + self._buffer = b'' + + def read(self, size: int = 1) -> bytes: + """Read size bytes from the input buffer.""" + assert len(self._buffer) >= size + + data = self._buffer[:size] + self._buffer = self._buffer[size:] + return data + + def readline(self) -> bytes: + """Read up to a newline on the serial port.""" + try: + pos = self._buffer.index(b'\n') + except ValueError: + return b'' + return self.read(pos) + + def write(self, data: bytes) -> int: + """Write the data to the serial port.""" + self.check_expects(data) + + # We only end up returning data once, check for that here. + if data == b'\x01': # Version Command + self.buffer_append(b'MCV4B:3', newline=True) + + return len(data) + + # Functions for helping us mock. + + def buffer_append(self, data: bytes, newline: bool = False) -> None: + """Append some data to the receive buffer.""" + self._buffer += data + if newline: + self._buffer += b'\n' + + def expects_prepend(self, data: bytes) -> None: + """Prepend some bytes to the output buffer that we expect to see.""" + self._expects = data + self._expects + + def check_expects(self, data: bytes) -> None: + """Check that the given data is what we expect to see on the output buffer.""" + length = len(data) + assert data == self._expects[:length] + self._expects = self._expects[length:] diff --git a/tests/backends/hardware/sr/v4/test_motor_board.py b/tests/backends/hardware/sr/v4/test_motor_board.py index e4aade33..a774457d 100644 --- a/tests/backends/hardware/sr/v4/test_motor_board.py +++ b/tests/backends/hardware/sr/v4/test_motor_board.py @@ -4,6 +4,7 @@ import pytest from serial import SerialException, SerialTimeoutException +from tests.backends.hardware.j5.mock_serial import MockSerial from j5.backends import CommunicationError from j5.backends.hardware.sr.v4.motor_board import ( @@ -50,102 +51,6 @@ def test_func(exception: Type[IOError]) -> None: test_func(SerialTimeoutException) -class MockSerial: - """This class mocks the behaviour of serial.Serial.""" - - def __init__(self, - port: Optional[str] = None, - baudrate: int = 9600, - bytesize: int = 8, - parity: str = 'N', - stopbits: float = 1, - timeout: Optional[float] = None, - expects: bytes = b'', - ): - self._is_open: bool = True - self._buffer: bytes = b'' - self.port = port - self._expects = expects - - assert baudrate == 1000000 - assert bytesize == 8 - assert parity == 'N' - assert stopbits == 1 - assert timeout is not None - assert 0.1 <= timeout <= 0.3 # Acceptable range of timeouts - - def close(self) -> None: - """Close the serial port.""" - assert self._is_open # Check the port is open first. - self._is_open = False - - def flush(self) -> None: - """Flush the buffer on the serial port.""" - self._buffer = b'' - - def read(self, size: int = 1) -> bytes: - """Read size bytes from the input buffer.""" - assert len(self._buffer) >= size - - data = self._buffer[:size] - self._buffer = self._buffer[size:] - return data - - def readline(self) -> bytes: - """Read up to a newline on the serial port.""" - try: - pos = self._buffer.index(b'\n') - except ValueError: - return b'' - return self.read(pos) - - def write(self, data: bytes) -> int: - """Write the data to the serial port.""" - self.check_expects(data) - - # We only end up returning data once, check for that here. - if data == b'\x01': # Version Command - self.buffer_append(b'MCV4B:3', newline=True) - - return len(data) - - # Functions for helping us mock. - - def buffer_append(self, data: bytes, newline: bool = False) -> None: - """Append some data to the receive buffer.""" - self._buffer += data - if newline: - self._buffer += b'\n' - - def expects_prepend(self, data: bytes) -> None: - """Prepend some bytes to the output buffer that we expect to see.""" - self._expects = data + self._expects - - def check_expects(self, data: bytes) -> None: - """Check that the given data is what we expect to see on the output buffer.""" - length = len(data) - assert data == self._expects[:length] - self._expects = self._expects[length:] - - -class MockSerialBadWrite(MockSerial): - """MockSerial, but never writes properly.""" - - def write(self, data: bytes) -> int: - """Don't write any data, always return 0.""" - return 0 - - -class MockSerialBadFirmware(MockSerial): - """MockSerial but with the wrong firmware version.""" - - def write(self, data: bytes) -> int: - """Write data to the serial, but with the wrong fw version.""" - if data == b'\x01': # Version Command - self.buffer_append(b'MCV4B:5', newline=True) - return len(data) - - class MockListPortInfo: """This class mocks the behaviour of serial.tools.ListPortInfo.""" @@ -202,9 +107,28 @@ def __init__(self, b'\x03\x02' # Brake Motor 1 at init b'\x02\x02' # Brake Motor 0 at del b'\x03\x02', # Brake Motor 1 at del + expected_baudrate=1000000, ) +class MotorSerialBadWrite(MotorSerial): + """MotorSerial, but never writes properly.""" + + def write(self, data: bytes) -> int: + """Don't write any data, always return 0.""" + return 0 + + +class MotorSerialBadFirmware(MotorSerial): + """MotorSerial but with the wrong firmware version.""" + + def write(self, data: bytes) -> int: + """Write data to the serial, but with the wrong fw version.""" + if data == b'\x01': # Version Command + self.buffer_append(b'MCV4B:5', newline=True) + return len(data) + + def test_backend_initialisation() -> None: """Test that we can initialise a backend.""" backend = SRV4MotorBoardHardwareBackend("COM0", serial_class=MotorSerial) @@ -219,7 +143,7 @@ def test_backend_initialisation() -> None: def test_backend_bad_firmware_version() -> None: """Test that we can detect a bad firmware version.""" with pytest.raises(CommunicationError): - SRV4MotorBoardHardwareBackend("COM0", serial_class=MockSerialBadFirmware) + SRV4MotorBoardHardwareBackend("COM0", serial_class=MotorSerialBadFirmware) def test_backend_discover() -> None: @@ -248,7 +172,7 @@ def test_backend_send_command_bad_write() -> None: """Test that an error is thrown if we can't write bytes.""" backend = SRV4MotorBoardHardwareBackend("COM0", serial_class=MotorSerial) - bad_serial_driver = MockSerialBadWrite("COM0", baudrate=1000000, timeout=0.25) + bad_serial_driver = MotorSerialBadWrite("COM0", baudrate=1000000, timeout=0.25) backend._serial = bad_serial_driver with pytest.raises(CommunicationError): backend.send_command(4) From e31ffd67c555424e5ccd5be076672067dadfa4f7 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 14:20:38 +0100 Subject: [PATCH 08/23] Refactor MockSerial to not require overriding the constructor This makes the code a bit cleaner. --- tests/backends/hardware/arduino/test_uno.py | 20 +----------- tests/backends/hardware/j5/mock_serial.py | 9 +++--- .../hardware/sr/v4/test_motor_board.py | 32 ++++++------------- 3 files changed, 16 insertions(+), 45 deletions(-) diff --git a/tests/backends/hardware/arduino/test_uno.py b/tests/backends/hardware/arduino/test_uno.py index fc4d3d93..01c76167 100644 --- a/tests/backends/hardware/arduino/test_uno.py +++ b/tests/backends/hardware/arduino/test_uno.py @@ -1,7 +1,5 @@ """Tests for the Arduino Uno hardware implementation.""" -from typing import Optional - from tests.backends.hardware.j5.mock_serial import MockSerial from j5.backends.hardware.arduino.uno import ArduinoUnoHardwareBackend @@ -10,23 +8,7 @@ class UnoSerial(MockSerial): """UnoSerial is the same as MockSerial, but includes data we expect to receive.""" - def __init__(self, - port: Optional[str] = None, - baudrate: int = 9600, - bytesize: int = 8, - parity: str = 'N', - stopbits: float = 1, - timeout: Optional[float] = None): - super().__init__( - port=port, - baudrate=baudrate, - bytesize=bytesize, - parity=parity, - stopbits=stopbits, - timeout=timeout, - expects=b"", - expected_baudrate=115200, - ) + expected_baudrate = 115200 def test_backend_initialisation() -> None: diff --git a/tests/backends/hardware/j5/mock_serial.py b/tests/backends/hardware/j5/mock_serial.py index a9fa098d..40f08957 100644 --- a/tests/backends/hardware/j5/mock_serial.py +++ b/tests/backends/hardware/j5/mock_serial.py @@ -6,6 +6,9 @@ class MockSerial: """This class mocks the behaviour of serial.Serial.""" + initial_expects = b"" + expected_baudrate = 9600 + def __init__(self, port: Optional[str] = None, baudrate: int = 9600, @@ -13,15 +16,13 @@ def __init__(self, parity: str = 'N', stopbits: float = 1, timeout: Optional[float] = None, - expects: bytes = b'', - expected_baudrate: int = 9600, ): self._is_open: bool = True self._buffer: bytes = b'' self.port = port - self._expects = expects + self._expects = self.initial_expects - assert baudrate == expected_baudrate + assert baudrate == self.expected_baudrate assert bytesize == 8 assert parity == 'N' assert stopbits == 1 diff --git a/tests/backends/hardware/sr/v4/test_motor_board.py b/tests/backends/hardware/sr/v4/test_motor_board.py index a774457d..2f3128da 100644 --- a/tests/backends/hardware/sr/v4/test_motor_board.py +++ b/tests/backends/hardware/sr/v4/test_motor_board.py @@ -1,6 +1,6 @@ """Test the SR v4 motor board hardware backend and associated classes.""" -from typing import List, Optional, Type, cast +from typing import List, Type, cast import pytest from serial import SerialException, SerialTimeoutException @@ -88,27 +88,15 @@ def mock_comports(include_links: bool = False) -> List[MockListPortInfo]: class MotorSerial(MockSerial): """MotorSerial is the same as MockSerial, but includes data we expect to receive.""" - def __init__(self, - port: Optional[str] = None, - baudrate: int = 9600, - bytesize: int = 8, - parity: str = 'N', - stopbits: float = 1, - timeout: Optional[float] = None): - super().__init__( - port=port, - baudrate=baudrate, - bytesize=bytesize, - parity=parity, - stopbits=stopbits, - timeout=timeout, - expects=b'\x01' # Version Check - b'\x02\x02' # Brake Motor 0 at init - b'\x03\x02' # Brake Motor 1 at init - b'\x02\x02' # Brake Motor 0 at del - b'\x03\x02', # Brake Motor 1 at del - expected_baudrate=1000000, - ) + initial_expects = ( + b'\x01' # Version Check + b'\x02\x02' # Brake Motor 0 at init + b'\x03\x02' # Brake Motor 1 at init + b'\x02\x02' # Brake Motor 0 at del + b'\x03\x02' # Brake Motor 1 at del + ) + + expected_baudrate = 1000000 class MotorSerialBadWrite(MotorSerial): From 9d8e147bec40ee854c184ad73da38663b6b614d5 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 15:33:15 +0100 Subject: [PATCH 09/23] Refactor MockSerial for clarity --- tests/backends/hardware/arduino/test_uno.py | 4 ++ tests/backends/hardware/j5/mock_serial.py | 43 ++++++------- .../hardware/sr/v4/test_motor_board.py | 64 ++++++++++++------- 3 files changed, 63 insertions(+), 48 deletions(-) diff --git a/tests/backends/hardware/arduino/test_uno.py b/tests/backends/hardware/arduino/test_uno.py index 01c76167..e5575f9e 100644 --- a/tests/backends/hardware/arduino/test_uno.py +++ b/tests/backends/hardware/arduino/test_uno.py @@ -9,6 +9,10 @@ class UnoSerial(MockSerial): """UnoSerial is the same as MockSerial, but includes data we expect to receive.""" expected_baudrate = 115200 + initial_received_data = ( + b"# Booted\n" + b"# SBDuino GPIO v2019.6.0\n" + ) def test_backend_initialisation() -> None: diff --git a/tests/backends/hardware/j5/mock_serial.py b/tests/backends/hardware/j5/mock_serial.py index 40f08957..3b9651d3 100644 --- a/tests/backends/hardware/j5/mock_serial.py +++ b/tests/backends/hardware/j5/mock_serial.py @@ -6,8 +6,8 @@ class MockSerial: """This class mocks the behaviour of serial.Serial.""" - initial_expects = b"" expected_baudrate = 9600 + initial_received_data = b"" def __init__(self, port: Optional[str] = None, @@ -18,9 +18,9 @@ def __init__(self, timeout: Optional[float] = None, ): self._is_open: bool = True - self._buffer: bytes = b'' + self._receive_buffer: bytes = self.initial_received_data + self._send_buffer: bytes = b"" self.port = port - self._expects = self.initial_expects assert baudrate == self.expected_baudrate assert bytesize == 8 @@ -35,49 +35,44 @@ def close(self) -> None: self._is_open = False def flush(self) -> None: - """Flush the buffer on the serial port.""" - self._buffer = b'' + """Ensure all data written to the serial port has been sent.""" + pass def read(self, size: int = 1) -> bytes: """Read size bytes from the input buffer.""" - assert len(self._buffer) >= size + assert len(self._receive_buffer) >= size - data = self._buffer[:size] - self._buffer = self._buffer[size:] + data = self._receive_buffer[:size] + self._receive_buffer = self._receive_buffer[size:] return data def readline(self) -> bytes: """Read up to a newline on the serial port.""" try: - pos = self._buffer.index(b'\n') + pos = self._receive_buffer.index(b'\n') except ValueError: return b'' - return self.read(pos) + return self.read(pos + 1) def write(self, data: bytes) -> int: """Write the data to the serial port.""" - self.check_expects(data) + self._send_buffer += data # We only end up returning data once, check for that here. if data == b'\x01': # Version Command - self.buffer_append(b'MCV4B:3', newline=True) + self.append_received_data(b'MCV4B:3', newline=True) return len(data) # Functions for helping us mock. - def buffer_append(self, data: bytes, newline: bool = False) -> None: + def append_received_data(self, data: bytes, newline: bool = False) -> None: """Append some data to the receive buffer.""" - self._buffer += data + self._receive_buffer += data if newline: - self._buffer += b'\n' + self._receive_buffer += b'\n' - def expects_prepend(self, data: bytes) -> None: - """Prepend some bytes to the output buffer that we expect to see.""" - self._expects = data + self._expects - - def check_expects(self, data: bytes) -> None: - """Check that the given data is what we expect to see on the output buffer.""" - length = len(data) - assert data == self._expects[:length] - self._expects = self._expects[length:] + def check_sent_data(self, data: bytes) -> None: + """Check that the given data is what was written to the serial port.""" + assert data == self._send_buffer, f"{data!r} != {self._send_buffer!r}" + self._send_buffer = b"" diff --git a/tests/backends/hardware/sr/v4/test_motor_board.py b/tests/backends/hardware/sr/v4/test_motor_board.py index 2f3128da..340fa735 100644 --- a/tests/backends/hardware/sr/v4/test_motor_board.py +++ b/tests/backends/hardware/sr/v4/test_motor_board.py @@ -88,16 +88,16 @@ def mock_comports(include_links: bool = False) -> List[MockListPortInfo]: class MotorSerial(MockSerial): """MotorSerial is the same as MockSerial, but includes data we expect to receive.""" - initial_expects = ( - b'\x01' # Version Check - b'\x02\x02' # Brake Motor 0 at init - b'\x03\x02' # Brake Motor 1 at init - b'\x02\x02' # Brake Motor 0 at del - b'\x03\x02' # Brake Motor 1 at del - ) - expected_baudrate = 1000000 + def check_data_sent_by_constructor(self) -> None: + """Check that the backend constructor sent expected data to the serial port.""" + self.check_sent_data( + b'\x01' # Version Check + b'\x02\x02' # Brake Motor 0 at init + b'\x03\x02', # Brake Motor 1 at init + ) + class MotorSerialBadWrite(MotorSerial): """MotorSerial, but never writes properly.""" @@ -113,7 +113,7 @@ class MotorSerialBadFirmware(MotorSerial): def write(self, data: bytes) -> int: """Write data to the serial, but with the wrong fw version.""" if data == b'\x01': # Version Command - self.buffer_append(b'MCV4B:5', newline=True) + self.append_received_data(b'MCV4B:5', newline=True) return len(data) @@ -148,12 +148,13 @@ def test_backend_send_command() -> None: """Test that the backend can send commands.""" backend = SRV4MotorBoardHardwareBackend("COM0", serial_class=MotorSerial) serial = cast(MotorSerial, backend._serial) + serial.check_data_sent_by_constructor() - serial.expects_prepend(b'\x04') backend.send_command(4) + serial.check_sent_data(b"\x04") - serial.expects_prepend(b'\x02d') backend.send_command(2, 100) + serial.check_sent_data(b'\x02d') def test_backend_send_command_bad_write() -> None: @@ -170,8 +171,8 @@ def test_read_serial_line() -> None: """Test that we can we lines from the serial interface.""" backend = SRV4MotorBoardHardwareBackend("COM0", serial_class=MotorSerial) serial = cast(MotorSerial, backend._serial) - serial.flush() - serial.buffer_append(b"Green Beans", newline=True) + serial.check_data_sent_by_constructor() + serial.append_received_data(b"Green Beans", newline=True) data = backend.read_serial_line() assert data == "Green Beans" @@ -179,7 +180,8 @@ def test_read_serial_line() -> None: def test_read_serial_line_no_data() -> None: """Check that a communication error is thrown if we get no data.""" backend = SRV4MotorBoardHardwareBackend("COM0", serial_class=MotorSerial) - backend._serial.flush() + serial = cast(MotorSerial, backend._serial) + serial.check_data_sent_by_constructor() with pytest.raises(CommunicationError): backend.read_serial_line() @@ -189,47 +191,61 @@ def test_get_firmware_version() -> None: """Test that we can get the firmware version from the serial interface.""" backend = SRV4MotorBoardHardwareBackend("COM0", serial_class=MotorSerial) serial = cast(MotorSerial, backend._serial) - serial.flush() - serial.expects_prepend(b'\x01') + serial.check_data_sent_by_constructor() assert backend.firmware_version == "3" + serial.check_sent_data(b'\x01') - serial.flush() - serial.expects_prepend(b'\x01') - serial.buffer_append(b'PBV4C:5', newline=True) + serial.append_received_data(b'PBV4C:5', newline=True) with pytest.raises(CommunicationError): backend.firmware_version + serial.check_sent_data(b'\x01') def test_get_set_motor_state() -> None: """Test that we can get and set the motor state.""" backend = SRV4MotorBoardHardwareBackend("COM0", serial_class=MotorSerial) serial = cast(MotorSerial, backend._serial) + serial.check_data_sent_by_constructor() assert backend.get_motor_state(0) == MotorSpecialState.BRAKE assert backend.get_motor_state(1) == MotorSpecialState.BRAKE - serial.expects_prepend(b'\x02\xd1') backend.set_motor_state(0, 0.65) + serial.check_sent_data(b'\x02\xd1') assert backend.get_motor_state(0) == 0.65 - serial.expects_prepend(b'\x02\xfd') backend.set_motor_state(0, 1.0) + serial.check_sent_data(b'\x02\xfd') assert backend.get_motor_state(0) == 1.0 - serial.expects_prepend(b'\x02\x03') backend.set_motor_state(0, -1.0) + serial.check_sent_data(b'\x02\x03') assert backend.get_motor_state(0) == -1.0 - serial.expects_prepend(b'\x02\x02') backend.set_motor_state(0, MotorSpecialState.BRAKE) + serial.check_sent_data(b'\x02\x02') assert backend.get_motor_state(0) == MotorSpecialState.BRAKE - serial.expects_prepend(b'\x02\x01') backend.set_motor_state(0, MotorSpecialState.COAST) + serial.check_sent_data(b'\x02\x01') assert backend.get_motor_state(0) == MotorSpecialState.COAST with pytest.raises(ValueError): backend.set_motor_state(0, 20.0) + serial.check_sent_data(b'') with pytest.raises(ValueError): backend.set_motor_state(2, 0.0) + serial.check_sent_data(b'') + + +def test_brake_motors_at_deletion() -> None: + """Test that both motors are set to BRAKE when the backend is garbage collected.""" + backend = SRV4MotorBoardHardwareBackend("COM0", serial_class=MotorSerial) + serial = cast(MotorSerial, backend._serial) + serial.check_data_sent_by_constructor() + del backend + serial.check_sent_data( + b'\x02\x02' # Brake motor 0 + b'\x03\x02', # Brake motor 1 + ) From 7c367eeec31a4af26c36cb638f9a979b185cd836 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 15:33:47 +0100 Subject: [PATCH 10/23] Improve arduino hardware backend initialisation test --- tests/backends/hardware/arduino/test_uno.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/backends/hardware/arduino/test_uno.py b/tests/backends/hardware/arduino/test_uno.py index e5575f9e..1fdb78ca 100644 --- a/tests/backends/hardware/arduino/test_uno.py +++ b/tests/backends/hardware/arduino/test_uno.py @@ -3,6 +3,7 @@ from tests.backends.hardware.j5.mock_serial import MockSerial from j5.backends.hardware.arduino.uno import ArduinoUnoHardwareBackend +from j5.components import GPIOPinMode class UnoSerial(MockSerial): @@ -17,6 +18,7 @@ class UnoSerial(MockSerial): def test_backend_initialisation() -> None: """Test that we can initialise a ArduinoUnoHardwareBackend.""" - backend = ArduinoUnoHardwareBackend("/dev/ttyUSB1", UnoSerial) + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) assert type(backend) is ArduinoUnoHardwareBackend assert type(backend._serial) is UnoSerial + assert all(mode is GPIOPinMode.DIGITAL_INPUT for mode in backend._pins.values()) From 233f52fcde9b6d49e776a272387255891ae857cb Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 15:45:42 +0100 Subject: [PATCH 11/23] Add test for version number check --- tests/backends/hardware/arduino/test_uno.py | 70 +++++++++++++++++++-- tests/backends/hardware/j5/mock_serial.py | 3 +- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/tests/backends/hardware/arduino/test_uno.py b/tests/backends/hardware/arduino/test_uno.py index 1fdb78ca..9d2b8b01 100644 --- a/tests/backends/hardware/arduino/test_uno.py +++ b/tests/backends/hardware/arduino/test_uno.py @@ -1,7 +1,11 @@ """Tests for the Arduino Uno hardware implementation.""" +from typing import Optional + +import pytest from tests.backends.hardware.j5.mock_serial import MockSerial +from j5.backends import CommunicationError from j5.backends.hardware.arduino.uno import ArduinoUnoHardwareBackend from j5.components import GPIOPinMode @@ -10,10 +14,57 @@ class UnoSerial(MockSerial): """UnoSerial is the same as MockSerial, but includes data we expect to receive.""" expected_baudrate = 115200 - initial_received_data = ( - b"# Booted\n" - b"# SBDuino GPIO v2019.6.0\n" - ) + firmware_version = "2019.6.0" + + def __init__(self, + port: Optional[str] = None, + baudrate: int = 9600, + bytesize: int = 8, + parity: str = 'N', + stopbits: float = 1, + timeout: Optional[float] = None, + ): + super().__init__( + port=port, + baudrate=baudrate, + bytesize=bytesize, + parity=parity, + stopbits=stopbits, + timeout=timeout, + ) + self.append_received_data(b"# Booted", newline=True) + version_line = b"# SBDuino GPIO v" + self.firmware_version.encode("utf-8") + self.append_received_data(version_line, newline=True) + + +class UnoSerialOldVersion1(UnoSerial): + """Like UnoSerial, but reports an older version number.""" + + firmware_version = "2018.7.0" + + +class UnoSerialOldVersion2(UnoSerial): + """Like UnoSerial, but reports an older version number.""" + + firmware_version = "2019.5.0" + + +class UnoSerialNewVersion1(UnoSerial): + """Like UnoSerial, but reports an newer version number.""" + + firmware_version = "2019.6.1" + + +class UnoSerialNewVersion2(UnoSerial): + """Like UnoSerial, but reports an newer version number.""" + + firmware_version = "2019.7.0" + + +class UnoSerialNewVersion3(UnoSerial): + """Like UnoSerial, but reports an newer version number.""" + + firmware_version = "2020.1.0" def test_backend_initialisation() -> None: @@ -22,3 +73,14 @@ def test_backend_initialisation() -> None: assert type(backend) is ArduinoUnoHardwareBackend assert type(backend._serial) is UnoSerial assert all(mode is GPIOPinMode.DIGITAL_INPUT for mode in backend._pins.values()) + + +def test_backend_version_check() -> None: + """Test that an exception is raised if the arduino reports an unsupported version.""" + with pytest.raises(CommunicationError): + ArduinoUnoHardwareBackend("COM0", UnoSerialOldVersion1) + with pytest.raises(CommunicationError): + ArduinoUnoHardwareBackend("COM0", UnoSerialOldVersion2) + ArduinoUnoHardwareBackend("COM0", UnoSerialNewVersion1) + ArduinoUnoHardwareBackend("COM0", UnoSerialNewVersion2) + ArduinoUnoHardwareBackend("COM0", UnoSerialNewVersion3) diff --git a/tests/backends/hardware/j5/mock_serial.py b/tests/backends/hardware/j5/mock_serial.py index 3b9651d3..a20ecdbe 100644 --- a/tests/backends/hardware/j5/mock_serial.py +++ b/tests/backends/hardware/j5/mock_serial.py @@ -7,7 +7,6 @@ class MockSerial: """This class mocks the behaviour of serial.Serial.""" expected_baudrate = 9600 - initial_received_data = b"" def __init__(self, port: Optional[str] = None, @@ -18,7 +17,7 @@ def __init__(self, timeout: Optional[float] = None, ): self._is_open: bool = True - self._receive_buffer: bytes = self.initial_received_data + self._receive_buffer: bytes = b"" self._send_buffer: bytes = b"" self.port = port From 6ef1492d56243a03923abaf3f5dc20c88e075c2b Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 15:46:01 +0100 Subject: [PATCH 12/23] Fix version number check Previously it would reject "2020.1.0" since the month component is less than 6 despite the year component being greater than 2019. --- j5/backends/hardware/arduino/uno.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/j5/backends/hardware/arduino/uno.py b/j5/backends/hardware/arduino/uno.py index 0ffea912..38c593fe 100644 --- a/j5/backends/hardware/arduino/uno.py +++ b/j5/backends/hardware/arduino/uno.py @@ -93,11 +93,11 @@ def __init__(self, serial_port: str, serial_class: Type[Serial] = Serial) -> Non self._version_line = self.read_serial_line() if self.firmware_version is not None: - version_ids = self.firmware_version.split(".") + version_ids = tuple(map(int, self.firmware_version.split("."))) else: - version_ids = ["0", "0", "0"] + version_ids = (0, 0, 0) - if int(version_ids[0]) < 2019 or int(version_ids[1]) < 6: + if version_ids < (2019, 6, 0): raise CommunicationError( f"Unexpected firmware version: {self.firmware_version},", f" expected at least: \"2019.6.0\".", From b7b36135830d00bf0486451eef8773b884fe6d12 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 15:51:10 +0100 Subject: [PATCH 13/23] Use a timedelta for timeout in SerialHardwardBackend constructor --- j5/backends/hardware/j5/serial.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/j5/backends/hardware/j5/serial.py b/j5/backends/hardware/j5/serial.py index b4a4f6a6..4ef7fd12 100644 --- a/j5/backends/hardware/j5/serial.py +++ b/j5/backends/hardware/j5/serial.py @@ -1,5 +1,6 @@ """Abstract hardware backend implementation provided by j5 for serial comms.""" from abc import abstractmethod +from datetime import timedelta from functools import wraps from typing import TYPE_CHECKING, Callable, Optional, Set, Type, TypeVar @@ -80,12 +81,13 @@ def __init__( serial_port: str, serial_class: Type[Seriallike] = Serial, baud: int = 115200, - timeout: float = 0.25, + timeout: timedelta = timedelta(milliseconds=250), ) -> None: + timeout_secs = timeout / timedelta(seconds=1) self._serial = serial_class( port=serial_port, baudrate=baud, - timeout=timeout, + timeout=timeout_secs, ) @classmethod From 2f2e805eda00bbcf7957c9f48f16fea8547e36e7 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 15:53:35 +0100 Subject: [PATCH 14/23] Add isort command to makefile This allows you to run 'make isort' to fix imports in all python source files. --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 9e5d23db..5982d9e5 100644 --- a/Makefile +++ b/Makefile @@ -17,5 +17,8 @@ test: test-cov: $(CMD) pytest --cov=$(PYMODULE) tests --cov-report html +isort: + $(CMD) isort --recursive $(PYMODULE) tests tests_hw + clean: git clean -Xdf # Delete all files in .gitignore From 79d17b374c11714dd42ec456fff08ce525299544 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 15:55:13 +0100 Subject: [PATCH 15/23] Change order of tasks in Makefile This changes the order in which linting, testing and typechecking is done when the 'make' command is executed. Reasoning is that one usually wants to see and fix functional or type errors before dealing with linting errors. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5982d9e5..dcb485c7 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CMD:=poetry run PYMODULE:=j5 -all: lint type test +all: type test lint lint: $(CMD) flake8 $(PYMODULE) tests tests_hw From 31f5d50ab91030a844601ec93cfd77b9c4cebd09 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 19:11:26 +0100 Subject: [PATCH 16/23] Update error message --- j5/backends/hardware/j5/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/j5/backends/hardware/j5/serial.py b/j5/backends/hardware/j5/serial.py index 4ef7fd12..4dd349bc 100644 --- a/j5/backends/hardware/j5/serial.py +++ b/j5/backends/hardware/j5/serial.py @@ -117,7 +117,7 @@ def read_serial_line(self, empty: bool = False) -> str: if empty: return "" raise CommunicationError( - "Unable to communicate with board. ", + "No response from board. " "Is it correctly powered?", ) From 47bf90d854a69865138e7a4c8c75a6b69954f08e Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 19:08:08 +0100 Subject: [PATCH 17/23] Implement all the things --- j5/backends/hardware/arduino/uno.py | 158 +++++++++-- tests/backends/hardware/arduino/test_uno.py | 252 +++++++++++++++++- tests/backends/hardware/j5/mock_serial.py | 15 +- .../hardware/sr/v4/test_motor_board.py | 6 + 4 files changed, 409 insertions(+), 22 deletions(-) diff --git a/j5/backends/hardware/arduino/uno.py b/j5/backends/hardware/arduino/uno.py index 38c593fe..2e917a38 100644 --- a/j5/backends/hardware/arduino/uno.py +++ b/j5/backends/hardware/arduino/uno.py @@ -1,13 +1,16 @@ """Arduino Uno Hardware Implementation.""" -from typing import Callable, List, Optional, Set, Tuple, Type +from typing import Callable, List, Mapping, Optional, Set, Tuple, Type from serial import Serial from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo from j5.backends import CommunicationError -from j5.backends.hardware.env import HardwareEnvironment +from j5.backends.hardware.env import ( + HardwareEnvironment, + NotSupportedByHardwareError, +) from j5.backends.hardware.j5.serial import ( SerialHardwareBackend, handle_serial_error, @@ -22,12 +25,25 @@ (0x1a86, 0x7523), # Real Uno } +FIRST_ANALOGUE_PIN = 14 + def is_arduino_uno(port: ListPortInfo) -> bool: """Check if a ListPortInfo represents an Arduino Uno.""" return (port.vid, port.pid) in USB_IDS +class DigitalPinData: + """Contains data about a digital pin.""" + + mode: GPIOPinMode + state: bool + + def __init__(self, *, mode: GPIOPinMode, state: bool): + self.mode = mode + self.state = state + + class ArduinoUnoHardwareBackend( LEDInterface, GPIOPinInterface, @@ -72,11 +88,9 @@ def __init__(self, serial_port: str, serial_class: Type[Serial] = Serial) -> Non baud=115200, ) - self._pins = { - i: GPIOPinMode.DIGITAL_INPUT - for i in range(2, 20) - # Digital 2 - 13 - # Analogue 14 - 19 + self._digital_pins: Mapping[int, DigitalPinData] = { + i: DigitalPinData(mode=GPIOPinMode.DIGITAL_INPUT, state=False) + for i in range(2, FIRST_ANALOGUE_PIN) } count = 0 @@ -103,36 +117,145 @@ def __init__(self, serial_port: str, serial_class: Type[Serial] = Serial) -> Non f" expected at least: \"2019.6.0\".", ) + for pin_number in self._digital_pins.keys(): + self.set_gpio_pin_mode(pin_number, GPIOPinMode.DIGITAL_INPUT) + @property def firmware_version(self) -> Optional[str]: """The firmware version of the board.""" return self._version_line.split("v")[1] - def _command(self, command: str, params: List[str]) -> str: + @handle_serial_error + def _command(self, command: str, *params: str) -> List[str]: """Send a command to the board.""" + message = " ".join([command] + list(params)) + "\n" + self._serial.write(message.encode("utf-8")) + + results: List[str] = [] + while True: + line = self.read_serial_line(empty=False) + code, param = line.split(None, 1) + if code == "+": + return results + elif code == "-": + raise CommunicationError(f"Arduino error: {param}") + elif code == ">": + results.append(param) + elif code == "#": + pass # Ignore comment lines + else: + raise CommunicationError( + f"Arduino returned unrecognised response line: {line}", + ) + + def _update_digital_pin(self, identifier: int) -> None: + assert identifier < FIRST_ANALOGUE_PIN + pin = self._digital_pins[identifier] + char: str + if pin.mode == GPIOPinMode.DIGITAL_INPUT: + char = "Z" + elif pin.mode == GPIOPinMode.DIGITAL_INPUT_PULLUP: + char = "P" + elif pin.mode == GPIOPinMode.DIGITAL_OUTPUT: + if pin.state: + char = "H" + else: + char = "L" + else: + assert False, "unreachable" + self._command("W", str(identifier), char) def set_gpio_pin_mode(self, identifier: int, pin_mode: GPIOPinMode) -> None: """Set the hardware mode of a GPIO pin.""" - self._pins[identifier] = pin_mode + digital_pin_modes = ( + GPIOPinMode.DIGITAL_INPUT, + GPIOPinMode.DIGITAL_INPUT_PULLUP, + GPIOPinMode.DIGITAL_OUTPUT, + ) + if identifier < FIRST_ANALOGUE_PIN: + # Digital pin + if pin_mode in digital_pin_modes: + self._digital_pins[identifier].mode = pin_mode + self._update_digital_pin(identifier) + return + else: + # Analogue pin + if pin_mode is GPIOPinMode.ANALOGUE_INPUT: + return + raise NotSupportedByHardwareError( + f"Arduino Uno does not support mode {pin_mode} on pin {identifier}", + ) def get_gpio_pin_mode(self, identifier: int) -> GPIOPinMode: """Get the hardware mode of a GPIO pin.""" - return self._pins[identifier] + if identifier < FIRST_ANALOGUE_PIN: + return self._digital_pins[identifier].mode + else: + return GPIOPinMode.ANALOGUE_INPUT def write_gpio_pin_digital_state(self, identifier: int, state: bool) -> None: """Write to the digital state of a GPIO pin.""" + if identifier >= FIRST_ANALOGUE_PIN: + raise NotSupportedByHardwareError( + "Digital functions not supported on analogue pins", + ) + if self._digital_pins[identifier].mode is not GPIOPinMode.DIGITAL_OUTPUT: + raise ValueError(f"Pin {identifier} mode needs to be DIGITAL_OUTPUT" + f"in order to set the digital state.") + self._digital_pins[identifier].state = state + self._update_digital_pin(identifier) def get_gpio_pin_digital_state(self, identifier: int) -> bool: """Get the last written state of the GPIO pin.""" - return False + if identifier >= FIRST_ANALOGUE_PIN: + raise NotSupportedByHardwareError( + "Digital functions not supported on analogue pins", + ) + if self._digital_pins[identifier].mode is not GPIOPinMode.DIGITAL_OUTPUT: + raise ValueError(f"Pin {identifier} mode needs to be DIGITAL_OUTPUT" + f"in order to read the digital state.") + return self._digital_pins[identifier].state def read_gpio_pin_digital_state(self, identifier: int) -> bool: """Read the digital state of the GPIO pin.""" - return False + if identifier >= FIRST_ANALOGUE_PIN: + raise NotSupportedByHardwareError( + "Digital functions not supported on analogue pins", + ) + if self._digital_pins[identifier].mode not in ( + GPIOPinMode.DIGITAL_INPUT, + GPIOPinMode.DIGITAL_INPUT_PULLUP, + ): + raise ValueError(f"Pin {identifier} mode needs to be DIGITAL_INPUT_*" + f"in order to read the digital state.") + results = self._command("R", str(identifier)) + if len(results) != 1: + raise CommunicationError(f"Invalid response from Arduino: {results}") + result = results[0] + if result == "H": + return True + elif result == "L": + return False + else: + raise CommunicationError(f"Invalid response from Arduino: {result}") def read_gpio_pin_analogue_value(self, identifier: int) -> float: - """Read the scaled analogue value of the GPIO pin.""" - return 0.0 + """Read the analogue voltage of the GPIO pin.""" + if identifier < FIRST_ANALOGUE_PIN: + raise NotSupportedByHardwareError( + "Analogue functions not supported on digital pins", + ) + if identifier >= FIRST_ANALOGUE_PIN + 4: + raise NotSupportedByHardwareError( + f"Arduino Uno firmware only supports analogue pins 0-3 (IDs 14-17)", + ) + analogue_pin_num = identifier - 14 + results = self._command("A") + if len(results) != 4: + raise CommunicationError(f"Invalid response from Arduino: {results}") + reading = int(results[analogue_pin_num]) + voltage = (reading / 1024.0) * 5.0 + return voltage def write_gpio_pin_dac_value(self, identifier: int, scaled_value: float) -> None: """Write a scaled analogue value to the DAC on the GPIO pin.""" @@ -146,7 +269,12 @@ def write_gpio_pin_pwm_value(self, identifier: int, duty_cycle: float) -> None: def get_led_state(self, identifier: int) -> bool: """Get the state of an LED.""" - return False + if identifier != 0: + raise ValueError("Arduino Uno only has LED 0 (digital pin 13).") + return self.get_gpio_pin_digital_state(13) def set_led_state(self, identifier: int, state: bool) -> None: """Set the state of an LED.""" + if identifier != 0: + raise ValueError("Arduino Uno only has LED 0 (digital pin 13)") + self.write_gpio_pin_digital_state(13, state) diff --git a/tests/backends/hardware/arduino/test_uno.py b/tests/backends/hardware/arduino/test_uno.py index 9d2b8b01..1e7276d0 100644 --- a/tests/backends/hardware/arduino/test_uno.py +++ b/tests/backends/hardware/arduino/test_uno.py @@ -1,12 +1,14 @@ """Tests for the Arduino Uno hardware implementation.""" -from typing import Optional +from math import isclose +from typing import Optional, cast import pytest from tests.backends.hardware.j5.mock_serial import MockSerial from j5.backends import CommunicationError from j5.backends.hardware.arduino.uno import ArduinoUnoHardwareBackend +from j5.backends.hardware.env import NotSupportedByHardwareError from j5.components import GPIOPinMode @@ -36,6 +38,15 @@ def __init__(self, version_line = b"# SBDuino GPIO v" + self.firmware_version.encode("utf-8") self.append_received_data(version_line, newline=True) + def respond_to_write(self, data: bytes) -> None: + """Hook that can be overriden by subclasses to respond to sent data.""" + self.append_received_data(b"+ OK", newline=True) + + def check_data_sent_by_constructor(self) -> None: + """Check that the backend constructor sent expected data to the serial port.""" + data = "".join(f"W {i} Z\n" for i in range(2, 14)) + self.check_sent_data(data.encode("utf-8")) + class UnoSerialOldVersion1(UnoSerial): """Like UnoSerial, but reports an older version number.""" @@ -67,12 +78,31 @@ class UnoSerialNewVersion3(UnoSerial): firmware_version = "2020.1.0" +class UnoSerialFailureResponse(UnoSerial): + """Like UnoSerial, but returns a failure response rather than success.""" + + def respond_to_write(self, data: bytes) -> None: + """Hook that can be overriden by subclasses to respond to sent data.""" + self.append_received_data(b"- Something went wrong", newline=True) + + def test_backend_initialisation() -> None: """Test that we can initialise a ArduinoUnoHardwareBackend.""" backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) assert type(backend) is ArduinoUnoHardwareBackend assert type(backend._serial) is UnoSerial - assert all(mode is GPIOPinMode.DIGITAL_INPUT for mode in backend._pins.values()) + assert all( + pin.mode is GPIOPinMode.DIGITAL_INPUT for pin in backend._digital_pins.values() + ) + assert all(pin.state is False for pin in backend._digital_pins.values()) + + +def test_backend_initialisation_serial() -> None: + """Test commands/responses are sent/received during initialisation.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + serial = cast(UnoSerial, backend._serial) + serial.check_data_sent_by_constructor() + serial.check_all_received_data_consumed() def test_backend_version_check() -> None: @@ -84,3 +114,221 @@ def test_backend_version_check() -> None: ArduinoUnoHardwareBackend("COM0", UnoSerialNewVersion1) ArduinoUnoHardwareBackend("COM0", UnoSerialNewVersion2) ArduinoUnoHardwareBackend("COM0", UnoSerialNewVersion3) + + +def test_backend_firmware_version() -> None: + """Test that the firmware version is parsed correctly.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + assert backend.firmware_version == UnoSerial.firmware_version + + +def test_backend_handles_failure() -> None: + """Test that an exception is raised when a failure response is received.""" + with pytest.raises(CommunicationError): + ArduinoUnoHardwareBackend("COM0", UnoSerialFailureResponse) + + +def test_backend_get_set_pin_mode() -> None: + """Test that we can get and set pin modes.""" + pin = 2 + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + assert backend.get_gpio_pin_mode(pin) is GPIOPinMode.DIGITAL_INPUT + backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_OUTPUT) + assert backend.get_gpio_pin_mode(pin) is GPIOPinMode.DIGITAL_OUTPUT + + +def test_backend_digital_pin_modes() -> None: + """Test that only certain modes are valid on digital pins.""" + pin = 2 + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_INPUT) + backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_INPUT_PULLUP) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_INPUT_PULLDOWN) + backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_OUTPUT) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.ANALOGUE_INPUT) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.ANALOGUE_OUTPUT) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.PWM_OUTPUT) + + +def test_backend_analogue_pin_modes() -> None: + """Test that only certain modes are valid on digital pins.""" + pin = 14 + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_INPUT) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_INPUT_PULLUP) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_INPUT_PULLDOWN) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_OUTPUT) + backend.set_gpio_pin_mode(pin, GPIOPinMode.ANALOGUE_INPUT) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.ANALOGUE_OUTPUT) + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(pin, GPIOPinMode.PWM_OUTPUT) + + +def test_backend_write_digital_state() -> None: + """Test that we can write the digital state of a pin.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + serial = cast(UnoSerial, backend._serial) + serial.check_data_sent_by_constructor() + # This should put the pin into the most recent (or default) output state. + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) + serial.check_sent_data(b"W 2 L\n") + backend.write_gpio_pin_digital_state(2, True) + serial.check_sent_data(b"W 2 H\n") + backend.write_gpio_pin_digital_state(2, False) + serial.check_sent_data(b"W 2 L\n") + serial.check_all_received_data_consumed() + + +def test_backend_write_digital_state_requires_pin_mode() -> None: + """Check that pin must be in DIGITAL_OUTPUT mode for write digital state to work.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + assert backend.get_gpio_pin_mode(2) is not GPIOPinMode.DIGITAL_OUTPUT + with pytest.raises(ValueError): + backend.write_gpio_pin_digital_state(2, True) + + +def test_backend_write_digital_state_requires_digital_pin() -> None: + """Check that pins 14-19 are not supported by write digital state.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + with pytest.raises(NotSupportedByHardwareError): + backend.write_gpio_pin_digital_state(14, True) + + +def test_backend_digital_state_persists() -> None: + """Test switching to a different mode and then back to output.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + serial = cast(UnoSerial, backend._serial) + serial.check_data_sent_by_constructor() + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) + serial.check_sent_data(b"W 2 L\n") + backend.write_gpio_pin_digital_state(2, True) + serial.check_sent_data(b"W 2 H\n") + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_INPUT) + serial.check_sent_data(b"W 2 Z\n") + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) + serial.check_sent_data(b"W 2 H\n") + backend.write_gpio_pin_digital_state(2, False) + serial.check_sent_data(b"W 2 L\n") + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_INPUT) + serial.check_sent_data(b"W 2 Z\n") + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) + serial.check_sent_data(b"W 2 L\n") + serial.check_all_received_data_consumed() + + +def test_backend_get_digital_state() -> None: + """Test that we can read back the digital state of a pin.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + # This should put the pin into the most recent (or default) output state. + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) + assert backend.get_gpio_pin_digital_state(2) is False + backend.write_gpio_pin_digital_state(2, True) + assert backend.get_gpio_pin_digital_state(2) is True + backend.write_gpio_pin_digital_state(2, False) + assert backend.get_gpio_pin_digital_state(2) is False + + +def test_backend_get_digital_state_requires_pin_mode() -> None: + """Check that pin must be in DIGITAL_OUTPUT mode for get digital state to work.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + assert backend.get_gpio_pin_mode(2) is not GPIOPinMode.DIGITAL_OUTPUT + with pytest.raises(ValueError): + backend.get_gpio_pin_digital_state(2) + + +def test_backend_get_digital_state_requires_digital_pin() -> None: + """Check that pins 14-19 are not supported by get digital state.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + with pytest.raises(NotSupportedByHardwareError): + backend.get_gpio_pin_digital_state(14) + + +def test_backend_input_modes() -> None: + """Check that the correct commands are send when setting pins to input modes.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + serial = cast(UnoSerial, backend._serial) + serial.check_data_sent_by_constructor() + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_INPUT) + serial.check_sent_data(b"W 2 Z\n") + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_INPUT_PULLUP) + serial.check_sent_data(b"W 2 P\n") + with pytest.raises(NotSupportedByHardwareError): + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_INPUT_PULLDOWN) + serial.check_all_received_data_consumed() + + +def test_backend_read_digital_state() -> None: + """Test that we can read the digital state of a pin.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + serial = cast(UnoSerial, backend._serial) + serial.check_data_sent_by_constructor() + + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_INPUT) + serial.check_sent_data(b"W 2 Z\n") + + serial.append_received_data(b"> H", newline=True) + assert backend.read_gpio_pin_digital_state(2) is True + serial.check_sent_data(b"R 2\n") + + serial.append_received_data(b"> L", newline=True) + assert backend.read_gpio_pin_digital_state(2) is False + serial.check_sent_data(b"R 2\n") + + serial.append_received_data(b"> X", newline=True) # invalid + with pytest.raises(CommunicationError): + backend.read_gpio_pin_digital_state(2) + serial.check_sent_data(b"R 2\n") + + serial.check_all_received_data_consumed() + + +def test_backend_read_digital_state_requires_pin_mode() -> None: + """Check that pin must be in DIGITAL_INPUT* mode for read digital state to work.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) + assert backend.get_gpio_pin_mode(2) is not GPIOPinMode.DIGITAL_INPUT + with pytest.raises(ValueError): + backend.read_gpio_pin_digital_state(2) + + +def test_backend_read_digital_state_requires_digital_pin() -> None: + """Check that pins 14-19 are not supported by read digital state.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + with pytest.raises(NotSupportedByHardwareError): + backend.read_gpio_pin_digital_state(14) + + +def test_backend_read_analogue() -> None: + """Test that we can read the digital state of a pin.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + serial = cast(UnoSerial, backend._serial) + serial.check_data_sent_by_constructor() + + readings = [212, 535, 662, 385] + for i, expected_reading in enumerate(readings): + # "read analogue" command reads all four pins at once. + identifier = 14 + i + for reading in readings: + serial.append_received_data(f"> {reading}".encode("utf-8"), newline=True) + expected_voltage = (expected_reading / 1024.0) * 5.0 + measured_voltage = backend.read_gpio_pin_analogue_value(identifier) + assert isclose(measured_voltage, expected_voltage) + serial.check_sent_data(b"A\n") + + serial.check_all_received_data_consumed() + + +def test_backend_read_analogue_requires_analogue_pin() -> None: + """Check that pins 2-13 are not supported by read analogue.""" + backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + with pytest.raises(NotSupportedByHardwareError): + backend.read_gpio_pin_analogue_value(13) diff --git a/tests/backends/hardware/j5/mock_serial.py b/tests/backends/hardware/j5/mock_serial.py index a20ecdbe..c3d46aac 100644 --- a/tests/backends/hardware/j5/mock_serial.py +++ b/tests/backends/hardware/j5/mock_serial.py @@ -56,15 +56,15 @@ def readline(self) -> bytes: def write(self, data: bytes) -> int: """Write the data to the serial port.""" self._send_buffer += data - - # We only end up returning data once, check for that here. - if data == b'\x01': # Version Command - self.append_received_data(b'MCV4B:3', newline=True) - + self.respond_to_write(data) return len(data) # Functions for helping us mock. + def respond_to_write(self, data: bytes) -> None: + """Hook that can be overriden by subclasses to respond to sent data.""" + pass + def append_received_data(self, data: bytes, newline: bool = False) -> None: """Append some data to the receive buffer.""" self._receive_buffer += data @@ -75,3 +75,8 @@ def check_sent_data(self, data: bytes) -> None: """Check that the given data is what was written to the serial port.""" assert data == self._send_buffer, f"{data!r} != {self._send_buffer!r}" self._send_buffer = b"" + + def check_all_received_data_consumed(self) -> None: + """Check all data queued by append_received_data was consumed by backend.""" + assert self._receive_buffer == b"", \ + "Backend didn't consume all expected incoming data" diff --git a/tests/backends/hardware/sr/v4/test_motor_board.py b/tests/backends/hardware/sr/v4/test_motor_board.py index 340fa735..0e500bf7 100644 --- a/tests/backends/hardware/sr/v4/test_motor_board.py +++ b/tests/backends/hardware/sr/v4/test_motor_board.py @@ -90,6 +90,12 @@ class MotorSerial(MockSerial): expected_baudrate = 1000000 + def respond_to_write(self, data: bytes) -> None: + """Hook that can be overriden by subclasses to respond to sent data.""" + # We only end up returning data once, check for that here. + if data == b'\x01': # Version Command + self.append_received_data(b'MCV4B:3', newline=True) + def check_data_sent_by_constructor(self) -> None: """Check that the backend constructor sent expected data to the serial port.""" self.check_sent_data( From cbe07db203b536b80017be6c66582bd67d823803 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 20:05:06 +0100 Subject: [PATCH 18/23] Big boi rename --- j5/backends/console/arduino/__init__.py | 1 - j5/backends/console/sb/__init__.py | 1 + .../console/{arduino/uno.py => sb/arduino.py} | 12 +-- j5/backends/hardware/arduino/__init__.py | 1 - j5/backends/hardware/sb/__init__.py | 1 + .../{arduino/uno.py => sb/arduino.py} | 14 +-- j5/boards/arduino/__init__.py | 8 -- j5/boards/sb/__init__.py | 8 ++ j5/boards/{arduino/uno.py => sb/arduino.py} | 6 +- tests/backends/console/arduino/__init__.py | 1 - tests/backends/console/sb/__init__.py | 1 + .../test_uno.py => sb/test_arduino.py} | 44 ++++---- tests/backends/hardware/arduino/__init__.py | 1 - tests/backends/hardware/sb/__init__.py | 1 + .../test_uno.py => sb/test_arduino.py} | 102 +++++++++--------- tests/boards/arduino/__init__.py | 1 - tests/boards/sb/__init__.py | 1 + .../test_uno.py => sb/test_arduino.py} | 22 ++-- 18 files changed, 113 insertions(+), 113 deletions(-) delete mode 100644 j5/backends/console/arduino/__init__.py create mode 100644 j5/backends/console/sb/__init__.py rename j5/backends/console/{arduino/uno.py => sb/arduino.py} (93%) delete mode 100644 j5/backends/hardware/arduino/__init__.py create mode 100644 j5/backends/hardware/sb/__init__.py rename j5/backends/hardware/{arduino/uno.py => sb/arduino.py} (97%) delete mode 100644 j5/boards/arduino/__init__.py create mode 100644 j5/boards/sb/__init__.py rename j5/boards/{arduino/uno.py => sb/arduino.py} (96%) delete mode 100644 tests/backends/console/arduino/__init__.py create mode 100644 tests/backends/console/sb/__init__.py rename tests/backends/console/{arduino/test_uno.py => sb/test_arduino.py} (86%) delete mode 100644 tests/backends/hardware/arduino/__init__.py create mode 100644 tests/backends/hardware/sb/__init__.py rename tests/backends/hardware/{arduino/test_uno.py => sb/test_arduino.py} (77%) delete mode 100644 tests/boards/arduino/__init__.py create mode 100644 tests/boards/sb/__init__.py rename tests/boards/{arduino/test_uno.py => sb/test_arduino.py} (83%) diff --git a/j5/backends/console/arduino/__init__.py b/j5/backends/console/arduino/__init__.py deleted file mode 100644 index 0e2b4b38..00000000 --- a/j5/backends/console/arduino/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Backends for Arduino Boards in the Console Environment.""" diff --git a/j5/backends/console/sb/__init__.py b/j5/backends/console/sb/__init__.py new file mode 100644 index 00000000..d203d5fc --- /dev/null +++ b/j5/backends/console/sb/__init__.py @@ -0,0 +1 @@ +"""Backends for SourceBots boards in the Console Environment.""" diff --git a/j5/backends/console/arduino/uno.py b/j5/backends/console/sb/arduino.py similarity index 93% rename from j5/backends/console/arduino/uno.py rename to j5/backends/console/sb/arduino.py index 73d91b1c..31720d66 100644 --- a/j5/backends/console/arduino/uno.py +++ b/j5/backends/console/sb/arduino.py @@ -1,10 +1,10 @@ -"""Console Backend for the Arduino Uno.""" +"""Console Backend for the SourceBots Arduino.""" from typing import Mapping, Optional, Set, Type from j5.backends import Backend from j5.backends.console import Console, ConsoleEnvironment from j5.boards import Board -from j5.boards.arduino import ArduinoUnoBoard +from j5.boards.sb import SBArduinoBoard from j5.components import GPIOPinInterface, GPIOPinMode, LEDInterface @@ -19,11 +19,11 @@ def __init__(self, *, mode: GPIOPinMode, digital_state: bool): self.digital_state = digital_state -class ArduinoUnoConsoleBackend(GPIOPinInterface, LEDInterface, Backend): - """Console Backend for the Arduino Uno.""" +class SBArduinoConsoleBackend(GPIOPinInterface, LEDInterface, Backend): + """Console Backend for the SourceBots Arduino.""" environment = ConsoleEnvironment - board = ArduinoUnoBoard + board = SBArduinoBoard @classmethod def discover(cls) -> Set[Board]: @@ -97,7 +97,7 @@ def write_gpio_pin_dac_value(self, identifier: int, scaled_value: float) -> None def write_gpio_pin_pwm_value(self, identifier: int, duty_cycle: float) -> None: """Write a scaled analogue value to the PWM on the GPIO pin.""" - # Not implemented on ArduinoUnoBoard yet. + # Not implemented on SBArduinoBoard yet. raise NotImplementedError def get_led_state(self, identifier: int) -> bool: diff --git a/j5/backends/hardware/arduino/__init__.py b/j5/backends/hardware/arduino/__init__.py deleted file mode 100644 index aa5a8e61..00000000 --- a/j5/backends/hardware/arduino/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Hardware backends for Arduino Boards.""" diff --git a/j5/backends/hardware/sb/__init__.py b/j5/backends/hardware/sb/__init__.py new file mode 100644 index 00000000..b443962f --- /dev/null +++ b/j5/backends/hardware/sb/__init__.py @@ -0,0 +1 @@ +"""Backends for SourceBots boards in the Hardware Environment.""" diff --git a/j5/backends/hardware/arduino/uno.py b/j5/backends/hardware/sb/arduino.py similarity index 97% rename from j5/backends/hardware/arduino/uno.py rename to j5/backends/hardware/sb/arduino.py index 2e917a38..90b551a5 100644 --- a/j5/backends/hardware/arduino/uno.py +++ b/j5/backends/hardware/sb/arduino.py @@ -1,4 +1,4 @@ -"""Arduino Uno Hardware Implementation.""" +"""SourceBots Arduino Hardware Implementation.""" from typing import Callable, List, Mapping, Optional, Set, Tuple, Type @@ -16,7 +16,7 @@ handle_serial_error, ) from j5.boards import Board -from j5.boards.arduino.uno import ArduinoUnoBoard +from j5.boards.sb.arduino import SBArduinoBoard from j5.components import GPIOPinInterface, GPIOPinMode, LEDInterface USB_IDS: Set[Tuple[int, int]] = { @@ -44,7 +44,7 @@ def __init__(self, *, mode: GPIOPinMode, state: bool): self.state = state -class ArduinoUnoHardwareBackend( +class SBArduinoHardwareBackend( LEDInterface, GPIOPinInterface, SerialHardwareBackend, @@ -56,7 +56,7 @@ class ArduinoUnoHardwareBackend( """ environment = HardwareEnvironment - board = ArduinoUnoBoard + board = SBArduinoBoard @classmethod def discover( @@ -72,7 +72,7 @@ def discover( boards: Set[Board] = set() for port in filter(is_arduino_uno, ports): boards.add( - ArduinoUnoBoard( + SBArduinoBoard( port.serial_number, cls(port.device, serial_class), ), @@ -82,7 +82,7 @@ def discover( @handle_serial_error def __init__(self, serial_port: str, serial_class: Type[Serial] = Serial) -> None: - super(ArduinoUnoHardwareBackend, self).__init__( + super(SBArduinoHardwareBackend, self).__init__( serial_port=serial_port, serial_class=serial_class, baud=115200, @@ -264,7 +264,7 @@ def write_gpio_pin_dac_value(self, identifier: int, scaled_value: float) -> None def write_gpio_pin_pwm_value(self, identifier: int, duty_cycle: float) -> None: """Write a scaled analogue value to the PWM on the GPIO pin.""" - # Not implemented on ArduinoUnoBoard yet. + # Not implemented on SBArduinoBoard yet. raise NotImplementedError def get_led_state(self, identifier: int) -> bool: diff --git a/j5/boards/arduino/__init__.py b/j5/boards/arduino/__init__.py deleted file mode 100644 index 5d4470be..00000000 --- a/j5/boards/arduino/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Arduino Boards.""" - -from .uno import AnaloguePin, ArduinoUnoBoard - -__all__ = [ - 'ArduinoUnoBoard', - 'AnaloguePin', -] diff --git a/j5/boards/sb/__init__.py b/j5/boards/sb/__init__.py new file mode 100644 index 00000000..5867dbaa --- /dev/null +++ b/j5/boards/sb/__init__.py @@ -0,0 +1,8 @@ +"""SourceBots Boards.""" + +from .arduino import AnaloguePin, SBArduinoBoard + +__all__ = [ + 'AnaloguePin', + 'SBArduinoBoard', +] diff --git a/j5/boards/arduino/uno.py b/j5/boards/sb/arduino.py similarity index 96% rename from j5/boards/arduino/uno.py rename to j5/boards/sb/arduino.py index 87ec8512..d5df1db3 100644 --- a/j5/boards/arduino/uno.py +++ b/j5/boards/sb/arduino.py @@ -1,4 +1,4 @@ -"""Classes for the Arduino Uno.""" +"""Classes for the SourceBots Arduino.""" from enum import IntEnum from typing import Mapping, Optional, Set, Type, Union, cast @@ -28,8 +28,8 @@ class AnaloguePin(IntEnum): PinNumber = Union[int, AnaloguePin] -class ArduinoUnoBoard(Board): - """Arduino Uno Board.""" +class SBArduinoBoard(Board): + """SourceBots Arduino Board.""" _led: LED _digital_pins: Mapping[int, GPIOPin] diff --git a/tests/backends/console/arduino/__init__.py b/tests/backends/console/arduino/__init__.py deleted file mode 100644 index 619a7fd4..00000000 --- a/tests/backends/console/arduino/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for Arduino Console backends.""" diff --git a/tests/backends/console/sb/__init__.py b/tests/backends/console/sb/__init__.py new file mode 100644 index 00000000..b37c31b6 --- /dev/null +++ b/tests/backends/console/sb/__init__.py @@ -0,0 +1 @@ +"""Tests for Sourcebots Arduino Console backends.""" diff --git a/tests/backends/console/arduino/test_uno.py b/tests/backends/console/sb/test_arduino.py similarity index 86% rename from tests/backends/console/arduino/test_uno.py rename to tests/backends/console/sb/test_arduino.py index a388e3dc..00593755 100644 --- a/tests/backends/console/arduino/test_uno.py +++ b/tests/backends/console/sb/test_arduino.py @@ -1,16 +1,16 @@ -"""Tests for the Arduino Uno console backend.""" +"""Tests for the SourceBots Arduino console backend.""" import pytest from tests.backends.console.helpers import MockConsole -from j5.backends.console.arduino.uno import ArduinoUnoConsoleBackend +from j5.backends.console.sb.arduino import SBArduinoConsoleBackend from j5.components.gpio_pin import GPIOPinMode def test_backend_initialisation() -> None: """Test that we can initialise a Backend.""" - backend = ArduinoUnoConsoleBackend("test") - assert isinstance(backend, ArduinoUnoConsoleBackend) + backend = SBArduinoConsoleBackend("test") + assert isinstance(backend, SBArduinoConsoleBackend) assert len(backend._pins) == 18 @@ -28,19 +28,19 @@ def test_backend_discover() -> None: This backend does not support discovery. """ with pytest.raises(NotImplementedError): - ArduinoUnoConsoleBackend.discover() + SBArduinoConsoleBackend.discover() def test_backend_firmware_version() -> None: """Test that we can get the firmware version.""" - backend = ArduinoUnoConsoleBackend("TestBoard") + backend = SBArduinoConsoleBackend("TestBoard") assert backend.firmware_version is None def test_set_gpio_pin_mode() -> None: """Test that we can set the mode of a GPIO pin.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -57,7 +57,7 @@ def test_set_gpio_pin_mode() -> None: def test_get_gpio_pin_mode() -> None: """Test that we can get the mode of a GPIO Pin.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -73,7 +73,7 @@ def test_get_gpio_pin_mode() -> None: def test_write_gpio_pin_digital_state() -> None: """Test that we can write a digital state.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -87,7 +87,7 @@ def test_write_gpio_pin_digital_state() -> None: def test_write_gpio_pin_digital_state_bad_mode() -> None: """Test that we cannot write a digital state in the wrong mode.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -100,7 +100,7 @@ def test_write_gpio_pin_digital_state_bad_mode() -> None: def test_get_gpio_pin_digital_state() -> None: """Test that we can get a digital state.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -114,7 +114,7 @@ def test_get_gpio_pin_digital_state() -> None: def test_get_gpio_pin_digital_state_bad_mode() -> None: """Test that we cannot get a digital state in the wrong mode.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -127,7 +127,7 @@ def test_get_gpio_pin_digital_state_bad_mode() -> None: def test_read_gpio_pin_digital_state() -> None: """Test that we can read the digital state of a pin.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -139,7 +139,7 @@ def test_read_gpio_pin_digital_state() -> None: def test_read_gpio_pin_digital_state_bad_mode() -> None: """Test that we cannot read a digital state in the wrong mode.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -152,7 +152,7 @@ def test_read_gpio_pin_digital_state_bad_mode() -> None: def test_read_gpio_pin_analogue_value() -> None: """Test that we can read an analogue value.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -164,7 +164,7 @@ def test_read_gpio_pin_analogue_value() -> None: def test_read_gpio_pin_analogue_value_bad_mode() -> None: """Test that we cannot read an analogue value in the wrong mode.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -176,7 +176,7 @@ def test_read_gpio_pin_analogue_value_bad_mode() -> None: def test_write_gpio_pin_dac_value() -> None: """Test that this isn't implemented.""" - backend = ArduinoUnoConsoleBackend("test") + backend = SBArduinoConsoleBackend("test") with pytest.raises(NotImplementedError): backend.write_gpio_pin_dac_value(10, 1.0) @@ -184,7 +184,7 @@ def test_write_gpio_pin_dac_value() -> None: def test_write_gpio_pin_pwm_value() -> None: """Test that this isn't implemented.""" - backend = ArduinoUnoConsoleBackend("test") + backend = SBArduinoConsoleBackend("test") with pytest.raises(NotImplementedError): backend.write_gpio_pin_pwm_value(10, 1.0) @@ -192,7 +192,7 @@ def test_write_gpio_pin_pwm_value() -> None: def test_get_led_state() -> None: """Test that we can get the LED state.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -204,7 +204,7 @@ def test_get_led_state() -> None: def test_set_led_state() -> None: """Test that we can set the LED state.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -220,7 +220,7 @@ def test_set_led_state() -> None: def test_that_led_is_pin_13() -> None: """Test that the LED has the same state as Pin 13.""" - backend = ArduinoUnoConsoleBackend( + backend = SBArduinoConsoleBackend( "TestBoard", console_class=MockConsole, ) @@ -232,7 +232,7 @@ def test_that_led_is_pin_13() -> None: def test_one_led() -> None: """Test that we can only control LED 0.""" - backend = ArduinoUnoConsoleBackend("test") + backend = SBArduinoConsoleBackend("test") with pytest.raises(ValueError): backend.set_led_state(1, False) diff --git a/tests/backends/hardware/arduino/__init__.py b/tests/backends/hardware/arduino/__init__.py deleted file mode 100644 index 501d89b2..00000000 --- a/tests/backends/hardware/arduino/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for arduino hardware implementations.""" diff --git a/tests/backends/hardware/sb/__init__.py b/tests/backends/hardware/sb/__init__.py new file mode 100644 index 00000000..03f2c641 --- /dev/null +++ b/tests/backends/hardware/sb/__init__.py @@ -0,0 +1 @@ +"""Tests for SourceBots hardware implementations.""" diff --git a/tests/backends/hardware/arduino/test_uno.py b/tests/backends/hardware/sb/test_arduino.py similarity index 77% rename from tests/backends/hardware/arduino/test_uno.py rename to tests/backends/hardware/sb/test_arduino.py index 1e7276d0..15ff40a6 100644 --- a/tests/backends/hardware/arduino/test_uno.py +++ b/tests/backends/hardware/sb/test_arduino.py @@ -1,4 +1,4 @@ -"""Tests for the Arduino Uno hardware implementation.""" +"""Tests for the SourceBots Arduino hardware implementation.""" from math import isclose from typing import Optional, cast @@ -7,13 +7,13 @@ from tests.backends.hardware.j5.mock_serial import MockSerial from j5.backends import CommunicationError -from j5.backends.hardware.arduino.uno import ArduinoUnoHardwareBackend from j5.backends.hardware.env import NotSupportedByHardwareError +from j5.backends.hardware.sb.arduino import SBArduinoHardwareBackend from j5.components import GPIOPinMode -class UnoSerial(MockSerial): - """UnoSerial is the same as MockSerial, but includes data we expect to receive.""" +class SBArduinoSerial(MockSerial): + """SBArduinoSerial is the same as MockSerial, but includes expected received data.""" expected_baudrate = 115200 firmware_version = "2019.6.0" @@ -48,38 +48,38 @@ def check_data_sent_by_constructor(self) -> None: self.check_sent_data(data.encode("utf-8")) -class UnoSerialOldVersion1(UnoSerial): - """Like UnoSerial, but reports an older version number.""" +class SBArduinoSerialOldVersion1(SBArduinoSerial): + """Like SBArduinoSerial, but reports an older version number.""" firmware_version = "2018.7.0" -class UnoSerialOldVersion2(UnoSerial): - """Like UnoSerial, but reports an older version number.""" +class SBArduinoSerialOldVersion2(SBArduinoSerial): + """Like SBArduinoSerial, but reports an older version number.""" firmware_version = "2019.5.0" -class UnoSerialNewVersion1(UnoSerial): - """Like UnoSerial, but reports an newer version number.""" +class SBArduinoSerialNewVersion1(SBArduinoSerial): + """Like SBArduinoSerial, but reports an newer version number.""" firmware_version = "2019.6.1" -class UnoSerialNewVersion2(UnoSerial): - """Like UnoSerial, but reports an newer version number.""" +class SBArduinoSerialNewVersion2(SBArduinoSerial): + """Like SBArduinoSerial, but reports an newer version number.""" firmware_version = "2019.7.0" -class UnoSerialNewVersion3(UnoSerial): - """Like UnoSerial, but reports an newer version number.""" +class SBArduinoSerialNewVersion3(SBArduinoSerial): + """Like SBArduinoSerial, but reports an newer version number.""" firmware_version = "2020.1.0" -class UnoSerialFailureResponse(UnoSerial): - """Like UnoSerial, but returns a failure response rather than success.""" +class SBArduinoSerialFailureResponse(SBArduinoSerial): + """Like SBArduinoSerial, but returns a failure response rather than success.""" def respond_to_write(self, data: bytes) -> None: """Hook that can be overriden by subclasses to respond to sent data.""" @@ -87,10 +87,10 @@ def respond_to_write(self, data: bytes) -> None: def test_backend_initialisation() -> None: - """Test that we can initialise a ArduinoUnoHardwareBackend.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) - assert type(backend) is ArduinoUnoHardwareBackend - assert type(backend._serial) is UnoSerial + """Test that we can initialise a SBArduinoHardwareBackend.""" + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) + assert type(backend) is SBArduinoHardwareBackend + assert type(backend._serial) is SBArduinoSerial assert all( pin.mode is GPIOPinMode.DIGITAL_INPUT for pin in backend._digital_pins.values() ) @@ -99,8 +99,8 @@ def test_backend_initialisation() -> None: def test_backend_initialisation_serial() -> None: """Test commands/responses are sent/received during initialisation.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) - serial = cast(UnoSerial, backend._serial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) + serial = cast(SBArduinoSerial, backend._serial) serial.check_data_sent_by_constructor() serial.check_all_received_data_consumed() @@ -108,30 +108,30 @@ def test_backend_initialisation_serial() -> None: def test_backend_version_check() -> None: """Test that an exception is raised if the arduino reports an unsupported version.""" with pytest.raises(CommunicationError): - ArduinoUnoHardwareBackend("COM0", UnoSerialOldVersion1) + SBArduinoHardwareBackend("COM0", SBArduinoSerialOldVersion1) with pytest.raises(CommunicationError): - ArduinoUnoHardwareBackend("COM0", UnoSerialOldVersion2) - ArduinoUnoHardwareBackend("COM0", UnoSerialNewVersion1) - ArduinoUnoHardwareBackend("COM0", UnoSerialNewVersion2) - ArduinoUnoHardwareBackend("COM0", UnoSerialNewVersion3) + SBArduinoHardwareBackend("COM0", SBArduinoSerialOldVersion2) + SBArduinoHardwareBackend("COM0", SBArduinoSerialNewVersion1) + SBArduinoHardwareBackend("COM0", SBArduinoSerialNewVersion2) + SBArduinoHardwareBackend("COM0", SBArduinoSerialNewVersion3) def test_backend_firmware_version() -> None: """Test that the firmware version is parsed correctly.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) - assert backend.firmware_version == UnoSerial.firmware_version + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) + assert backend.firmware_version == SBArduinoSerial.firmware_version def test_backend_handles_failure() -> None: """Test that an exception is raised when a failure response is received.""" with pytest.raises(CommunicationError): - ArduinoUnoHardwareBackend("COM0", UnoSerialFailureResponse) + SBArduinoHardwareBackend("COM0", SBArduinoSerialFailureResponse) def test_backend_get_set_pin_mode() -> None: """Test that we can get and set pin modes.""" pin = 2 - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) assert backend.get_gpio_pin_mode(pin) is GPIOPinMode.DIGITAL_INPUT backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_OUTPUT) assert backend.get_gpio_pin_mode(pin) is GPIOPinMode.DIGITAL_OUTPUT @@ -140,7 +140,7 @@ def test_backend_get_set_pin_mode() -> None: def test_backend_digital_pin_modes() -> None: """Test that only certain modes are valid on digital pins.""" pin = 2 - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_INPUT) backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_INPUT_PULLUP) with pytest.raises(NotSupportedByHardwareError): @@ -157,7 +157,7 @@ def test_backend_digital_pin_modes() -> None: def test_backend_analogue_pin_modes() -> None: """Test that only certain modes are valid on digital pins.""" pin = 14 - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) with pytest.raises(NotSupportedByHardwareError): backend.set_gpio_pin_mode(pin, GPIOPinMode.DIGITAL_INPUT) with pytest.raises(NotSupportedByHardwareError): @@ -175,8 +175,8 @@ def test_backend_analogue_pin_modes() -> None: def test_backend_write_digital_state() -> None: """Test that we can write the digital state of a pin.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) - serial = cast(UnoSerial, backend._serial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) + serial = cast(SBArduinoSerial, backend._serial) serial.check_data_sent_by_constructor() # This should put the pin into the most recent (or default) output state. backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) @@ -190,7 +190,7 @@ def test_backend_write_digital_state() -> None: def test_backend_write_digital_state_requires_pin_mode() -> None: """Check that pin must be in DIGITAL_OUTPUT mode for write digital state to work.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) assert backend.get_gpio_pin_mode(2) is not GPIOPinMode.DIGITAL_OUTPUT with pytest.raises(ValueError): backend.write_gpio_pin_digital_state(2, True) @@ -198,15 +198,15 @@ def test_backend_write_digital_state_requires_pin_mode() -> None: def test_backend_write_digital_state_requires_digital_pin() -> None: """Check that pins 14-19 are not supported by write digital state.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) with pytest.raises(NotSupportedByHardwareError): backend.write_gpio_pin_digital_state(14, True) def test_backend_digital_state_persists() -> None: """Test switching to a different mode and then back to output.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) - serial = cast(UnoSerial, backend._serial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) + serial = cast(SBArduinoSerial, backend._serial) serial.check_data_sent_by_constructor() backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) serial.check_sent_data(b"W 2 L\n") @@ -227,7 +227,7 @@ def test_backend_digital_state_persists() -> None: def test_backend_get_digital_state() -> None: """Test that we can read back the digital state of a pin.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) # This should put the pin into the most recent (or default) output state. backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) assert backend.get_gpio_pin_digital_state(2) is False @@ -239,7 +239,7 @@ def test_backend_get_digital_state() -> None: def test_backend_get_digital_state_requires_pin_mode() -> None: """Check that pin must be in DIGITAL_OUTPUT mode for get digital state to work.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) assert backend.get_gpio_pin_mode(2) is not GPIOPinMode.DIGITAL_OUTPUT with pytest.raises(ValueError): backend.get_gpio_pin_digital_state(2) @@ -247,15 +247,15 @@ def test_backend_get_digital_state_requires_pin_mode() -> None: def test_backend_get_digital_state_requires_digital_pin() -> None: """Check that pins 14-19 are not supported by get digital state.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) with pytest.raises(NotSupportedByHardwareError): backend.get_gpio_pin_digital_state(14) def test_backend_input_modes() -> None: """Check that the correct commands are send when setting pins to input modes.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) - serial = cast(UnoSerial, backend._serial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) + serial = cast(SBArduinoSerial, backend._serial) serial.check_data_sent_by_constructor() backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_INPUT) serial.check_sent_data(b"W 2 Z\n") @@ -268,8 +268,8 @@ def test_backend_input_modes() -> None: def test_backend_read_digital_state() -> None: """Test that we can read the digital state of a pin.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) - serial = cast(UnoSerial, backend._serial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) + serial = cast(SBArduinoSerial, backend._serial) serial.check_data_sent_by_constructor() backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_INPUT) @@ -293,7 +293,7 @@ def test_backend_read_digital_state() -> None: def test_backend_read_digital_state_requires_pin_mode() -> None: """Check that pin must be in DIGITAL_INPUT* mode for read digital state to work.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) backend.set_gpio_pin_mode(2, GPIOPinMode.DIGITAL_OUTPUT) assert backend.get_gpio_pin_mode(2) is not GPIOPinMode.DIGITAL_INPUT with pytest.raises(ValueError): @@ -302,15 +302,15 @@ def test_backend_read_digital_state_requires_pin_mode() -> None: def test_backend_read_digital_state_requires_digital_pin() -> None: """Check that pins 14-19 are not supported by read digital state.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) with pytest.raises(NotSupportedByHardwareError): backend.read_gpio_pin_digital_state(14) def test_backend_read_analogue() -> None: """Test that we can read the digital state of a pin.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) - serial = cast(UnoSerial, backend._serial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) + serial = cast(SBArduinoSerial, backend._serial) serial.check_data_sent_by_constructor() readings = [212, 535, 662, 385] @@ -329,6 +329,6 @@ def test_backend_read_analogue() -> None: def test_backend_read_analogue_requires_analogue_pin() -> None: """Check that pins 2-13 are not supported by read analogue.""" - backend = ArduinoUnoHardwareBackend("COM0", UnoSerial) + backend = SBArduinoHardwareBackend("COM0", SBArduinoSerial) with pytest.raises(NotSupportedByHardwareError): backend.read_gpio_pin_analogue_value(13) diff --git a/tests/boards/arduino/__init__.py b/tests/boards/arduino/__init__.py deleted file mode 100644 index 7e800fd3..00000000 --- a/tests/boards/arduino/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test supported arduino boards.""" diff --git a/tests/boards/sb/__init__.py b/tests/boards/sb/__init__.py new file mode 100644 index 00000000..019aa044 --- /dev/null +++ b/tests/boards/sb/__init__.py @@ -0,0 +1 @@ +"""Test supported SourceBots boards.""" diff --git a/tests/boards/arduino/test_uno.py b/tests/boards/sb/test_arduino.py similarity index 83% rename from tests/boards/arduino/test_uno.py rename to tests/boards/sb/test_arduino.py index 84e1f18c..c176f6a3 100644 --- a/tests/boards/arduino/test_uno.py +++ b/tests/boards/sb/test_arduino.py @@ -1,9 +1,9 @@ -"""Tests for the Arduino Uno and related classes.""" +"""Tests for the SourceBots Arduino and related classes.""" from typing import TYPE_CHECKING, Optional, Set from j5.backends import Backend, Environment -from j5.boards.arduino import AnaloguePin, ArduinoUnoBoard +from j5.boards.sb import AnaloguePin, SBArduinoBoard from j5.components import GPIOPin, GPIOPinInterface, GPIOPinMode, LEDInterface if TYPE_CHECKING: @@ -13,7 +13,7 @@ MockEnvironment = Environment("MockEnvironment") -class MockArduinoUnoBackend( +class MockSBArduinoBackend( GPIOPinInterface, LEDInterface, Backend, @@ -21,7 +21,7 @@ class MockArduinoUnoBackend( """Mock Backend for testing the Arduino Uno.""" environment = MockEnvironment - board = ArduinoUnoBoard + board = SBArduinoBoard def set_gpio_pin_mode(self, identifier: int, pin_mode: GPIOPinMode) -> None: """Set the GPIO pin mode.""" @@ -76,44 +76,44 @@ def firmware_version(self) -> Optional[str]: def test_uno_initialisation() -> None: """Test that we can initialise an Uno.""" - ArduinoUnoBoard("SERIAL0", MockArduinoUnoBackend()) + SBArduinoBoard("SERIAL0", MockSBArduinoBackend()) def test_uno_discover() -> None: """Test that we can discover Unos.""" - assert MockArduinoUnoBackend.discover() == set() + assert MockSBArduinoBackend.discover() == set() def test_uno_name() -> None: """Test the name attribute of the Uno.""" - uno = ArduinoUnoBoard("SERIAL0", MockArduinoUnoBackend()) + uno = SBArduinoBoard("SERIAL0", MockSBArduinoBackend()) assert uno.name == "Arduino Uno" def test_uno_serial() -> None: """Test the serial attribute of the Uno.""" - uno = ArduinoUnoBoard("SERIAL0", MockArduinoUnoBackend()) + uno = SBArduinoBoard("SERIAL0", MockSBArduinoBackend()) assert uno.serial == "SERIAL0" def test_uno_firmware_version() -> None: """Test the firmware_version attribute of the Uno.""" - uno = ArduinoUnoBoard("SERIAL0", MockArduinoUnoBackend()) + uno = SBArduinoBoard("SERIAL0", MockSBArduinoBackend()) assert uno.firmware_version is None def test_uno_make_safe() -> None: """Test the make_safe method of the Uno.""" - uno = ArduinoUnoBoard("SERIAL0", MockArduinoUnoBackend()) + uno = SBArduinoBoard("SERIAL0", MockSBArduinoBackend()) uno.make_safe() def test_uno_pins() -> None: """Test the pins of the Uno.""" - uno = ArduinoUnoBoard("SERIAL0", MockArduinoUnoBackend()) + uno = SBArduinoBoard("SERIAL0", MockSBArduinoBackend()) assert len(uno.pins) == 12 + 6 From 1f1a28245afc85b2fa6a577bbafc78947fc7fd45 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 20:40:44 +0100 Subject: [PATCH 19/23] Fix arduino analogue syntax --- j5/backends/hardware/sb/arduino.py | 11 ++++++----- tests/backends/hardware/sb/test_arduino.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/j5/backends/hardware/sb/arduino.py b/j5/backends/hardware/sb/arduino.py index 90b551a5..c5302438 100644 --- a/j5/backends/hardware/sb/arduino.py +++ b/j5/backends/hardware/sb/arduino.py @@ -251,11 +251,12 @@ def read_gpio_pin_analogue_value(self, identifier: int) -> float: ) analogue_pin_num = identifier - 14 results = self._command("A") - if len(results) != 4: - raise CommunicationError(f"Invalid response from Arduino: {results}") - reading = int(results[analogue_pin_num]) - voltage = (reading / 1024.0) * 5.0 - return voltage + for result in results: + pin_name, reading = result.split(None, 1) + if pin_name == f"a{analogue_pin_num}": + voltage = (int(reading) / 1024.0) * 5.0 + return voltage + raise CommunicationError(f"Invalid response from Arduino: {results}") def write_gpio_pin_dac_value(self, identifier: int, scaled_value: float) -> None: """Write a scaled analogue value to the DAC on the GPIO pin.""" diff --git a/tests/backends/hardware/sb/test_arduino.py b/tests/backends/hardware/sb/test_arduino.py index 15ff40a6..2d0888a4 100644 --- a/tests/backends/hardware/sb/test_arduino.py +++ b/tests/backends/hardware/sb/test_arduino.py @@ -317,8 +317,8 @@ def test_backend_read_analogue() -> None: for i, expected_reading in enumerate(readings): # "read analogue" command reads all four pins at once. identifier = 14 + i - for reading in readings: - serial.append_received_data(f"> {reading}".encode("utf-8"), newline=True) + for j, reading in enumerate(readings): + serial.append_received_data(f"> a{j} {reading}".encode("utf-8"), newline=True) expected_voltage = (expected_reading / 1024.0) * 5.0 measured_voltage = backend.read_gpio_pin_analogue_value(identifier) assert isclose(measured_voltage, expected_voltage) From 25375cf0d45e3d11ec9ce4b065aa2c7aab114a7f Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 20:40:55 +0100 Subject: [PATCH 20/23] Whitespace cleanup --- stubs/usb/core.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/usb/core.pyi b/stubs/usb/core.pyi index c0e03a7b..91f31a7c 100644 --- a/stubs/usb/core.pyi +++ b/stubs/usb/core.pyi @@ -31,4 +31,4 @@ def find( find_all: bool = False, idVendor: Optional[int] = None, idProduct: Optional[int] = None, -) -> Generator[Device, None, None]: ... \ No newline at end of file +) -> Generator[Device, None, None]: ... From c64dc8141b7edb2a7e986fd1f5268b679e631397 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Sat, 13 Jul 2019 20:41:48 +0100 Subject: [PATCH 21/23] Add test script for arduino --- tests_hw/test_sb_arduino.py | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests_hw/test_sb_arduino.py diff --git a/tests_hw/test_sb_arduino.py b/tests_hw/test_sb_arduino.py new file mode 100644 index 00000000..452db692 --- /dev/null +++ b/tests_hw/test_sb_arduino.py @@ -0,0 +1,67 @@ +"""Test the SourceBots Arduino.""" + +from datetime import timedelta +from time import sleep + +import j5.backends.hardware.sb.arduino # noqa: F401 +from j5 import BaseRobot, BoardGroup +from j5.backends.hardware import HardwareEnvironment +from j5.boards.sb.arduino import SBArduinoBoard +from j5.boards.sr.v4 import PowerBoard +from j5.components.gpio_pin import GPIOPinMode +from j5.components.piezo import Note + + +class Robot(BaseRobot): + """A basic robot with a power board.""" + + def __init__(self) -> None: + self.power_boards = BoardGroup[PowerBoard]( + HardwareEnvironment.get_backend(PowerBoard), + ) + self.power_board: PowerBoard = self.power_boards.singular() + + self.power_board.outputs.power_on() + sleep(0.2) # Give time for arduino to initialise. + + self.arduinos = BoardGroup[SBArduinoBoard]( + HardwareEnvironment.get_backend(SBArduinoBoard), + ) + + self.arduino: SBArduinoBoard = self.arduinos.singular() + + +if __name__ == '__main__': + + print("Testing SR Arduino.") + + r = Robot() + + r.power_board.piezo.buzz(timedelta(seconds=0.1), Note.A6) + + print("Waiting for start button...") + r.power_board.wait_for_start_flash() + + print(f"Serial number: {r.arduino.serial}") + print(f"Firmware version: {r.arduino.firmware_version}") + + print("Setting all pins high.") + for pin in range(2, 14): + r.arduino.pins[pin].mode = GPIOPinMode.DIGITAL_OUTPUT + r.arduino.pins[pin].digital_state = True + + sleep(1) + + print("Setting all pins low.") + for pin in range(2, 14): + r.arduino.pins[pin].mode = GPIOPinMode.DIGITAL_OUTPUT + r.arduino.pins[pin].digital_state = False + + sleep(1) + + for pin in range(2, 14): + r.arduino.pins[pin].mode = GPIOPinMode.DIGITAL_INPUT + print(f"Pin {pin} digital state = {r.arduino.pins[pin].digital_state}") + for pin in range(14, 18): + r.arduino.pins[pin].mode = GPIOPinMode.ANALOGUE_INPUT + print(f"Pin {pin} analogue voltage = {r.arduino.pins[pin].analogue_value}") From e932029cc27b588298f1321be00d93718c7e4b09 Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Thu, 18 Jul 2019 12:38:20 +0100 Subject: [PATCH 22/23] Update exception types --- j5/backends/hardware/sb/arduino.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/j5/backends/hardware/sb/arduino.py b/j5/backends/hardware/sb/arduino.py index c5302438..5c00ee15 100644 --- a/j5/backends/hardware/sb/arduino.py +++ b/j5/backends/hardware/sb/arduino.py @@ -260,13 +260,13 @@ def read_gpio_pin_analogue_value(self, identifier: int) -> float: def write_gpio_pin_dac_value(self, identifier: int, scaled_value: float) -> None: """Write a scaled analogue value to the DAC on the GPIO pin.""" - # Uno doesn't have any of these. - raise NotImplementedError + raise NotSupportedByHardwareError("SB Arduino does not have a DAC") def write_gpio_pin_pwm_value(self, identifier: int, duty_cycle: float) -> None: """Write a scaled analogue value to the PWM on the GPIO pin.""" - # Not implemented on SBArduinoBoard yet. - raise NotImplementedError + raise NotSupportedByHardwareError( + "SB Arduino firmware does not implement PWM output", + ) def get_led_state(self, identifier: int) -> bool: """Get the state of an LED.""" From fdaaa769e8a6691dae7f561c6166c26ff298791f Mon Sep 17 00:00:00 2001 From: Kier Davis Date: Thu, 18 Jul 2019 12:41:10 +0100 Subject: [PATCH 23/23] Use repr for debugging values in exception messages --- j5/backends/hardware/sb/arduino.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/j5/backends/hardware/sb/arduino.py b/j5/backends/hardware/sb/arduino.py index 5c00ee15..2b365f63 100644 --- a/j5/backends/hardware/sb/arduino.py +++ b/j5/backends/hardware/sb/arduino.py @@ -230,14 +230,14 @@ def read_gpio_pin_digital_state(self, identifier: int) -> bool: f"in order to read the digital state.") results = self._command("R", str(identifier)) if len(results) != 1: - raise CommunicationError(f"Invalid response from Arduino: {results}") + raise CommunicationError(f"Invalid response from Arduino: {results!r}") result = results[0] if result == "H": return True elif result == "L": return False else: - raise CommunicationError(f"Invalid response from Arduino: {result}") + raise CommunicationError(f"Invalid response from Arduino: {result!r}") def read_gpio_pin_analogue_value(self, identifier: int) -> float: """Read the analogue voltage of the GPIO pin.""" @@ -256,7 +256,7 @@ def read_gpio_pin_analogue_value(self, identifier: int) -> float: if pin_name == f"a{analogue_pin_num}": voltage = (int(reading) / 1024.0) * 5.0 return voltage - raise CommunicationError(f"Invalid response from Arduino: {results}") + raise CommunicationError(f"Invalid response from Arduino: {results!r}") def write_gpio_pin_dac_value(self, identifier: int, scaled_value: float) -> None: """Write a scaled analogue value to the DAC on the GPIO pin."""