PK!1j5/__init__.py"""j5 Robotics API.""" from .base_robot import BaseRobot from .boards import BoardGroup __all__ = ["BoardGroup", "BaseRobot", "__version__", "__version_short__"] __version__ = "0.2.0" __version_short__ = "0.2.0" PK!\j5/backends/__init__.py"""Backend classes.""" from .backend import Backend, CommunicationError, Environment __all__ = ["Backend", "CommunicationError", "Environment"] PK!,pj5/backends/backend.py"""The base classes for backends.""" from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, Dict, List, Optional, Type if TYPE_CHECKING: from j5.boards import Board # noqa class CommunicationError(Exception): """ A communication error occurred. This error is thrown when there is an error communicating with a board, if a more specific exception is available, then that may be thrown instead, but it should inherit from this one. """ class BackendMeta(ABCMeta): """ The metaclass for a backend. Responsible for registering the board-backend mapping with an Environment. """ def __new__(mcs, name, bases, namespace, **kwargs): # type:ignore """Create a new class object.""" cls = super().__new__(mcs, name, bases, namespace, **kwargs) # type: ignore if cls.__name__ == "Backend": return cls if hasattr(cls, "environment"): if cls.environment is not None and cls.board is not None: mcs._check_compatibility(cls) mcs._check_component_interfaces(cls) cls.environment.register_backend(cls.board, cls) return cls raise RuntimeError(f"The {str(cls)} has no environment attribute") def _check_compatibility(cls): # type: ignore """Check that the backend and environment are compatible.""" if type(cls.environment) != Environment: raise ValueError("The environment must be of type Environment.") if cls.board in cls.environment.supported_boards: raise RuntimeError( "You cannot register multiple backends for the same board in the same Environment.") # noqa: E501 def _check_component_interfaces(cls): # type: ignore """ Check that the backend has the right interfaces. Certain interfaces are required to support components, and we want to make sure that the Backend implements them. This is a run-time type check. """ for component in cls.board.supported_components(): if not issubclass(cls, component.interface_class()): raise TypeError("The backend class doesn't have a required interface.") # noqa: E501 class Backend(metaclass=BackendMeta): """ The base class for a backend. A backend is an implementation of a specific board for an environment. It can hold data about the actual board it is controlling. There should be a ratio of one instance of a Backend to one instance of a Board. The Backend object should not hold any references to the Board, instead having it's methods executed by the code for the individual Board. A Backend usually also implements a number of ComponentInterfaces which thus allow a physical component to be controlled by the abstract Component representation. """ @classmethod @abstractmethod def discover(cls) -> List['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 @abstractmethod def get_firmware_version(self) -> Optional[str]: """Get the firmware version of the board.""" raise NotImplementedError # pragma: no cover class Environment: """ A collection of board implementations that can work together. Auto-populated with board mappings using metaclass magic. """ def __init__(self, name: str): self.name = name self.board_backend_mapping: Dict[Type['Board'], Type[Backend]] = {} @property def supported_boards(self) -> List[Type['Board']]: """The boards that are supported by this backend group.""" return list(self.board_backend_mapping.keys()) def __str__(self) -> str: """Get a string representation of this group.""" return self.name def register_backend(self, board: Type['Board'], backend: Type[Backend]) -> None: """Register a new backend with this Backend Group.""" self.board_backend_mapping[board] = backend def get_backend(self, board: Type['Board']) -> Type[Backend]: """Get the backend for a board.""" if board not in self.supported_boards: raise NotImplementedError(f"The {str(self)} does not support {str(board)}") return self.board_backend_mapping[board] PK!OPDj5/backends/console/__init__.py"""Backends for the Console Environment.""" from .env import Console, ConsoleEnvironment __all__ = ["Console", "ConsoleEnvironment"] PK!gc??j5/backends/console/env.py"""The Console Environment.""" from typing import Callable, Optional, Type, TypeVar from j5.backends import Environment ConsoleEnvironment = Environment("ConsoleEnvironment") T = TypeVar("T") class Console: """A helper class for the console environment.""" def __init__( self, descriptor: str, print_function: Callable = print, # type: ignore input_function: Callable = input, # type: ignore ) -> None: self._descriptor = descriptor self._print = print_function self._input = input_function def info(self, message: str) -> None: """Print information to the user.""" self._print(f"{self._descriptor}: {message}") def read( # type: ignore self, prompt: str, return_type: Optional[Type[T]] = str, ) -> T: """Get a value of type 'return_type' from the user.""" if return_type is not None: while True: response = self._input(f"{self._descriptor}: {prompt}: ") try: # We have to ignore the types on this function unfortunately, # as static type checking is not powerful enough to confirm # that it is correct at runtime. return return_type(response) # type: ignore except ValueError: self.info(f"Unable to construct a {return_type.__name__}" f" from '{response}'") else: self._input(f"{self._descriptor}: {prompt}: ") PK!ۭVGG"j5/backends/console/sr/__init__.py"""Backends for Student Robotics boards in the console environment.""" PK!QQ%j5/backends/console/sr/v4/__init__.py"""Backends for Student Robotics version 4 boards in the console environment.""" PK!(j5/backends/console/sr/v4/power_board.py"""Console Backend for the SR V4 power board.""" from datetime import timedelta from typing import Dict, List, Optional, Type from j5.backends import Backend from j5.backends.console.env import Console, ConsoleEnvironment from j5.boards import Board from j5.boards.sr.v4.power_board import PowerBoard, PowerOutputPosition from j5.components import ( BatterySensorInterface, ButtonInterface, LEDInterface, PiezoInterface, PowerOutputInterface, ) class SRV4PowerBoardConsoleBackend( PowerOutputInterface, PiezoInterface, ButtonInterface, BatterySensorInterface, LEDInterface, Backend, ): """The console implementation of the SR V4 power board.""" environment = ConsoleEnvironment board = PowerBoard @classmethod def discover(cls) -> List[Board]: """Discover boards that this backend can control.""" raise NotImplementedError("The Console Backend cannot discover boards.") def __init__(self, serial: str, console_class: Type[Console] = Console) -> None: self._serial = serial self._output_states: Dict[int, bool] = { output.value: False for output in PowerOutputPosition } self._led_states: Dict[int, bool] = { i: False for i in range(2) } # Setup console helper self._console = console_class(f"{self.board.__name__}({self._serial})") @property def firmware_version(self) -> Optional[str]: """The firmware version reported by the board.""" return None # Console, so no firmware @property def serial(self) -> str: """The serial number reported by the board.""" return self._serial def get_firmware_version(self) -> Optional[str]: """Get the firmware version reported by the board.""" return None def get_power_output_enabled(self, identifier: int) -> bool: """Get whether a power output is enabled.""" try: return self._output_states[identifier] except KeyError: raise ValueError(f"Invalid power output identifier {identifier!r}; " f"valid identifiers are " f"{self._output_states.keys()}") from None def set_power_output_enabled( self, identifier: int, enabled: bool, ) -> None: """Set whether a power output is enabled.""" self._console.info(f"Setting output {identifier} to {enabled}") if identifier not in self._output_states.keys(): raise ValueError(f"Invalid power output identifier {identifier!r}; " f"valid identifiers are " f"{self._output_states.keys()}") self._output_states[identifier] = enabled def get_power_output_current(self, identifier: int) -> float: """Get the current being drawn on a power output, in amperes.""" if identifier in self._output_states: return self._console.read( f"Current for power output {identifier} [amps]", float, ) else: raise ValueError(f"Invalid power output identifier {identifier!r}; " f"valid identifiers are " f"{self._output_states.keys()}") from None def buzz(self, identifier: int, duration: timedelta, pitch: int) -> None: """Queue a pitch to be played.""" if identifier != 0: raise ValueError(f"invalid piezo identifier {identifier!r}; " f"the only valid identifier is 0") duration_ms = round(duration / timedelta(milliseconds=1)) if duration_ms > 65535: raise ValueError("Maximum piezo duration is 65535ms.") self._console.info(f"Buzzing at {pitch}Hz for {duration_ms}ms") def get_button_state(self, identifier: int) -> bool: """Get the state of a button.""" if identifier != 0: raise ValueError(f"invalid button identifier {identifier!r}; " f"the only valid identifier is 0") return self._console.read("Start button state [true/false]", bool) def wait_until_button_pressed(self, identifier: int) -> None: """Halt the program until this button is pushed.""" self._console.info("Waiting for start button press.") self._console.read("Hit return to press start button", None) def get_battery_sensor_voltage(self, identifier: int) -> float: """Get the voltage of a battery sensor.""" if identifier != 0: raise ValueError(f"invalid battery sensor identifier {identifier!r}; " f"the only valid identifier is 0") return self._console.read("Battery voltage [volts]", float) def get_battery_sensor_current(self, identifier: int) -> float: """Get the current of a battery sensor.""" if identifier != 0: raise ValueError(f"invalid battery sensor identifier {identifier!r}; " f"the only valid identifier is 0") return self._console.read("Battery current [amps]", float) def get_led_state(self, identifier: int) -> bool: """Get the state of an LED.""" return self._led_states[identifier] def set_led_state(self, identifier: int, state: bool) -> None: """Set the state of an LED.""" if identifier in self._led_states.keys(): self._console.info(f"Set LED {identifier} to {state}") self._led_states[identifier] = state else: raise ValueError(f"invalid LED identifier {identifier!r}; valid identifiers " f"are 0 (run LED) and 1 (error LED)") from None PK!%vv j5/backends/hardware/__init__.py"""Backends for the hardware environment.""" from .env import HardwareEnvironment __all__ = ["HardwareEnvironment"] PK!Y=j5/backends/hardware/env.py"""The hardware Environment.""" from j5.backends import Environment HardwareEnvironment = Environment("HardwareEnvironment") PK!AHH#j5/backends/hardware/sr/__init__.py"""Backends for Student Robotics boards in the hardware environment.""" PK!'RR&j5/backends/hardware/sr/v4/__init__.py"""Backends for Student Robotics version 4 boards in the hardware environment.""" PK!V)I;';')j5/backends/hardware/sr/v4/power_board.py"""Hardware Backend for the SR V4 power board.""" import struct from datetime import timedelta from functools import wraps from time import sleep from typing import ( Callable, Dict, List, Mapping, NamedTuple, Optional, TypeVar, Union, cast, ) import usb from j5.backends import Backend, CommunicationError from j5.backends.hardware.env import HardwareEnvironment from j5.boards import Board from j5.boards.sr.v4.power_board import PowerBoard, PowerOutputPosition from j5.components import ( BatterySensorInterface, ButtonInterface, LEDInterface, PiezoInterface, PowerOutputInterface, ) class ReadCommand(NamedTuple): """ Models a command to read information from the power board using USB controlRead. code identifies the command in accordance with the definitions in usb.h in the firmware source. data_len is the number of bytes that will be returned by the command. """ code: int data_len: int class WriteCommand(NamedTuple): """ Models a command to write information to the power board using USB controlWrite. code identifies the command in accordance with the definitions in usb.h in the firmware source. """ code: int # The names and codes of these commands match the definitions in usb.h in the firmware # source. CMD_READ_OUTPUT: Mapping[int, ReadCommand] = { output.value: ReadCommand(output.value, 4) for output in PowerOutputPosition } CMD_READ_5VRAIL = ReadCommand(6, 4) CMD_READ_BATTERY = ReadCommand(7, 8) CMD_READ_BUTTON = ReadCommand(8, 4) CMD_READ_FWVER = ReadCommand(9, 4) CMD_WRITE_OUTPUT: Mapping[int, WriteCommand] = { output.value: WriteCommand(output.value) for output in PowerOutputPosition } CMD_WRITE_RUNLED = WriteCommand(6) CMD_WRITE_ERRORLED = WriteCommand(7) CMD_WRITE_PIEZO = WriteCommand(8) # Stop the library from closing the USB connections before make_safe is called. usb._objfinalizer._AutoFinalizedObjectBase._do_finalize_object = ( # type: ignore lambda x: None ) class USBCommunicationError(CommunicationError): """An error occurred during USB communication.""" def __init__(self, usb_error: usb.core.USBError) -> None: super().__init__(usb_error.strerror) RT = TypeVar('RT') def handle_usb_error(func: Callable[..., RT]) -> Callable[..., RT]: # type: ignore """ Wrap functions that use usb1 and give friendly errors. The exceptions from usb1 are hard to find in documentation or code and are confusing to users. This decorator catches the USBErrors and throws a friendlier exception that can also be caught more easily. """ @wraps(func) def catch_exceptions(*args, **kwargs): # type: ignore try: return func(*args, **kwargs) except usb.core.USBError as e: raise USBCommunicationError(e) return catch_exceptions class SRV4PowerBoardHardwareBackend( PowerOutputInterface, PiezoInterface, ButtonInterface, BatterySensorInterface, LEDInterface, Backend, ): """The hardware implementation of the SR V4 power board.""" environment = HardwareEnvironment board = PowerBoard @classmethod @handle_usb_error def discover(cls, find: Callable = usb.core.find) -> List[Board]: """Discover boards that this backend can control.""" boards: List[Board] = [] device_list = find(idVendor=0x1bda, idProduct=0x0010, find_all=True) for device in device_list: backend = cls(device) board = PowerBoard(backend.serial, backend) boards.append(cast(Board, board)) return boards @handle_usb_error def __init__(self, usb_device: usb.core.Device) -> None: self._usb_device = usb_device self._output_states: Dict[int, bool] = { output.value: False for output in PowerOutputPosition } self._led_states: Dict[int, bool] = { i: False for i in range(2) } self.check_firmware_version_supported() @handle_usb_error def __del__(self) -> None: """Clean up device on destruction of object.""" usb.util.dispose_resources(self._usb_device) @handle_usb_error def _read(self, command: ReadCommand) -> bytes: return self._usb_device.ctrl_transfer( 0x80, 64, wValue=0, wIndex=command.code, data_or_wLength=command.data_len, ) @handle_usb_error def _write(self, command: WriteCommand, param: Union[int, bytes]) -> None: req_val: int = 0 req_data: bytes = b"" if isinstance(param, int): req_val = param else: req_data = param self._usb_device.ctrl_transfer( 0x00, 64, wValue=req_val, wIndex=command.code, data_or_wLength=req_data, ) def check_firmware_version_supported(self) -> None: """Raises an exception if the firmware version is not supported.""" v = self.firmware_version if v != "3": raise NotImplementedError(f"this power board is running firmware " f"version {v}, but only version 3 is supported") @property def firmware_version(self) -> str: """The firmware version reported by the board.""" version, = struct.unpack(" str: """The serial number reported by the board.""" # https://github.com/python/mypy/issues/1362 return self._usb_device.serial_number # type: ignore def get_firmware_version(self) -> Optional[str]: """Get the firmware version reported by the board.""" return str(self.firmware_version) def get_power_output_enabled(self, identifier: int) -> bool: """Get whether a power output is enabled.""" try: return self._output_states[identifier] except KeyError: raise ValueError(f"Invalid power output identifier {identifier!r}; " f"valid identifiers are {CMD_WRITE_OUTPUT.keys()}") from None def set_power_output_enabled( self, identifier: int, enabled: bool, ) -> None: """Set whether a power output is enabled.""" try: cmd = CMD_WRITE_OUTPUT[identifier] except KeyError: raise ValueError(f"Invalid power output identifier {identifier!r}; " f"valid identifiers are {CMD_WRITE_OUTPUT.keys()}") from None self._write(cmd, int(enabled)) self._output_states[identifier] = enabled def get_power_output_current(self, identifier: int) -> float: """Get the current being drawn on a power output, in amperes.""" try: cmd = CMD_READ_OUTPUT[identifier] except KeyError: raise ValueError(f"invalid power output identifier {identifier!r}; " f"valid identifiers are {CMD_READ_OUTPUT.keys()}") from None current, = struct.unpack(" None: """Queue a pitch to be played.""" if identifier != 0: raise ValueError(f"invalid piezo identifier {identifier!r}; " f"the only valid identifier is 0") duration_ms = round(duration / timedelta(milliseconds=1)) if duration_ms > 65535: raise ValueError("Maximum piezo duration is 65535ms.") data = struct.pack(" bool: """Get the state of a button.""" if identifier != 0: raise ValueError(f"invalid button identifier {identifier!r}; " f"the only valid identifier is 0") state, = struct.unpack(" None: """Halt the program until this button is pushed.""" while not self.get_button_state(identifier): sleep(0.05) def get_battery_sensor_voltage(self, identifier: int) -> float: """Get the voltage of a battery sensor.""" if identifier != 0: raise ValueError(f"invalid battery sensor identifier {identifier!r}; " f"the only valid identifier is 0") current, voltage = struct.unpack(" float: """Get the current of a battery sensor.""" if identifier != 0: raise ValueError(f"invalid battery sensor identifier {identifier!r}; " f"the only valid identifier is 0") current, voltage = struct.unpack(" bool: """Get the state of an LED.""" return self._led_states[identifier] def set_led_state(self, identifier: int, state: bool) -> None: """Set the state of an LED.""" cmds = {0: CMD_WRITE_RUNLED, 1: CMD_WRITE_ERRORLED} try: cmd = cmds[identifier] except KeyError: raise ValueError(f"invalid LED identifier {identifier!r}; valid identifiers " f"are 0 (run LED) and 1 (error LED)") from None self._write(cmd, int(state)) self._led_states[identifier] = state PK!|kj5/base_robot.py"""A base class for robots.""" import socket from j5.boards import Board class UnableToObtainLock(OSError): """Unable to obtain lock.""" pass class BaseRobot: """A base robot.""" def __new__(cls, *args, **kwargs) -> 'BaseRobot': # type: ignore """Create a new instance of the class.""" # We have to ignore some of the types here as they are unknown. obj: BaseRobot = super().__new__(cls, *args, **kwargs) # type: ignore obj._obtain_lock() return obj def make_safe(self) -> None: """Make this robot safe.""" Board.make_all_safe() def _obtain_lock(self, lock_port: int = 10653) -> None: """ Obtain a lock. This ensures that there can only be one instance of Robot at any time, which is a safety feature. """ if not hasattr(self, '_lock'): self._lock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self._lock.bind(('localhost', lock_port)) except OSError: raise UnableToObtainLock( "Unable to obtain lock. \ Are you trying to create more than one Robot object?", ) from None lock_details = self._lock.getsockname() if lock_details[1] != lock_port: raise OSError("Socket for lock is on the wrong port.") PK! j5/boards/__init__.py"""This module contains the boards that we support.""" from .board import Board, BoardGroup __all__ = ["Board", "BoardGroup"] PK!\j5/boards/board.py"""The base classes for boards and group of boards.""" import atexit from abc import ABCMeta, abstractmethod from collections import OrderedDict from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Type from j5.backends import Backend if TYPE_CHECKING: # pragma: nocover from j5.components import Component # noqa class Board(metaclass=ABCMeta): """A collection of hardware that has an implementation.""" # BOARDS is a list of currently instantiated boards. # This is useful to know so that we can make them safe in a crash. BOARDS: List['Board'] = [] def __str__(self) -> str: """A string representation of this board.""" return f"{self.name} - {self.serial}" def __new__(cls, *args, **kwargs): # type: ignore """Ensure any instantiated board is added to the boards list.""" instance = super().__new__(cls) Board.BOARDS.append(instance) return instance def __repr__(self) -> str: """A representation of this board.""" return f"<{self.__class__.__name__} serial={self.serial}>" @property @abstractmethod def name(self) -> str: """A human friendly name for this board.""" raise NotImplementedError # pragma: no cover @property @abstractmethod def serial(self) -> str: """The serial number of the board.""" raise NotImplementedError # pragma: no cover @property @abstractmethod def firmware_version(self) -> Optional[str]: """The firmware version of the board.""" raise NotImplementedError # pragma: no cover @abstractmethod def make_safe(self) -> None: """Make all components on this board safe.""" raise NotImplementedError # pragma: no cover @staticmethod @abstractmethod def supported_components() -> List[Type['Component']]: """The types of component supported by this board.""" raise NotImplementedError # pragma: no cover @staticmethod @atexit.register def make_all_safe() -> None: """Make all boards safe.""" for board in Board.BOARDS: board.make_safe() class BoardGroup: """A group of boards that can be accessed.""" def __init__(self, board_class: Type[Board], backend_class: Type[Backend]): self._board_class = board_class self._backend_class = backend_class self._boards: Dict[str, Board] = OrderedDict() self.update_boards() def update_boards(self) -> None: """Update the boards in this group to see if new boards have been added.""" self._boards: Dict[str, Board] = OrderedDict() discovered_boards = self._backend_class.discover() discovered_boards.sort(key=lambda b: b.serial) for board in discovered_boards: self._boards.update({board.serial: board}) def singular(self) -> Board: """If there is only a single board in the group, return that board.""" if len(self) == 1: return list(self._boards.values())[0] raise Exception("There is more than one or zero boards connected.") def make_safe(self) -> None: """Make all of the boards safe.""" for board in self._boards.values(): board.make_safe() def __str__(self) -> str: """A string representation of the board group.""" list_str = ', '.join(map(str, self._boards.values())) return f"Group of Boards - [{list_str}]" def __len__(self) -> int: """Get the number of boards in this group.""" return len(self._boards) def __iter__(self) -> Iterator[Board]: """ Iterate over the boards in the group. The boards are ordered lexiographically by serial number. """ return iter(self._boards.values()) def __getitem__(self, serial: str) -> Board: """Get the board from serial.""" try: return self._boards[serial] except KeyError: if type(serial) != str: raise TypeError("Serial must be a string") raise KeyError(f"Could not find a board with the serial {serial}") @property def board_class(self) -> Type[Board]: """The type of board that this group contains.""" return self._board_class @property def backend_class(self) -> Type[Backend]: """The Backend that this group uses for Boards.""" return self._backend_class @property def boards(self) -> List[Board]: """Get an unordered list of boards in this group.""" return list(self._boards.values()) PK!p''j5/boards/sr/__init__.py"""Boards made by Student Robotics.""" PK!9aMj5/boards/sr/v4/__init__.py"""Boards in the v4 series of Student Robotics boards.""" from .power_board import PowerBoard, PowerOutputGroup, PowerOutputPosition __all__ = [ 'PowerBoard', 'PowerOutputGroup', 'PowerOutputPosition', ] PK!d穦 j5/boards/sr/v4/power_board.py"""Classes for the SR v4 Power Board.""" from enum import Enum from time import sleep from typing import TYPE_CHECKING, List, Mapping, Optional, cast from j5.backends import Backend from j5.boards import Board from j5.components import ( LED, BatterySensor, Button, Piezo, PowerOutput, PowerOutputGroup, ) if TYPE_CHECKING: # pragma: no cover from j5.components import ( # noqa: F401 Component, ButtonInterface, PowerOutputInterface, PiezoInterface, BatterySensorInterface, LEDInterface, ) from typing import Type # noqa: F401 class PowerOutputPosition(Enum): """ A mapping of name to number of the PowerBoard outputs. The numbers here are the same as used in wire communication with the PowerBoard. """ H0 = 0 H1 = 1 L0 = 2 L1 = 3 L2 = 4 L3 = 5 class PowerBoard(Board): """Student Robotics v4 Power Board.""" def __init__(self, serial: str, backend: Backend): self._serial = serial self._backend = backend self._outputs: Mapping[PowerOutputPosition, PowerOutput] = { output: PowerOutput( output.value, self, cast("PowerOutputInterface", self._backend), ) for output in PowerOutputPosition # Note that in Python 3, Enums are ordered. } self._output_group = PowerOutputGroup(self._outputs) self._piezo = Piezo(0, self, cast("PiezoInterface", self._backend)) self._start_button = Button(0, self, cast("ButtonInterface", self._backend)) self._battery_sensor = BatterySensor( 0, self, cast("BatterySensorInterface", self._backend), ) self._run_led = LED(0, self, cast("LEDInterface", self._backend)) self._error_led = LED(1, self, cast("LEDInterface", self._backend)) @property def name(self) -> str: """Get a human friendly name for this board.""" return "Student Robotics v4 Power Board" @property def serial(self) -> str: """Get the serial number.""" return self._serial @property def firmware_version(self) -> Optional[str]: """Get the firmware version of the board.""" return self._backend.get_firmware_version() @property def outputs(self) -> PowerOutputGroup: """Get the power outputs.""" return self._output_group @property def piezo(self) -> Piezo: """Get the piezo sounder.""" return self._piezo @property def start_button(self) -> Button: """Get the start button.""" return self._start_button @property def battery_sensor(self) -> BatterySensor: """Get the battery sensor.""" return self._battery_sensor def make_safe(self) -> None: """Make this board safe.""" self._output_group.power_off() def wait_for_start_flash(self) -> None: """Wait for the start button to be pressed and flash.""" counter = 0 led_state = False while not self.start_button.is_pressed: if counter % 6 == 0: led_state = not led_state self._run_led.state = led_state sleep(0.05) counter += 1 @staticmethod def supported_components() -> List["Type[Component]"]: """List the types of components supported by this board.""" return [PowerOutput, Piezo, Button, BatterySensor, LED] PK!gcUj5/components/__init__.py"""This module contains components, which are the smallest logical element of hardware.""" from .battery_sensor import BatterySensor, BatterySensorInterface from .button import Button, ButtonInterface from .component import Component, Interface, NotSupportedByHardwareError from .gpio_pin import GPIOPin, GPIOPinInterface, GPIOPinMode from .led import LED, LEDInterface from .motor import Motor, MotorInterface from .piezo import Piezo, PiezoInterface from .power_output import PowerOutput, PowerOutputGroup, PowerOutputInterface from .servo import Servo, ServoInterface __all__ = [ "BatterySensor", "BatterySensorInterface", "Button", "ButtonInterface", "Component", "GPIOPin", "GPIOPinInterface", "GPIOPinMode", "Interface", "LED", "LEDInterface", "Motor", "MotorInterface", "NotSupportedByHardwareError", "Piezo", "PiezoInterface", "PowerOutput", "PowerOutputInterface", "PowerOutputGroup", "Servo", "ServoInterface", ] PK!@C  j5/components/battery_sensor.py"""Classes for Battery Sensing Components.""" from abc import abstractmethod from typing import Type from j5.boards import Board from j5.components.component import Component, Interface class BatterySensorInterface(Interface): """An interface containing the methods required to read data from a BatterySensor.""" @abstractmethod def get_battery_sensor_voltage(self, identifier: int) -> float: """Get the voltage of a battery sensor.""" raise NotImplementedError # pragma: no cover @abstractmethod def get_battery_sensor_current(self, identifier: int) -> float: """Get the current of a battery sensor.""" raise NotImplementedError # pragma: no cover class BatterySensor(Component): """A sensor capable of monitoring a battery.""" def __init__( self, identifier: int, board: Board, backend: BatterySensorInterface, ) -> None: self._board = board self._backend = backend self._identifier = identifier @staticmethod def interface_class() -> Type[BatterySensorInterface]: """Get the interface class that is required to use this component.""" return BatterySensorInterface @property def voltage(self) -> float: """Get the voltage of the battery sensor.""" return self._backend.get_battery_sensor_voltage(self._identifier) @property def current(self) -> float: """Get the current of the battery sensor.""" return self._backend.get_battery_sensor_current(self._identifier) PK!?|zzj5/components/button.py"""Classes for Button.""" from abc import abstractmethod from typing import Type from j5.boards import Board from j5.components.component import Component, Interface class ButtonInterface(Interface): """An interface containing the methods required for a button.""" @abstractmethod def get_button_state(self, identifier: int) -> bool: """Set the state of a button.""" raise NotImplementedError # pragma: no cover @abstractmethod def wait_until_button_pressed(self, identifier: int) -> None: """Halt the program until this button is pushed.""" raise NotImplementedError # pragma: no cover class Button(Component): """A button.""" def __init__(self, identifier: int, board: Board, backend: ButtonInterface) -> None: self._board = board self._backend = backend self._identifier = identifier @staticmethod def interface_class() -> Type[ButtonInterface]: """Get the interface class that is required to use this component.""" return ButtonInterface @property def is_pressed(self) -> bool: """Get the current pushed state of the button.""" return self._backend.get_button_state(self._identifier) def wait_until_pressed(self) -> None: """Halt the program until this button is pushed.""" self._backend.wait_until_button_pressed(self._identifier) PK!Jj5/components/component.py"""Base classes for components.""" from abc import ABCMeta, abstractmethod from typing import Type class Interface(metaclass=ABCMeta): """A base class for interfaces to inherit from.""" class Component(metaclass=ABCMeta): """A component is the smallest logical part of some hardware.""" @staticmethod @abstractmethod def interface_class() -> Type[Interface]: """Get the interface class that is required to use this component.""" raise NotImplementedError # pragma: no cover class NotSupportedByHardwareError(Exception): """This is thrown when hardware does not support the action that is attempted.""" pass PK!| j5/components/gpio_pin.py"""Classes for GPIO Pins.""" from abc import abstractmethod from enum import IntEnum from typing import List, Optional, Type from j5.boards import Board from j5.components.component import ( Component, Interface, NotSupportedByHardwareError, ) class BadGPIOPinModeError(Exception): """The pin is not in the correct mode.""" pass class GPIOPinMode(IntEnum): """Hardware modes that a GPIO pin can be set to.""" DIGITAL_INPUT = 0 #: The digital state of the pin can be read DIGITAL_INPUT_PULLUP = 1 #: Same as DIGITAL_INPUT but internal pull-up is enabled DIGITAL_INPUT_PULLDOWN = 2 #: Same as DIGITAL_INPUT but internal pull-down is enabled DIGITAL_OUTPUT = 3 #: The digital state of the pin can be set. ANALOGUE_INPUT = 4 #: The analogue voltage of the pin can be read. ANALOGUE_OUTPUT = 5 #: The analogue voltage of the pin can be set using a DAC. PWM_OUTPUT = 6 #: A PWM output signal can be created on the pin. class GPIOPinInterface(Interface): """An interface containing the methods required for a GPIO Pin.""" @abstractmethod def set_gpio_pin_mode(self, identifier: int, pin_mode: GPIOPinMode, ) -> None: """Set the hardware mode of a GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod def get_gpio_pin_mode(self, identifier: int) -> GPIOPinMode: """Get the hardware mode of a GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod def write_gpio_pin_digital_state(self, identifier: int, state: bool, ) -> None: """Write to the digital state of a GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod def get_gpio_pin_digital_state(self, identifier: int) -> bool: """Get the last written state of the GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod def read_gpio_pin_digital_state(self, identifier: int) -> bool: """Read the digital state of the GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod def read_gpio_pin_analogue_value(self, identifier: int) -> float: """Read the scaled analogue value of the GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod 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.""" raise NotImplementedError # pragma: nocover @abstractmethod 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.""" raise NotImplementedError # pragma: nocover class GPIOPin(Component): """A GPIO Pin.""" def __init__( self, identifier: int, board: Board, backend: GPIOPinInterface, supported_modes: List[GPIOPinMode] = [GPIOPinMode.DIGITAL_OUTPUT], initial_mode: Optional[GPIOPinMode] = None, ) -> None: self._board = board self._backend = backend self._identifier = identifier self._supported_modes = supported_modes if len(supported_modes) < 1: raise ValueError("A GPIO pin must support at least one GPIOPinMode.") if initial_mode is None: # If no initial mode is set, choose the first supported mode. initial_mode = self._supported_modes[0] self.mode = initial_mode @staticmethod def interface_class() -> Type[GPIOPinInterface]: """Get the interface class that is required to use this component.""" return GPIOPinInterface def _require_pin_modes(self, pin_modes: List[GPIOPinMode]) -> None: """Ensure that this pin is in the specified hardware mode.""" if not any(self.mode == mode for mode in pin_modes) and not len(pin_modes) == 0: raise BadGPIOPinModeError( f"Pin {self._identifier} needs to be in one of {pin_modes}", ) @property def mode(self) -> GPIOPinMode: """Get the hardware mode of this pin.""" return self._backend.get_gpio_pin_mode(self._identifier) @mode.setter def mode(self, pin_mode: GPIOPinMode) -> None: """Set the hardware mode of this pin.""" if pin_mode not in self._supported_modes: raise NotSupportedByHardwareError( f"Pin {self._identifier} on {str(self._board)} \ does not support {str(pin_mode)}.", ) self._backend.set_gpio_pin_mode(self._identifier, pin_mode) @property def digital_state(self) -> bool: """Get the digital state of the pin.""" self._require_pin_modes([ GPIOPinMode.DIGITAL_OUTPUT, GPIOPinMode.DIGITAL_INPUT, GPIOPinMode.DIGITAL_INPUT_PULLUP, GPIOPinMode.DIGITAL_INPUT_PULLDOWN], ) # Behave differently depending on the hardware mode. if self.mode is GPIOPinMode.DIGITAL_OUTPUT: return self._backend.get_gpio_pin_digital_state(self._identifier) return self._backend.read_gpio_pin_digital_state(self._identifier) @digital_state.setter def digital_state(self, state: bool) -> None: """Set the digital state of the pin.""" self._require_pin_modes([GPIOPinMode.DIGITAL_OUTPUT]) self._backend.write_gpio_pin_digital_state(self._identifier, state) @property def analogue_value(self) -> float: """Get the scaled analogue reading of the pin.""" self._require_pin_modes([GPIOPinMode.ANALOGUE_INPUT]) return self._backend.read_gpio_pin_analogue_value(self._identifier) @analogue_value.setter def analogue_value(self, new_value: float) -> None: """Set the analogue value of the pin.""" self._require_pin_modes([ GPIOPinMode.ANALOGUE_OUTPUT, GPIOPinMode.PWM_OUTPUT, ]) if new_value < 0 or new_value > 1: raise ValueError("An analogue pin value must be between 0 and 1.") if self.mode is GPIOPinMode.ANALOGUE_OUTPUT: self._backend.write_gpio_pin_dac_value( self._identifier, new_value, ) else: # We must be a PWM_OUTPUT self._backend.write_gpio_pin_pwm_value( self._identifier, new_value, ) PK!ku9ffj5/components/led.py"""Classes for the LED support.""" from abc import abstractmethod from typing import Type from j5.boards import Board from j5.components.component import Component, Interface class LEDInterface(Interface): """An interface containing the methods required to control an LED.""" @abstractmethod def get_led_state(self, identifier: int) -> bool: """Get the state of an LED.""" raise NotImplementedError # pragma: no cover @abstractmethod def set_led_state(self, identifier: int, state: bool) -> None: """Set the state of an LED.""" raise NotImplementedError # pragma: no cover class LED(Component): """A standard Light Emitting Diode.""" def __init__(self, identifier: int, board: Board, backend: LEDInterface) -> None: self._board = board self._backend = backend self._identifier = identifier @staticmethod def interface_class() -> Type[LEDInterface]: """Get the interface class that is required to use this component.""" return LEDInterface @property def state(self) -> bool: """Get the current state of the LED.""" return self._backend.get_led_state(self._identifier) @state.setter def state(self, new_state: bool) -> None: """Set the state of the LED.""" self._backend.set_led_state(self._identifier, new_state) PK!88j5/components/motor.py"""Classes for Motor support.""" from abc import abstractmethod from enum import Enum from typing import Type, Union from j5.boards import Board from j5.components.component import Component, Interface class MotorSpecialState(Enum): """An enum of the special states that a motor can be set to.""" COAST = 0 BRAKE = 1 MotorState = Union[float, MotorSpecialState] class MotorInterface(Interface): """An interface containing the methods required to control a motor board.""" @abstractmethod def get_motor_state(self, identifier: int) -> MotorState: """Get the motor state.""" raise NotImplementedError # pragma: no cover @abstractmethod def set_motor_state(self, identifier: int, power: MotorState) -> None: """Set the motor state.""" raise NotImplementedError # pragma: no cover class Motor(Component): """Brushed DC motor output.""" def __init__( self, identifier: int, board: Board, backend: MotorInterface, ) -> None: self._board = board self._backend = backend self._identifier = identifier @staticmethod def interface_class() -> Type[Interface]: """Get the interface class that is required to use this component.""" return MotorInterface @property def state(self) -> MotorState: """Get the current state of this output.""" return self._backend.get_motor_state(self._identifier) @state.setter def state(self, new_state: MotorState) -> None: """Set the current state of this output.""" if isinstance(new_state, float): if new_state < -1 or new_state > 1: raise ValueError("Motor speed must be between 1 and -1.") self._backend.set_motor_state(self._identifier, new_state) PK! x j5/components/piezo.py"""Classes for Piezo support.""" from abc import abstractmethod from datetime import timedelta from enum import IntEnum from typing import Generator, Type, Union from j5.boards import Board from j5.components.component import Component, Interface class Note(IntEnum): """An enumeration of notes. An enumeration of notes from scientific pitch notation and their related frequencies in Hz. """ C6 = 1047 D6 = 1175 E6 = 1319 F6 = 1397 G6 = 1568 A6 = 1760 B6 = 1976 C7 = 2093 D7 = 2349 E7 = 2637 F7 = 2794 G7 = 3136 A7 = 3520 B7 = 3951 C8 = 4186 def __reverse__(self) -> Generator['Note', None, None]: # Type is ignored because of an open bug within mypy # https://github.com/python/typeshed/issues/1590 # https://github.com/python/typeshed/issues/1595 yield from reversed(self.__members__.items()) # type: ignore Pitch = Union[int, Note] class PiezoInterface(Interface): """An interface containing the methods required to control an piezo.""" @abstractmethod def buzz(self, identifier: int, duration: timedelta, frequency: int) -> None: """Queue a pitch to be played.""" raise NotImplementedError # pragma: no cover class Piezo(Component): """A standard piezo.""" def __init__(self, identifier: int, board: Board, backend: PiezoInterface) -> None: self._board = board self._backend = backend self._identifier = identifier @staticmethod def interface_class() -> Type[PiezoInterface]: """Get the interface class that is required to use this component.""" return PiezoInterface def buzz(self, duration: timedelta, pitch: Pitch) -> None: """Queue a note to be played.""" self.verify_pitch(pitch) self.verify_duration(duration) self._backend.buzz(self._identifier, duration, pitch) @staticmethod def verify_pitch(pitch: Pitch) -> None: """Verify that a pitch is valid.""" # Verify that the type is correct. pitch_is_int = type(pitch) is int pitch_is_note = type(pitch) is Note if not (pitch_is_int or pitch_is_note): raise TypeError("Pitch must be int or Note") # Verify the length of the pitch is non-zero if pitch < 0: raise ValueError("Frequency must be greater than zero") @staticmethod def verify_duration(duration: timedelta) -> None: """Verify that a duration is valid.""" if not isinstance(duration, timedelta): raise TypeError("Duration must be of type datetime.timedelta") if duration < timedelta(seconds=0): raise ValueError("Duration must be greater than or equal to zero.") PK!T<  j5/components/power_output.py"""Classes for supporting toggleable power output channels.""" from abc import abstractmethod from typing import Iterator, Mapping, Type, TypeVar from j5.boards import Board from j5.components.component import Component, Interface class PowerOutputInterface(Interface): """An interface containing the methods required to control a power output channel.""" @abstractmethod def get_power_output_enabled(self, identifier: int) -> bool: """Get whether a power output is enabled.""" raise NotImplementedError # pragma: no cover @abstractmethod def set_power_output_enabled( self, identifier: int, enabled: bool, ) -> None: """Set whether a power output is enabled.""" raise NotImplementedError # pragma: no cover @abstractmethod def get_power_output_current(self, identifier: int) -> float: """Get the current being drawn on a power output, in amperes.""" raise NotImplementedError # pragma: no cover class PowerOutput(Component): """ A power output channel. It can be enabled/disabled, and the current being drawn on this channel can be measured. """ def __init__( self, identifier: int, board: Board, backend: PowerOutputInterface, ) -> None: self._identifier = identifier self._board = board self._backend = backend @staticmethod def interface_class() -> Type[PowerOutputInterface]: """Get the interface class that is required to use this component.""" return PowerOutputInterface @property def is_enabled(self) -> bool: """Get whether the output is enabled.""" return self._backend.get_power_output_enabled(self._identifier) @is_enabled.setter def is_enabled(self, new_state: bool) -> None: """Set whether the output is enabled.""" self._backend.set_power_output_enabled(self._identifier, new_state) @property def current(self) -> float: """Get the current being drawn on this power output, in amperes.""" return self._backend.get_power_output_current(self._identifier) T = TypeVar('T') class PowerOutputGroup: """A group of PowerOutputs.""" def __init__(self, outputs: Mapping[T, PowerOutput]): self._outputs = outputs def power_on(self) -> None: """Enable all outputs in the group.""" for output in self._outputs.values(): output.is_enabled = True def power_off(self) -> None: """Disable all outputs in the group.""" for output in self._outputs.values(): output.is_enabled = False def __getitem__(self, index: T) -> PowerOutput: """Get an output using list notation.""" return self._outputs[index] def __iter__(self) -> Iterator[PowerOutput]: """ Iterate over the outputs in the group. The outputs are in no particular order. """ return iter(self._outputs.values()) def __len__(self) -> int: """Get the number of outputs in the group.""" return len(self._outputs) PK! r|uj5/components/servo.py"""Classes for supporting Servomotors.""" from abc import abstractmethod from typing import Type, Union from j5.boards import Board from j5.components.component import Component, Interface # A servo can be powered down by setting its position to None. ServoPosition = Union[float, None] class ServoInterface(Interface): """An interface containing the methods required to control a Servo.""" @abstractmethod def get_servo_position(self, identifier: int) -> ServoPosition: """Get the position of a Servo.""" raise NotImplementedError # pragma: no cover @abstractmethod def set_servo_position( self, identifier: int, position: ServoPosition, ) -> None: """Set the position of a Servo.""" raise NotImplementedError # pragma: no cover class Servo(Component): """A standard servomotor.""" def __init__(self, identifier: int, board: Board, backend: ServoInterface) -> None: self._board = board self._backend = backend self._identifier = identifier @staticmethod def interface_class() -> Type[ServoInterface]: """Get the interface class that is required to use this component.""" return ServoInterface @property def position(self) -> ServoPosition: """Get the current position of the Servo.""" return self._backend.get_servo_position(self._identifier) @position.setter def position(self, new_position: ServoPosition) -> None: """Set the position of the Servo.""" if new_position is not None: if not -1 <= new_position <= 1: raise ValueError self._backend.set_servo_position(self._identifier, new_position) PK!XX j5/py.typed# Marker file for PEP 561. j5 uses inline type hints. See PEP 484 for more information. PK!QW00j5-0.2.0.dist-info/LICENSEMIT License Copyright (c) 2019 j5 contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTUj5-0.2.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H]j5-0.2.0.dist-info/METADATAX]o:}ׯ`qI7ݍM[!(bJl$RKRvR$Ey013<9s yYH/&#:=I>ȚFy}zNi[ҮG_dƫ܉O4hd  SK *LRe*'`r3K.[0ުƺjpot*}Ln?0?.N_~?q%SsET&K:1S1U{,68˦fIdžJbv+&'kJ+K]HMd?kyܙF]؋/_IK{)q2+"[mEn?кaPpEŮo#Ѭ[?.N{F|}B%£B=[jSzR*h475e%[p,B9!XN_WceƓyx\lnYq~]!ǿ#0/(T-=0EInx~Fgg2$#CE/$L?t# TBU \O=;FE3rD80,]ml{hlHzo'd:QE!Ae";U—H2?1gZSA?L^_L''y}>XSU/v$%8nE2haVO/ r7I2v>S5a-I ^f?gV[v ad_Azw9qco-3< Oglr%M+v>Pzldrh43v:2[?EH(9 F1̓\]nN]i##^;EDR, wẃ+ϲrVׁ%qn l:|dz|!)ĜX%f PK!H j5-0.2.0.dist-info/RECORD}ɶXy> ܠo9Phi/#*נrs ~u+0d[z.lʶx &&>kB15ts0ʤ=PDϋ)y ː#FWGii$ Ç#Û>ee;gК>6uOX f 'o\6C[%{SPJA֟TO9Moz`ef~%\tF8R\=.8ZIek]r3O<<}RNACI<2g>LKP̽!7tp{o72Lȋ2@ݵKa!n'!zBe)jY9MS?:{YK,vK(T),ѴtN"p X*3m<}{A&\ /8.Y6Ԡ쵫Õ`' 6=ဢ擇F ؀6WO랂K.'ImqHl(\qq٣.S=bP7&^`3CۣʑW&G=`~bH MZ /$A} ,Gh!)Sws/MAfMϮrZjj3YŸf^ޱ㩱3iFz,[t\␳w!oo~op-k0vɳ -F2߹I9{n]#$"iNjloˆ~ZBZgxQNDgd<)Ks% d&i(-Lv^\ |Swކ۟Ba0ICo.Xٟ1r 5Y*gܾ*f*,|;Sk qtql7| ˱},* 9ÍJk=V"~|#oUF?|!Q^i$u2Gy}]]>R *`OCX#='X@!V{ a0S;fcv~HpltO(&J}no=PI,9ײee'uᝨ"ڋdq^9['[UYi:JB8B\0%юnw>_Nc7'W3* ōqVc*Noz;D~~X3zW<⤢.[QJ *0̺k\$})6{<], v3SgDο(}!_W#7iC9A77:X\ !WΎuQkp Dz~].$٣hs#Zyv{! N3W@%@{&X X{ caz,T\qO\J{ENPK!1j5/__init__.pyPK!\j5/backends/__init__.pyPK!,pj5/backends/backend.pyPK!OPDj5/backends/console/__init__.pyPK!gc??j5/backends/console/env.pyPK!ۭVGG"j5/backends/console/sr/__init__.pyPK!QQ%j5/backends/console/sr/v4/__init__.pyPK!(0j5/backends/console/sr/v4/power_board.pyPK!%vv #3j5/backends/hardware/__init__.pyPK!Y=3j5/backends/hardware/env.pyPK!AHH#4j5/backends/hardware/sr/__init__.pyPK!'RR&5j5/backends/hardware/sr/v4/__init__.pyPK!V)I;';')5j5/backends/hardware/sr/v4/power_board.pyPK!|k0]j5/base_robot.pyPK! bj5/boards/__init__.pyPK!\cj5/boards/board.pyPK!p''uj5/boards/sr/__init__.pyPK!9aM:vj5/boards/sr/v4/__init__.pyPK!d穦 Mwj5/boards/sr/v4/power_board.pyPK!gcU/j5/components/__init__.pyPK!@C  Wj5/components/battery_sensor.pyPK!?|zzj5/components/button.pyPK!JNj5/components/component.pyPK!| j5/components/gpio_pin.pyPK!ku9ffֲj5/components/led.pyPK!88nj5/components/motor.pyPK! x ڿj5/components/piezo.pyPK!T<  j5/components/power_output.pyPK! r|uMj5/components/servo.pyPK!XX Rj5/py.typedPK!QW00j5-0.2.0.dist-info/LICENSEPK!HڽTU;j5-0.2.0.dist-info/WHEELPK!H]j5-0.2.0.dist-info/METADATAPK!H }j5-0.2.0.dist-info/RECORDPK""