Extending j5

j5 utilises a number of abstractions to enable similar APIs across platforms and hardware. This page explains design decisions behind the major abstractions and how to use them correctly.


A component is the smallest logical part of some hardware.

A component will have the same basic functionality no matter what hardware it is on. For example, an LED is still an LED, no matter whether it is on an Arduino, or the control panel of a jumbo jet; it still can be turned on and off.

The component should expose a user-friendly API, attempting to be consistent with other components where possible.

Validation of user input should be done in the component.


A component is implemented by sub-classing the j5.components.Component.

It is uniquely identified on a particular j5.boards.Board by an integer, which is usually passed into the constructor.

Every instance of a component should have a reference to a j5.backends.Backend, that implements the relevant j5.components.Interface.

The relevant j5.components.Interface should also be defined.

 1    def set_led_state(self, identifier: int, state: bool) -> None:
 2        """
 3        Set the state of an LED.
 5        :param identifier: identifier of the LED.
 6        :param state: desired state of the LED.
 7        """
 8        raise NotImplementedError  # pragma: no cover
11class LED(Component):
12    """A standard Light Emitting Diode."""
14    def __init__(self, identifier: int, backend: LEDInterface) -> None:
15        self._backend = backend
16        self._identifier = identifier
18    @staticmethod
19    def interface_class() -> Type[LEDInterface]:
20        """
21        Get the interface class that is required to use this component.
23        :returns: interface class.
24        """
25        return LEDInterface


An interface defines the low-level methods that are required to control a given component.


An interface should sub-class j5.components.Interface.

The interface class should contain abstract methods required to control the component.

 1class LEDInterface(Interface):
 2    """An interface containing the methods required to control an LED."""
 4    @abstractmethod
 5    def get_led_state(self, identifier: int) -> bool:
 6        """
 7        Get the state of an LED.
 9        :param identifier: identifier of the LED.
10        :returns: current state of the LED.
11        """
12        raise NotImplementedError  # pragma: no cover


A Board is a class that exposes a group of components, used to represent a physical board in a robotics kit.

The Board class should not directly interact with any hardware, instead making calls to the Backend class where necessary, and preferably diverting interaction through the component classes where possible.


An interface should sub-class j5.boards.Board.

It will need to implement a number of abstract functions on that class.

Components should be created in the constructor, and should be made available to the user through properties. Care should be taken to ensure that users cannot accidentally override components.

A backend should also be passed to the board in the constructor, usually done in j5.backends.Backend.discover()

A notable method that should be implemented is j5.boards.Board.make_safe(), which should call the appropriate methods on the components to ensure that the board is safe in the event of something going wrong.

 3class MotorBoard(Board):
 4    """Student Robotics v4 Motor Board."""
 6    name: str = "Student Robotics v4 Motor Board"
 8    def __init__(
 9        self,
10        serial: str,
11        backend: Backend,
12        *,
13        safe_state: MotorState = MotorSpecialState.BRAKE,
14    ) -> None:
15        self._serial = serial
16        self._backend = backend
17        self._safe_state = safe_state
19        self._outputs = ImmutableList[Motor](
20            Motor(output, cast(MotorInterface, self._backend)) for output in range(0, 2)
21        )
23    @property
24    def serial_number(self) -> str:
25        """
26        Get the serial number of the board.
28        :returns: Serial number of the board.
29        """
30        return self._serial
32    @property
33    def firmware_version(self) -> Optional[str]:
34        """
35        Get the firmware version of the board.
37        :returns: Firmware version of the board.
38        """
39        return self._backend.firmware_version


A backend implements all of the interfaces required to control a board.

A backend also contains a method that can discover boards.

Multiple backends can be implemented for one board, but a backend can only support one board. This could be used for implementing a simulated version of a board, in addition to the hardware implementation.

Backends can also validate is data is suitable for them, and throw an error if not; for example j5.backends.hardware.env.NotSupportedByHardwareError.


 1class SRV4MotorBoardConsoleBackend(
 2    MotorInterface,
 3    Backend,
 5    """The console implementation of the SR v4 motor board."""
 7    board = MotorBoard
 9    @classmethod
10    def discover(cls) -> Set[Board]:
11        """
12        Discover boards that this backend can control.
14        :returns: set of boards that this backend can control.
15        """
16        return {cast(Board, MotorBoard("SERIAL", cls("SERIAL")))}
18    def __init__(self, serial: str, console_class: Type[Console] = Console) -> None:
19        self._serial = serial
21        # Initialise our stored values for the state.
22        self._state: List[MotorState] = [MotorSpecialState.BRAKE for _ in range(0, 2)]
24        # Setup console helper
25        self._console = console_class(f"{self.board.__name__}({self._serial})")
27    @property
28    def serial(self) -> str:
29        """
30        The serial number reported by the board.
32        :returns: serial number reported by the board.
33        """
34        return self._serial
36    @property
37    def firmware_version(self) -> Optional[str]:
38        """
39        The firmware version reported by the board.
41        :returns: firmware version reported by the board, if any.
42        """
43        return None  # Console, so no firmware
45    def get_motor_state(self, identifier: int) -> MotorState:
46        """
47        Get the current motor state.
49        :param identifier: identifier of the motor
50        :returns: state of the motor.
51        """
52        # We are unable to read the state from the motor board, in hardware
53        # so instead of asking, we'll get the last set value.