PK!_ j5/__init__.py"""j5 Robotics API.""" from .base_robot import BaseRobot from .boards import BoardGroup __all__ = ["BoardGroup", "BaseRobot", "__version__", "__version_short__"] __version__ = "0.0.2" __version_short__ = "0.0.2" PK!Lj5/backends/__init__.py"""Backend classes.""" from .base import Backend, CommunicationError, Environment __all__ = ["Backend", "CommunicationError", "Environment"] PK! 5j5/backends/base.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. """ @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, board: 'Board') -> 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!Ommj5/backends/dummy/__init__.py"""Backends for the dummy environment.""" from .env import DummyEnvironment __all__ = ["DummyEnvironment"] PK!Smj5/backends/dummy/demo.py"""Dummy Backends for the demo boards.""" from typing import List, Optional from j5.backends import Backend from j5.backends.dummy.env import DummyEnvironment from j5.boards import Board from j5.boards.j5.demo import DemoBoard from j5.components import LEDInterface class DemoBoardDummyBackend(LEDInterface, Backend): """The dummy implementation of the DemoBoard.""" environment = DummyEnvironment board = DemoBoard def set_led_state(self, board: Board, identifier: int, state: bool) -> None: """Set the state of an LED.""" print(f"Set LED {str(identifier)} to {str(state)} on {str(board)}") def get_led_state(self, board: Board, identifier: int) -> bool: """Get the state of an LED.""" return False def get_firmware_version(self, board: 'Board') -> Optional[str]: """Get the firmware version of the board.""" return None @classmethod def discover(cls) -> List[Board]: """Discover boards available on this backend.""" return [] PK!Nvvj5/backends/dummy/env.py"""The Dummy Environment.""" from j5.backends import Environment DummyEnvironment = Environment("DummyEnvironment") 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!\4u(u()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, ) from j5.components.piezo import Note, Pitch 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) -> int: """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, board: 'Board') -> Optional[str]: """Get the firmware version reported by the board.""" return str(self.firmware_version) def get_power_output_enabled(self, board: Board, 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, board: Board, 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, board: Board, 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") if isinstance(pitch, Note): frequency = pitch.value else: frequency = pitch 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(board, identifier): sleep(0.05) def get_battery_sensor_voltage(self, board: Board, 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, board: Board, 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!P5ij5/boards/__init__.py"""This module contains the boards that we support.""" from .base import Board, BoardGroup __all__ = ["Board", "BoardGroup"] PK!+355j5/boards/base.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: 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 @abstractmethod def discover(backend: Backend) -> List['Board']: """Detect and return a list of boards of this type.""" 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: Board, backend: Backend): self.board_class: Board = board self._backend: Backend = backend 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.board_class.discover(self._backend) discovered_boards.sort(key=lambda board: board.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 __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}") PK! oTTj5/boards/j5/__init__.py"""Boards developed by j5.""" from .demo import DemoBoard __all__ = ["DemoBoard"] PK!O@j5/boards/j5/demo.py"""Classes for demonstration purposes.""" from typing import TYPE_CHECKING, List, Optional, cast from j5.backends import Backend, Environment from j5.boards import Board from j5.components.led import LED if TYPE_CHECKING: from j5.components import Component # noqa from typing import Type # noqa from j5.components.led import LEDInterface # noqa class DemoBoard(Board): """A board for demo purposes, containing 3 LEDs.""" def __init__(self, serial: str, environment: Environment): self._environment = environment self._backend = environment.get_backend(self.__class__)() self._serial = serial self._leds = [LED(n, self, cast('LEDInterface', self._backend)) for n in range(0, 3)] @property def name(self) -> str: """Get a human friendly name for this board.""" return "Demo 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 this board.""" return self._backend.get_firmware_version(self) def make_safe(self) -> None: """Make this board safe.""" pass @staticmethod def supported_components() -> List['Type[Component]']: """List the types of component supported by this Board.""" return [LED] @staticmethod def discover(backend: Backend) -> List[Board]: """Detect all connected boards of this type and return them.""" return [DemoBoard(str(n), backend.environment) for n in range(0, 3)] @property def leds(self) -> List[LED]: """Get the leds on the board.""" return self._leds 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!nEEj5/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(self) @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] @staticmethod def discover(backend: Backend) -> List["Board"]: """Detect all connected power boards.""" return backend.discover() PK!j5/components/__init__.py"""This module contains components, which are the smallest logical element of hardware.""" from .base import Component, Interface, NotSupportedByHardwareError from .battery_sensor import BatterySensor, BatterySensorInterface from .button import Button, ButtonInterface from .gpio_pin import GPIOPin, GPIOPinInterface, GPIOPinMode from .led import LED, LEDInterface 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", "NotSupportedByHardwareError", "Piezo", "PiezoInterface", "PowerOutput", "PowerOutputInterface", "PowerOutputGroup", "Servo", "ServoInterface", ] PK!Jj5/components/base.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!,}77j5/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 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, board: Board, identifier: int) -> float: """Get the voltage of a battery sensor.""" raise NotImplementedError # pragma: no cover @abstractmethod def get_battery_sensor_current(self, board: Board, 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._board, self._identifier) @property def current(self) -> float: """Get the current of the battery sensor.""" return self._backend.get_battery_sensor_current(self._board, self._identifier) PK!_؝¦j5/components/button.py"""Classes for Button.""" from abc import abstractmethod from typing import Type from j5.boards import Board from j5.components import Component, Interface class ButtonInterface(Interface): """An interface containing the methods required for a button.""" @abstractmethod def get_button_state(self, board: Board, identifier: int) -> bool: """Set the state of a button.""" raise NotImplementedError # pragma: no cover @abstractmethod def wait_until_button_pressed(self, board: Board, 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._board, self._identifier) def wait_until_pressed(self) -> None: """Halt the program until this button is pushed.""" self._backend.wait_until_button_pressed(self._board, self._identifier) PK!Yj5/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 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, board: Board, 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, board: Board, identifier: int) -> GPIOPinMode: """Get the hardware mode of a GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod def write_gpio_pin_digital_state(self, board: Board, 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, board: Board, identifier: int) -> bool: """Get the last written state of the GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod def read_gpio_pin_digital_state(self, board: Board, identifier: int) -> bool: """Read the digital state of the GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod def read_gpio_pin_analogue_value(self, board: Board, identifier: int) -> float: """Read the scaled analogue value of the GPIO pin.""" raise NotImplementedError # pragma: nocover @abstractmethod def write_gpio_pin_dac_value( self, board: Board, 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, board: Board, 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._board, 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._board, 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._board, self._identifier) return self._backend.read_gpio_pin_digital_state(self._board, 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._board, 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._board, 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._board, self._identifier, new_value, ) else: # We must be a PWM_OUTPUT self._backend.write_gpio_pin_pwm_value( self._board, self._identifier, new_value, ) PK!ej5/components/led.py"""Classes for the LED support.""" from abc import abstractmethod from typing import Type from j5.boards import Board from j5.components import Component, Interface class LEDInterface(Interface): """An interface containing the methods required to control an LED.""" @abstractmethod def get_led_state(self, board: Board, identifier: int) -> bool: """Get the state of an LED.""" raise NotImplementedError # pragma: no cover @abstractmethod def set_led_state(self, board: Board, 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._board, self._identifier) @state.setter def state(self, new_state: bool) -> None: """Set the state of the LED.""" self._backend.set_led_state(self._board, self._identifier, new_state) PK!.ȭ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 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, board: Board, identifier: int, duration: timedelta, pitch: Pitch) -> 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.""" if isinstance(pitch, int): frequency = pitch elif isinstance(pitch, Note): frequency = pitch.value else: raise TypeError("Pitch must be of either type int or Note") if frequency < 0: raise ValueError("Pitch must be greater than zero") else: self._backend.buzz(self._board, self._identifier, duration, pitch) PK!_=e e 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 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, board: Board, identifier: int) -> bool: """Get whether a power output is enabled.""" raise NotImplementedError # pragma: no cover @abstractmethod def set_power_output_enabled( self, board: Board, identifier: int, enabled: bool, ) -> None: """Set whether a power output is enabled.""" raise NotImplementedError # pragma: no cover @abstractmethod def get_power_output_current(self, board: Board, 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._board, 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._board, 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._board, 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!(  j5/components/servo.py"""Classes for supporting Servomotors.""" from abc import abstractmethod from typing import Type, Union from j5.boards import Board from j5.components 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, board: Board, identifier: int) -> ServoPosition: """Get the position of a Servo.""" raise NotImplementedError # pragma: no cover @abstractmethod def set_servo_position( self, board: Board, 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._board, 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._board, 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.1.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.1.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!Hv6~j5-0.1.0.dist-info/METADATAX]o:}ׯ`qI7ݍM[!(bJl$RKRvR$Ey013<9s yYH/&#:=I>ȚFy}zɴki#sqk2UIrmj4ƍo%Y&w2{v0%_ˋhoUzc]|@TH8k7:y>&VYrOkGܸΩ"D}KKLSboĩ˪Y=|NeXtzcCVzK1];Ou]PC~5u.[$]&Oi!CЉ*D .SPT=. 0Ȭ*DǕ9nȉ3)swC&z}q &㿼>̊;Loi"Z0+]ǧh_ėB9›$};t)RFy+sD pਚv/Kp}ӆ.6S~_e~z?)+^ |q^\-,e,.ۛ%${ $_ۆthh3Վs3tOG ;0 817'޳oӏpVUŦwN ;`t\g|Y GN_=62W94 ;mԢ$VWƱ)=>8q -7,Q/PSaq,a2k?.*:eE~ CGPd%Tq/Ҏȉ0i#XtG$f# a$+͂}t9"1{?0}7yv7dp?N>}\UƱ›Sk4} _{)fbgpA``Glq.iNP.w/ݝ"")J;_VgYHYplY7?^Qpu@>c2=DZebNw 3hPK!H2^? j5-0.1.0.dist-info/RECORD}ɖVy7 hN 樀4ǺIL|k Gu>DGB-H)IdNv, q`\H#Y@+ HJ!M&X8x pI܃걧L#cfdҜ_Wwoba"P becUϑ_LŐ]Nݑ9FbkokQG"  xv u5 F1$)7y+a exy6[W"I 7Y$IrUH?v*wNsJ zR,'b݉Vwv ḳ dZ܀aȼDc)O;\}K;e%',Ê&!cjԘB botQ<}K}Z}ʁAޕiZ:L6EEw3?_Q$"%hY-#B+ Y/hkͳO߯9v)R;ֆ)V2]dI Z:j{}&5?,[cQUmg7oP볛"oP?|نVؙsŃj9LY#j)8@ VB5 gz.CSȒq;<.i#86 {*<06i tAÔe1*=<<,lr)Ugȟ7mA=Kr4ɛӎEmYYe]9$)8ٞ  3' w.8M4TظPhk|I"0nw!BÆtkԃov9.̾So\Mc#P_jEeʹKO 6#(