Source code for inspy.instrument.oscilloscope._base

"""Generic Instrument Class for Oscilloscopes."""

from abc import abstractmethod

from inspy.instrument._base import InstrumentBase, InstrumentError


class Oscilloscope(InstrumentBase):
    """Base class for oscilloscopes.

    Specific instruments will use this class as a base.
    It will provide the basic functionality for the instruments.
    """

    # For validating channel numbers
    channels: list[int] = []
    channel_error_message = f"Valid channel(s): {', '.join([str(i) for i in channels])}"

    TimebaseReference: list[str] = []
    ChannelCoupling: list[str] = []
    TriggerSweep: list[str] = []
    TriggerSlope: list[str] = []
    TriggerCoupling: list[str] = []
    TriggerMode: list[str] = []

    @abstractmethod
    def set_timebase(self, timebase_range: float, reference: str) -> None:
        """Configure the timebase.

        Args:
            timebase_range: The duration of the captured waveform in seconds
                (not the time per division).
            reference: The position of the trigger in the waveform.

        """
        pass

    @abstractmethod
    def set_timebase_range(self, timebase_range: float) -> None:
        """Configure the timebase range in seconds."""
        pass

    @abstractmethod
    def get_timebase_range(self) -> float:
        """Get the timebase range in seconds."""
        pass

    @abstractmethod
    def set_timebase_reference(self, reference: str) -> None:
        """Configure the position of the trigger in the waveform."""
        pass

    @abstractmethod
    def get_timebase_reference(self) -> str:
        """Get the position of the trigger in the waveform."""
        pass

    @abstractmethod
    def set_channel(
        self,
        channel: int,
        voltage_range: float,
        offset: float,
        coupling: str,
        attenuation: float,
        bw_limit: bool,
        state: bool,
    ) -> None:
        """Configure input channel.

        Args:
            channel: Number of the channel starting at 1
            voltage_range: Total voltage range for the capture
            offset: Voltage range: {offset - range / 2, offset + range / 2}.
            coupling: DC or AC (i.e. with high pass filter).
            attenuation: Attenuation factor.
            bw_limit: Enable the 25 MHz low-pass filter.
            state: Enable or disable capture and display of the channel.

        """
        pass

    @abstractmethod
    def set_channel_range(self, channel: int, voltage_range: float) -> None:
        """Set the total voltage range for the capture."""
        pass

    @abstractmethod
    def get_channel_range(self, channel: int) -> float:
        """Get the total voltage range for the capture."""
        pass

    @abstractmethod
    def set_channel_offset(self, channel: int, offset: float) -> None:
        """Set the voltage offset.

        The voltage range will be: {offset - range / 2, offset + range / 2}
        """
        pass

    @abstractmethod
    def get_channel_offset(self, channel: int) -> float:
        """Get the voltage offset."""
        pass

    @abstractmethod
    def set_channel_coupling(self, channel: int, coupling: str) -> None:
        """Set to DC or AC (i.e. with high pass filter) coupling."""
        pass

    @abstractmethod
    def get_channel_coupling(self, channel: int) -> str:
        """Get coupling type."""
        pass

    @abstractmethod
    def set_channel_attenuation(self, channel: int, attenuation: float) -> None:
        """Set the attenuation  of the channel / probe.

        Note that this effectively changes the channel's input voltage range so it's
        recommended that this is set first.
        """
        pass

    @abstractmethod
    def get_channel_attenuation(self, channel: int) -> float:
        """Set the configured attenuation of the channel."""
        pass

    @abstractmethod
    def set_channel_bw_limit(self, channel: int, bw_limit: bool) -> None:
        """Enable the 25 MHz low-pass filter."""
        pass

    @abstractmethod
    def get_channel_bw_limit(self, channel: int) -> bool:
        """Get the state of the 25 MHz low-pass filter."""
        pass

    @abstractmethod
    def set_channel_state(self, channel: int, state: bool) -> None:
        """Enable capture and display of the channel."""
        pass

    @abstractmethod
    def get_channel_state(self, channel: int) -> bool:
        """Get the state of the capture and display for the channel."""
        pass

    @abstractmethod
    def set_trigger(
        self,
        sweep: str,
        channel: int,
        level: float,
        slope: str,
        coupling: str,
        mode: str,
        noise_reject: bool = False,
        hold_off: float | None = None,
    ) -> None:
        """Configure trigger.

        Args:
            sweep: NORMAL: only trigger when condition met, or AUTO: if trigger isn't
                fired regularly then trigger anyway
            channel: Channel to monitor
            level: Voltage threshold of trigger
            slope: Trigger on positive or negative edges (other options available)
            coupling: DC or AC (i.e. with high pass filter).
            noise_reject: Enable the noise reject filter. Defaults to False.
            hold_off: Time in seconds after a trigger before another trigger may fire.
                Defaults to None.
            mode: Current only edge mode is supported.

        """
        pass

    @abstractmethod
    def set_trigger_sweep(self, sweep: str) -> None:
        """Set the sweep mode of the trigger.

        NORMAL: only trigger when condition met
        AUTO: if trigger isn't fired regularly then trigger anyway.
        """
        pass

    @abstractmethod
    def get_trigger_sweep(self) -> str:
        """Get the sweep mode of the trigger.

        NORMAL: only trigger when condition met
        AUTO: if trigger isn't fired regularly then trigger anyway.
        """
        pass

    @abstractmethod
    def set_trigger_mode(self, mode: str) -> None:
        """Set the mode (e.g. edge, glitch, etc)."""
        pass

    @abstractmethod
    def get_trigger_mode(self) -> str:
        """Get the mode (e.g. edge, glitch, etc)."""
        pass

    @abstractmethod
    def get_trigger_coupling(self) -> str:
        """Get trigger input coupling type."""
        pass

    @abstractmethod
    def set_trigger_noise_reject(self, noise_reject: bool) -> None:
        """Enable noise rejection filter."""
        pass

    @abstractmethod
    def get_trigger_noise_reject(self) -> bool:
        """Get state of noise rejection filter."""
        pass

    @abstractmethod
    def set_trigger_hold_off(self, hold_off: float) -> None:
        """Set minimum gap between triggers."""
        pass

    @abstractmethod
    def get_trigger_hold_off(self) -> float:
        """Get configured minimum gap between triggers."""
        pass

    @abstractmethod
    def trigger_now(self) -> None:
        """Force an immediate capture of data irrespective of the trigger."""
        pass

    @abstractmethod
    def single(self) -> None:
        """Capture a single waveform using the trigger."""
        pass

    @abstractmethod
    def run(self) -> None:
        """Start continuous capture of waveforms."""
        pass

    @abstractmethod
    def stop(self) -> None:
        """Stop continuous capture of waveforms."""
        pass

    @abstractmethod
    def digitize(self) -> None:
        """Capture a single waveform using the trigger."""
        pass

    @abstractmethod
    def get_waveform(self, channel: int) -> tuple[list[float], list[float]]:
        """Get the waveform data of a capture.

        First :obj:`~scope.trigger_now` or
        :obj:`~scope.digitize`.

        Args:
            channel: Channel to get the waveform data from

        Returns:
            A tuple with the first element being a list of the time values and the
            second being a list of the voltages

        """
        pass

    @abstractmethod
    def set_setup(self, data: bytes) -> None:
        """Configure the scope using bytes of a .scp file.

        Example - Load from disk:
            >>> from pathlib import Path
            >>> scope.set_setup(Path('setup.scp').read_bytes())
        """
        pass

    @abstractmethod
    def get_setup(self) -> bytes:
        """Get setup from the scope as bytes of a .scp file.

        Example - Save to disk:
            >>> from pathlib import Path
            >>> Path('setup.scp').write_bytes(scope.get_setup())
        """
        pass

    @abstractmethod
    def get_screenshot(self) -> bytes:
        """Get screenshot from the scope as PNG format bytes.

        Example - Save to disk:
            >>> from pathlib import Path
            >>> Path('screenshot.png').write_bytes(scope.get_screenshot())

        Example - Open with PIL and then save as a JPEG:
            >>> import PIL.Image
            >>> from io import BytesIO
            >>> im = PIL.Image.open(BytesIO(scope.get_screenshot()))
            >>> im.save('screenshot.jpg')

        Example - View in Jupyter notebook
            >>> from IPython.display import Image
            >>> Image(scope.get_screenshot())
        """
        pass

    @abstractmethod
    def reset(self) -> None:
        """Reset and clear the scope configuration."""
        pass

    @abstractmethod
    def await_completion(self) -> None:
        """Wait for the scope to finish processing commands."""
        pass


[docs] class OscilloscopeError(InstrumentError): """Base class for errors raised by the oscilloscopes.""" pass