# All manual references are to R&S RTO Digital Oscilloscope User Manual
# for firmware 3.65, 2017

import logging
import warnings
import time
from typing import Optional, Any

import numpy as np
from distutils.version import LooseVersion

from qcodes import Instrument
from qcodes.instrument.visa import VisaInstrument
from qcodes.instrument.channel import InstrumentChannel
from qcodes.utils import validators as vals
from qcodes.instrument.parameter import ArrayParameter
from qcodes.utils.helpers import create_on_off_val_mapping

log = logging.getLogger(__name__)


class ScopeTrace(ArrayParameter):

    def __init__(self, name: str, instrument: InstrumentChannel,
                 channum: int) -> None:
        """
        The ScopeTrace parameter is attached to a channel of the oscilloscope.

        For now, we only support reading out the entire trace.
        """
        super().__init__(name=name,
                         shape=(1,),
                         label='Voltage',  # TODO: Is this sometimes dbm?
                         unit='V',
                         setpoint_names=('Time',),
                         setpoint_labels=('Time',),
                         setpoint_units=('s',),
                         docstring='Holds scope trace')

        self.channel = instrument
        self.channum = channum

    def prepare_trace(self) -> None:
        """
        Prepare the scope for returning data, calculate the setpoints
        """
        # We always use 16 bit integers for the data format
        self.channel._parent.dataformat('INT,16')
        # ensure little-endianess
        self.channel._parent.write('FORMat:BORder LSBFirst')
        # only export y-values
        self.channel._parent.write('EXPort:WAVeform:INCXvalues OFF')
        # only export one channel
        self.channel._parent.write('EXPort:WAVeform:MULTichannel OFF')

        # now get setpoints

        hdr = self.channel._parent.ask(f'CHANnel{self.channum}:'
                                       'DATA:HEADER?')
        hdr_vals = list(map(float, hdr.split(',')))
        t_start = hdr_vals[0]
        t_stop = hdr_vals[1]
        no_samples = int(hdr_vals[2])
        values_per_sample = hdr_vals[3]

        # NOTE (WilliamHPNielsen):
        # If samples are multi-valued, we need a `MultiParameter`
        # instead of an `ArrayParameter`.
        if values_per_sample > 1:
            raise NotImplementedError('There are several values per sample '
                                      'in this trace (are you using envelope'
                                      ' or peak detect?). We currently do '
                                      'not support saving such a trace.')

        self.shape = (no_samples,)
        self.setpoints = (tuple(np.linspace(t_start, t_stop, no_samples)),)

        self._trace_ready = True
        # we must ensure that all this took effect before proceeding
        self.channel._parent.ask('*OPC?')

    def get_raw(self) -> np.ndarray:
        """
        Returns a trace
        """

        instr = self.channel._parent

        if not self._trace_ready:
            raise ValueError('Trace not ready! Please call '
                             'prepare_trace().')

        if instr.run_mode() == 'RUN Nx SINGLE':
            total_acquisitions = instr.num_acquisitions()
            completed_acquisitions = instr.completed_acquisitions()
            log.info(f'Acquiring {total_acquisitions} traces.')
            while completed_acquisitions < total_acquisitions:
                log.info(f'Acquired {completed_acquisitions}:'
                         f'{total_acquisitions}')
                time.sleep(0.25)
                completed_acquisitions = instr.completed_acquisitions()

        log.info('Acquisition completed. Polling trace from instrument.')
        vh = instr.visa_handle
        vh.write(f'CHANnel{self.channum}:DATA?')
        raw_vals = vh.read_raw()

        num_length = int(raw_vals[1:2])
        no_points = int(raw_vals[2:2+num_length])

        # cut of the header and the trailing '\n'
        raw_vals = raw_vals[2+num_length:-1]

        dataformat = instr.dataformat.get_latest()

        if dataformat == 'INT,8':
            int_vals = np.fromstring(raw_vals, dtype=np.int8, count=no_points)
        else:
            int_vals = np.fromstring(raw_vals, dtype=np.int16,
                                     count=no_points//2)

        # now the integer values must be converted to physical
        # values

        scale = self.channel.scale()
        no_divs = 10  # TODO: Is this ever NOT 10?

        # we always export as 16 bit integers
        quant_levels = 253*256
        conv_factor = scale*no_divs/quant_levels
        output = conv_factor*int_vals + self.channel.offset()

        return output


class ScopeMeasurement(InstrumentChannel):
    """
    Class to hold a measurement of the scope.
    """

    def __init__(self, parent: Instrument, name: str, meas_nr: int) -> None:
        """
        Args:
            parent: The instrument to which the channel is attached
            name: The name of the measurement
            meas_nr: The number of the measurement in question. Must match the
                actual number as used by the instrument (1..8)
        """

        if meas_nr not in range(1, 9):
            raise ValueError('Invalid measurement number; Min: 1, max 8')

        self.meas_nr = meas_nr
        super().__init__(parent, name)

        self.sources = vals.Enum('C1W1', 'C1W2', 'C1W3',
                                 'C2W1', 'C2W2', 'C2W3',
                                 'C3W1', 'C3W2', 'C3W3',
                                 'C4W1', 'C4W2', 'C4W3',
                                 'M1', 'M2', 'M3', 'M4',
                                 'R1', 'R2', 'R3',  'R4',
                                 'SBUS1', 'SBUS2', 'SBUS3', 'SBUS4',
                                 'D0', 'D1', 'D2', 'D3',
                                 'D4', 'D5', 'D6', 'D7',
                                 'D8', 'D9', 'D10', 'D11',
                                 'D12', 'D13', 'D14', 'D15',
                                 'TRK1', 'TRK2', 'TRK3', 'TRK4',
                                 'TRK5', 'TRK6', 'TRK7', 'TRK8',
                                 'SG1TL1', 'SG1TL2',
                                 'SG2TL1', 'SG2TL2',
                                 'SG3TL1', 'SG3TL2',
                                 'SG4TL1', 'SG4TL2',
                                 'Z1V1', 'Z1V2', 'Z1V3', 'Z1V4',
                                 'Z1I1', 'Z1I2', 'Z1I3', 'Z1I4',
                                 'Z2V1', 'Z2V2', 'Z2V3',  'Z2V4',
                                 'Z2I1', 'Z2I2', 'Z2I3', 'Z2I4')

        self.categories = vals.Enum('AMPTime', 'JITTer', 'EYEJitter',
                                    'SPECtrum', 'HISTogram', 'PROTocol')

        self.meas_type = vals.Enum(
                        # Amplitude/time measurements
                        'HIGH', 'LOW', 'AMPLitude',
                        'MAXimum', 'MINimum', 'PDELta',
                        'MEAN', 'RMS', 'STDDev',
                        'POVershoot', 'NOVershoot', 'AREA',
                        'RTIMe', 'FTIMe', 'PPULse',
                        'NPULse', 'PERiod', 'FREQuency',
                        'PDCYcle', 'NDCYcle', 'CYCarea',
                        'CYCMean', 'CYCRms', 'CYCStddev',
                        'PULCnt', 'DELay', 'PHASe',
                        'BWIDth', 'PSWitching', 'NSWitching',
                        'PULSetrain', 'EDGecount', 'SHT',
                        'SHR', 'DTOTrigger', 'PROBemeter',
                        'SLERising', 'SLEFalling',
                        # Jitter measurements
                        'CCJitter', 'NCJitter', 'CCWidth',
                        'CCDutycycle', 'TIE', 'UINTerval',
                        'DRATe', 'SKWDelay', 'SKWPhase',
                        # Eye diagram measurements
                        'ERPercent', 'ERDB', 'EHEight',
                        'EWIDth', 'ETOP', 'EBASe',
                        'QFACtor', 'RMSNoise', 'SNRatio',
                        'DCDistortion', 'ERTime', 'EFTime',
                        'EBRate', 'EAMPlitude', 'PPJitter',
                        'STDJitter', 'RMSJitter',
                        # Spectrum measurements
                        'CPOWer', 'OBWidth', 'SBWidth',
                        'THD', 'THDPCT', 'THDA',
                        'THDU', 'THDR', 'HAR',
                        'PLISt',
                        # Histogram measurements
                        'WCOunt', 'WSAMples', 'HSAMples',
                        'HPEak', 'PEAK', 'UPEakvalue',
                        'LPEakvalue', 'HMAXimum', 'HMINimum',
                        'MEDian', 'MAXMin', 'HMEan',
                        'HSTDdev', 'M1STddev', 'M2STddev',
                        'M3STddev', 'MKPositive', 'MKNegative'
                        )

        self.add_parameter('enable',
                           label=f'Measurement {meas_nr} enable',
                           set_cmd=f'MEASurement{meas_nr}:ENABle {{}}',
                           vals=vals.Enum('ON', 'OFF'),
                           docstring='Switches the measurement on or off.')

        self.add_parameter('source',
                           label=f'Measurement {meas_nr} source',
                           set_cmd=f'MEASurement{meas_nr}:SOURce {{}}',
                           vals=self.sources,
                           docstring='Set the source of a measurement if the '
                                     'measurement only needs one source.')

        self.add_parameter('source_first',
                           label=f'Measurement {meas_nr} first source',
                           set_cmd=f'MEASurement{meas_nr}:FSRC {{}}',
                           vals=self.sources,
                           docstring='Set the first source of a measurement'
                                     ' if the measurement only needs multiple'
                                     ' sources.')

        self.add_parameter('source_second',
                           label=f'Measurement {meas_nr} second source',
                           set_cmd=f'MEASurement{meas_nr}:SSRC {{}}',
                           vals=self.sources,
                           docstring='Set the second source of a measurement'
                                     ' if the measurement only needs multiple'
                                     ' sources.')

        self.add_parameter('category',
                           label=f'Measurement {meas_nr} category',
                           set_cmd=f'MEASurement{meas_nr}:CATegory {{}}',
                           vals=self.categories,
                           docstring='Set the category of a measurement.')

        self.add_parameter('main',
                           label=f'Measurement {meas_nr} main',
                           set_cmd=f'MEASurement{meas_nr}:MAIN {{}}',
                           vals=self.meas_type,
                           docstring='Set the main of a measurement.')

        self.add_parameter('statistics_enable',
                           label=f'Measurement {meas_nr} enable statistics',
                           set_cmd=f'MEASurement{meas_nr}:STATistics:ENABle'
                                   f' {{}}',
                           vals=vals.Enum('ON', 'OFF'),
                           docstring='Switches the measurement on or off.')

        self.add_parameter('clear',
                           label=f'Measurement {meas_nr} clear statistics',
                           set_cmd=f'MEASurement{meas_nr}:CLEar',
                           docstring='Clears/reset measurement.')

        self.add_parameter('event_count',
                           label=f'Measurement {meas_nr} number of events',
                           get_cmd=f'MEASurement{meas_nr}:RESult:EVTCount?',
                           get_parser=int,
                           docstring='Number of measurement results in the'
                                     ' long-term measurement.')

        self.add_parameter('result_avg',
                           label=f'Measurement {meas_nr} averages',
                           get_cmd=f'MEASurement{meas_nr}:RESult:AVG?',
                           get_parser=float,
                           docstring='Average of the long-term measurement'
                                     ' results.')


class ScopeChannel(InstrumentChannel):
    """
    Class to hold an input channel of the scope.

    Exposes: state, coupling, ground, scale, range, position, offset,
    invert, bandwidth, impedance, overload.
    """

    def __init__(self, parent: Instrument, name: str, channum: int) -> None:
        """
        Args:
            parent: The instrument to which the channel is attached
            name: The name of the channel
            channum: The number of the channel in question. Must match the
                actual number as used by the instrument (1..4)
        """

        if channum not in [1, 2, 3, 4]:
            raise ValueError('Invalid channel number! Must be 1, 2, 3, or 4.')

        self.channum = channum

        super().__init__(parent, name)

        self.add_parameter('state',
                           label=f'Channel {channum} state',
                           get_cmd=f'CHANnel{channum}:STATe?',
                           set_cmd=f'CHANnel{channum}:STATE {{}}',
                           vals=vals.Enum('ON', 'OFF'),
                           docstring='Switches the channel on or off')

        self.add_parameter('coupling',
                           label=f'Channel {channum} coupling',
                           get_cmd=f'CHANnel{channum}:COUPling?',
                           set_cmd=f'CHANnel{channum}:COUPling {{}}',
                           vals=vals.Enum('DC', 'DCLimit', 'AC'),
                           docstring=('Selects the connection of the channel'
                                      'signal. DC: 50 Ohm, DCLimit 1 MOhm, '
                                      'AC: Con. through DC capacitor'))

        self.add_parameter('ground',
                           label=f'Channel {channum} ground',
                           get_cmd=f'CHANnel{channum}:GND?',
                           set_cmd=f'CHANnel{channum}:GND {{}}',
                           vals=vals.Enum('ON', 'OFF'),
                           docstring=('Connects/disconnects the signal to/from'
                                      'the ground.'))

        # NB (WilliamHPNielsen): This parameter depends on other parameters and
        # should be dynamically updated accordingly. Cf. p 1178 of the manual
        self.add_parameter('scale',
                           label=f'Channel {channum} Y scale',
                           unit='V/div',
                           get_cmd=f'CHANnel{channum}:SCALe?',
                           set_cmd=self._set_scale,
                           get_parser=float,
                           )

        self.add_parameter('range',
                           label=f'Channel {channum} Y range',
                           unit='V',
                           get_cmd=f'CHANnel{channum}:RANGe?',
                           set_cmd=self._set_range,
                           get_parser=float
                           )

        # TODO (WilliamHPNielsen): would it be better to recast this in terms
        # of Volts?
        self.add_parameter('position',
                           label=f'Channel {channum} vert. pos.',
                           unit='div',
                           get_cmd=f'CHANnel{channum}:POSition?',
                           set_cmd=f'CHANnel{channum}:POSition {{}}',
                           get_parser=float,
                           vals=vals.Numbers(-5, 5),
                           docstring=('Positive values move the waveform up,'
                                      ' negative values move it down.'))

        self.add_parameter('offset',
                           label=f'Channel {channum} offset',
                           unit='V',
                           get_cmd=f'CHANnel{channum}:OFFSet?',
                           set_cmd=f'CHANnel{channum}:OFFSet {{}}',
                           get_parser=float,
                           )

        self.add_parameter('invert',
                           label=f'Channel {channum} inverted',
                           get_cmd=f'CHANnel{channum}:INVert?',
                           set_cmd=f'CHANnel{channum}:INVert {{}}',
                           vals=vals.Enum('ON', 'OFF'))

        # TODO (WilliamHPNielsen): This parameter should be dynamically
        # validated since 800 MHz BW is only available for 50 Ohm coupling
        self.add_parameter('bandwidth',
                           label=f'Channel {channum} bandwidth',
                           get_cmd=f'CHANnel{channum}:BANDwidth?',
                           set_cmd=f'CHANnel{channum}:BANDwidth {{}}',
                           vals=vals.Enum('FULL', 'B800', 'B200', 'B20')
                           )

        self.add_parameter('impedance',
                           label=f'Channel {channum} impedance',
                           unit='Ohm',
                           get_cmd=f'CHANnel{channum}:IMPedance?',
                           set_cmd=f'CHANnel{channum}:IMPedance {{}}',
                           vals=vals.Ints(1, 100000),
                           docstring=('Sets the impedance of the channel '
                                      'for power calculations and '
                                      'measurements.'))

        self.add_parameter('overload',
                           label=f'Channel {channum} overload',
                           get_cmd=f'CHANnel{channum}:OVERload?')

        self.add_parameter('arithmetics',
                           label=f'Channel {channum} arithmetics',
                           set_cmd=f'CHANnel{channum}:ARIThmetics {{}}',
                           get_cmd=f'CHANnel{channum}:ARIThmetics?',
                           val_mapping={'AVERAGE': 'AVER',
                                        'OFF': 'OFF',
                                        'ENVELOPE': 'ENV'}
                           )

        self.add_parameter('trace',
                           channum=self.channum,
                           parameter_class=ScopeTrace)

        self._trace_ready = False

    # Specialised/interlinked set/getters
    def _set_range(self, value: float) -> None:
        self.scale.cache.set(value/10)

        self._parent.write(f'CHANnel{self.channum}:RANGe {value}')

    def _set_scale(self, value: float) -> None:
        self.range.cache.set(value*10)

        self._parent.write(f'CHANnel{self.channum}:SCALe {value}')


class RTO1000(VisaInstrument):
    """
    QCoDeS Instrument driver for the
    Rohde-Schwarz RTO1000 series oscilloscopes.

    """

    def __init__(self, name: str, address: str,
                 model: Optional[str] = None, timeout: float = 5.,
                 HD: bool = True,
                 terminator: str = '\n',
                 **kwargs: Any) -> None:
        """
        Args:
            name: name of the instrument
            address: VISA resource address
            model: The instrument model. For newer firmware versions,
                this can be auto-detected
            timeout: The VISA query timeout
            HD: Does the unit have the High Definition Option (allowing
                16 bit vertical resolution)
            terminator: Command termination character to strip from VISA
                commands.
        """
        super().__init__(name=name, address=address, timeout=timeout,
                         terminator=terminator, **kwargs)

        # With firmware versions earlier than 3.65, it seems that the
        # model number can NOT be queried from the instrument
        # (at least fails with RTO1024, fw 2.52.1.1), so in that case
        # the user must provide the model manually.
        firmware_version = self.get_idn()['firmware']

        if LooseVersion(firmware_version) < LooseVersion('3'):
            log.warning('Old firmware version detected. This driver may '
                        'not be compatible. Please upgrade your firmware.')

        if LooseVersion(firmware_version) >= LooseVersion('3.65'):
            # strip just in case there is a newline character at the end
            self.model = self.ask('DIAGnostic:SERVice:WFAModel?').strip()
            if model is not None and model != self.model:
                warnings.warn("The model number provided by the user "
                              "does not match the instrument's response."
                              " I am going to assume that this oscilloscope "
                              f"is a model {self.model}")
        else:
            if model is None:
                raise ValueError('No model number provided. Please provide '
                                 'a model number (eg. "RTO1024").')
            else:
                self.model = model

        self.HD = HD

        # Now assign model-specific values
        self.num_chans = int(self.model[-1])
        self.num_meas = 8

        self._horisontal_divs = int(self.ask('TIMebase:DIVisions?'))

        self.add_parameter('display',
                           label='Display state',
                           set_cmd='SYSTem:DISPlay:UPDate {}',
                           val_mapping={'remote': 0,
                                        'view': 1})

        # Triggering

        self.add_parameter('trigger_display',
                           label='Trigger display state',
                           set_cmd='DISPlay:TRIGger:LINes {}',
                           get_cmd='DISPlay:TRIGger:LINes?',
                           val_mapping={'ON': 1, 'OFF': 0})

        # TODO: (WilliamHPNielsen) There are more available trigger
        # settings than implemented here. See p. 1261 of the manual
        # here we just use trigger1, which is the A-trigger

        self.add_parameter('trigger_source',
                           label='Trigger source',
                           set_cmd='TRIGger1:SOURce {}',
                           get_cmd='TRIGger1:SOURce?',
                           val_mapping={'CH1': 'CHAN1',
                                        'CH2': 'CHAN2',
                                        'CH3': 'CHAN3',
                                        'CH4': 'CHAN4',
                                        'EXT': 'EXT'})

        self.add_parameter('trigger_mode',
                           label='Trigger mode',
                           set_cmd='TRIGger:MODE {}',
                           get_cmd='TRIGger1:SOURce?',
                           vals=vals.Enum('AUTO', 'NORMAL', 'FREERUN'),
                           docstring='Sets the trigger mode which determines'
                                     ' the behaviour of the instrument if no'
                                     ' trigger occurs.\n'
                                     'Options: AUTO, NORMAL, FREERUN.',
                           unit='none')

        self.add_parameter('trigger_type',
                           label='Trigger type',
                           set_cmd='TRIGger1:TYPE {}',
                           get_cmd='TRIGger1:TYPE?',
                           val_mapping={'EDGE': 'EDGE',
                                        'GLITCH': 'GLIT',
                                        'WIDTH': 'WIDT',
                                        'RUNT': 'RUNT',
                                        'WINDOW': 'WIND',
                                        'TIMEOUT': 'TIM',
                                        'INTERVAL': 'INT',
                                        'SLEWRATE': 'SLEW',
                                        'DATATOCLOCK': 'DAT',
                                        'STATE': 'STAT',
                                        'PATTERN': 'PATT',
                                        'ANEDGE': 'ANED',
                                        'SERPATTERN': 'SERP',
                                        'NFC': 'NFC',
                                        'TV': 'TV',
                                        'CDR': 'CDR'}
                           )
        # See manual p. 1262 for an explanation of trigger types

        self.add_parameter('trigger_level',
                           label='Trigger level',
                           set_cmd=self._set_trigger_level,
                           get_cmd=self._get_trigger_level)

        self.add_parameter('trigger_edge_slope',
                           label='Edge trigger slope',
                           set_cmd='TRIGger1:EDGE:SLOPe {}',
                           get_cmd='TRIGger1:EDGE:SLOPe?',
                           vals=vals.Enum('POS', 'NEG', 'EITH'))

        # Horizontal settings

        self.add_parameter('timebase_scale',
                           label='Timebase scale',
                           set_cmd=self._set_timebase_scale,
                           get_cmd='TIMebase:SCALe?',
                           unit='s/div',
                           get_parser=float,
                           vals=vals.Numbers(25e-12, 10000))

        self.add_parameter('timebase_range',
                           label='Timebase range',
                           set_cmd=self._set_timebase_range,
                           get_cmd='TIMebase:RANGe?',
                           unit='s',
                           get_parser=float,
                           vals=vals.Numbers(250e-12, 100e3))

        self.add_parameter('timebase_position',
                           label='Horizontal position',
                           set_cmd=self._set_timebase_position,
                           get_cmd='TIMEbase:HORizontal:POSition?',
                           get_parser=float,
                           unit='s',
                           vals=vals.Numbers(-100e24, 100e24))

        # Acquisition

        # I couldn't find a way to query the run mode, so we manually keep
        # track of it. It is very important when getting the trace to make
        # sense of completed_acquisitions.
        self.add_parameter('run_mode',
                           label='Run/acquisition mode of the scope',
                           get_cmd=None,
                           set_cmd=None)

        self.run_mode('RUN CONT')

        self.add_parameter('num_acquisitions',
                           label='Number of single acquisitions to perform',
                           get_cmd='ACQuire:COUNt?',
                           set_cmd='ACQuire:COUNt {}',
                           vals=vals.Ints(1, 16777215),
                           get_parser=int)

        self.add_parameter('completed_acquisitions',
                           label='Number of completed acquisitions',
                           get_cmd='ACQuire:CURRent?',
                           get_parser=int)

        self.add_parameter('sampling_rate',
                           label='Sample rate',
                           docstring='Number of averages for measuring '
                           'trace.',
                           unit='Sa/s',
                           get_cmd='ACQuire:POINts:ARATe' + '?',
                           get_parser=int)

        self.add_parameter('acquisition_sample_rate',
                           label='Acquisition sample rate',
                           unit='Sa/s',
                           docstring='recorded waveform samples per second',
                           get_cmd='ACQuire:SRATe'+'?',
                           set_cmd='ACQuire:SRATe ' + ' {:.2f}',
                           vals=vals.Numbers(2, 20e12),
                           get_parser=float)

        # Data

        self.add_parameter('dataformat',
                           label='Export data format',
                           set_cmd='FORMat:DATA {}',
                           get_cmd='FORMat:DATA?',
                           vals=vals.Enum('ASC,0', 'REAL,32',
                                          'INT,8', 'INT,16'))

        # High definition mode (might not be available on all instruments)

        if HD:
            self.add_parameter('high_definition_state',
                               label='High definition (16 bit) state',
                               set_cmd=self._set_hd_mode,
                               get_cmd='HDEFinition:STAte?',
                               val_mapping=create_on_off_val_mapping(on_val=1,
                                                                     off_val=0),
                               docstring='Sets the filter bandwidth for the'
                                         ' high definition mode.\n'
                                         'ON: high definition mode, up to 16'
                                         ' bit digital resolution\n'
                                         'Options: ON, OFF\n\n'
                                         'Warning/Bug: By opening the HD '
                                         'acquisition menu on the scope, '
                                         'this value will be set to "ON".')

            self.add_parameter('high_definition_bandwidth',
                               label='High definition mode bandwidth',
                               set_cmd='HDEFinition:BWIDth {}',
                               get_cmd='HDEFinition:BWIDth?',
                               unit='Hz',
                               get_parser=float,
                               vals=vals.Numbers(1e4, 1e9))

        self.add_parameter('error_count',
                           label='Number of errors in the error stack',
                           get_cmd='SYSTem:ERRor:COUNt?',
                           unit='#',
                           get_parser=int)

        self.add_parameter('error_next',
                           label='Next error from the error stack',
                           get_cmd='SYSTem:ERRor:NEXT?',
                           get_parser=str)

        # Add the channels to the instrument
        for ch in range(1, self.num_chans+1):
            chan = ScopeChannel(self, f'channel{ch}', ch)
            self.add_submodule(f'ch{ch}', chan)

        for measId in range(1, self.num_meas+1):
            measCh = ScopeMeasurement(self, f'measurement{measId}', measId)
            self.add_submodule(f'meas{measId}', measCh)

        self.add_function('stop', call_cmd='STOP')
        self.add_function('reset', call_cmd='*RST')
        self.add_parameter('opc', get_cmd='*OPC?')
        self.add_parameter('stop_opc', get_cmd='STOP;*OPC?')
        self.add_parameter('status_operation',
                           get_cmd='STATus:OPERation:CONDition?',
                           get_parser=int)
        self.add_function('run_continues', call_cmd='RUNContinous')
        # starts the shutdown of the system
        self.add_function('system_shutdown', call_cmd='SYSTem:EXIT')

        self.connect_message()

    def run_cont(self) -> None:
        """
        Set the instrument in 'RUN CONT' mode
        """
        self.write('RUN')
        self.run_mode.set('RUN CONT')

    def run_single(self) -> None:
        """
        Set the instrument in 'RUN Nx SINGLE' mode
        """
        self.write('SINGLE')
        self.run_mode.set('RUN Nx SINGLE')

    def is_triggered(self) -> bool:
        wait_trigger_mask = 0b01000
        return bool(self.status_operation() & wait_trigger_mask) == False

    def is_running(self) -> bool:
        measuring_mask = 0b10000
        return bool(self.status_operation() & measuring_mask)

    def is_acquiring(self) -> bool:
        return self.is_triggered() & self.is_running()

    # Specialised set/get functions

    def _set_hd_mode(self, value: int) -> None:
        """
        Set/unset the high def mode
        """
        self._make_traces_not_ready()
        self.write(f'HDEFinition:STAte {value}')

    def _set_timebase_range(self, value: float) -> None:
        """
        Set the full range of the timebase
        """
        self._make_traces_not_ready()
        self.timebase_scale.cache.set(value/self._horisontal_divs)

        self.write(f'TIMebase:RANGe {value}')

    def _set_timebase_scale(self, value: float) -> None:
        """
        Set the length of one horizontal division.
        """
        self._make_traces_not_ready()
        self.timebase_range.cache.set(value*self._horisontal_divs)

        self.write(f'TIMebase:SCALe {value}')

    def _set_timebase_position(self, value: float) -> None:
        """
        Set the horizontal position.
        """
        self._make_traces_not_ready()
        self.write(f'TIMEbase:HORizontal:POSition {value}')

    def _make_traces_not_ready(self) -> None:
        """
        Make the scope traces be not ready.
        """
        self.ch1.trace._trace_ready = False
        self.ch2.trace._trace_ready = False
        self.ch3.trace._trace_ready = False
        self.ch4.trace._trace_ready = False

    def _set_trigger_level(self, value: float) -> None:
        """
        Set the trigger level on the currently used trigger source
        channel.
        """
        trans = {'CH1': 1, 'CH2': 2, 'CH3': 3, 'CH4': 4, 'EXT': 5}
        # We use get and not get_latest because we don't trust users to
        # not touch the front panel of an oscilloscope.
        source = trans[self.trigger_source.get()]
        if source != 5:
            submodule = self.submodules[f'ch{source}']
            assert isinstance(submodule, InstrumentChannel)
            v_range = submodule.range()
            offset = submodule.offset()

            if (value < -v_range/2 + offset) or (value > v_range/2 + offset):
                raise ValueError('Trigger level outside channel range.')

        self.write(f'TRIGger1:LEVel{source} {value}')

    def _get_trigger_level(self) -> float:
        """
        Get the trigger level from the currently used trigger source
        """
        trans = {'CH1': 1, 'CH2': 2, 'CH3': 3, 'CH4': 4, 'EXT': 5}
        # we use get and not get_latest because we don't trust users to
        # not touch the front panel of an oscilloscope
        source = trans[self.trigger_source.get()]

        val = self.ask(f'TRIGger1:LEVel{source}?')

        return float(val.strip())
