"""Classes for Piezo support."""
from abc import abstractmethod
from datetime import timedelta
from enum import Enum
from typing import Optional, Type, Union
from j5.components.component import Component, Interface
class Note(float, Enum):
"""An enumeration of notes.
An enumeration of notes from scientific pitch
notation and their related frequencies in Hz.
"""
C6 = 1047.0
D6 = 1174.7
E6 = 1318.5
F6 = 1396.9
G6 = 1568.0
A6 = 1760.0
B6 = 1975.5
C7 = 2093.0
D7 = 2349.3
E7 = 2637.0
F7 = 2793.8
G7 = 3136.0
A7 = 3520.0
B7 = 3951.1
C8 = 4186.0
Pitch = Union[int, float, Note]
class PiezoInterface(Interface):
"""An interface containing the methods required to control an piezo."""
@abstractmethod
def buzz(
self,
identifier: int,
duration: timedelta,
frequency: float,
blocking: bool,
) -> None:
"""
Queue a pitch to be played.
A buzz can either be blocking, or non-blocking.
If a backend does not support a non-blocking buzz, it will
raise a :class:`j5.components.NotSupportedByComponentError`.
:param identifier: piezo identifier to play pitch on.
:param duration: duration of the tone.
:param frequency: Pitch of the tone in Hz.
:param blocking: whether the code waits for the buzz
"""
raise NotImplementedError # pragma: no cover
[docs]class Piezo(Component):
"""A standard piezo."""
def __init__(
self,
identifier: int,
backend: PiezoInterface,
*,
default_blocking: bool = False,
) -> None:
self._backend = backend
self._identifier = identifier
self._default_blocking = default_blocking
@staticmethod
def interface_class() -> Type[PiezoInterface]:
"""
Get the interface class that is required to use this component.
:returns: interface class.
"""
return PiezoInterface
@property
def identifier(self) -> int:
"""
An integer to identify the component on a board.
:returns: component identifier.
"""
return self._identifier
[docs] def buzz(
self,
duration: Union[int, float, timedelta],
pitch: Pitch,
*,
blocking: Optional[bool] = None,
) -> None:
"""
Queue a note to be played.
Float and integer durations are measured in seconds.
A buzz can either be blocking, or non-blocking and will fall back to
a default if it is not specified.
:param duration: length to play for
:param pitch: pitch of buzz.
:param blocking: whether the code waits for the buzz
"""
if isinstance(duration, float) or isinstance(duration, int):
duration = timedelta(seconds=duration)
if type(pitch) is int:
pitch = float(pitch)
self.verify_pitch(pitch)
self.verify_duration(duration)
self._backend.buzz(
self._identifier,
duration,
pitch,
blocking or self._default_blocking, # Fallback to component default.
)
[docs] @staticmethod
def verify_pitch(pitch: Pitch) -> None:
"""
Verify that a pitch is valid.
:param pitch: pitch to validate.
:raises TypeError: Pitch must be float or Note
:raises ValueError: Frequency must be greater than zero
"""
# Verify that the type is correct.
pitch_is_float = type(pitch) is float
pitch_is_note = type(pitch) is Note
if not (pitch_is_float or pitch_is_note):
raise TypeError("Pitch must be float or Note")
if pitch <= 0:
raise ValueError("Frequency must be greater than zero")
[docs] @staticmethod
def verify_duration(duration: timedelta) -> None:
"""
Verify that a duration is valid.
:param duration: duration to validate.
:raises TypeError: duration must be a timedelta.
:raises ValueError: duration cannot be negative.
"""
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.")