import re
from typing import Type
import pyvisa
from . import util
from . import scope_logger
[docs]
class MSO4TriggerBase(util.DisableNewAttr):
'''Base trigger for the MSO 4-Series, used for settings shared by all trigger types'''
_sources = ['auxiliary', 'aux', 'line'] # NOTE: Digital channels are not supported yet
_couplings = ['dc', 'hfrej', 'lfrej', 'noiserej']
_events = ['A', 'B']
_modes = ['auto', 'normal']
_type = None
[docs]
def __init__(self, res: pyvisa.resources.MessageBasedResource, ch_a_count: int, event: str = 'A'):
'''Create a new trigger object
Args:
res: The VISA resource to use for communication
ch_a_count: The number of analog channels available on the scope
event: The event channel (``A`` or ``B``) to use as a trigger.
See: 4/5/6 Series MSO Help ยง Trigger on sequential events (A and B triggers)
(https://www.tek.com/en/sitewide-content/manuals/4/5/6/4-5-6-series-mso-help)
'''
super().__init__()
self.sc: pyvisa.resources.MessageBasedResource = res
self._ch_a_count: int = ch_a_count
if event not in MSO4TriggerBase._events:
raise ValueError(f'Invalid event {event}. Valid events: {MSO4TriggerBase._events}')
self._event = event
if not self._type:
raise NotImplementedError("Can't instantiate MSO4TriggerBase directly. Use a subclass instead.")
self.sc.write(f'TRIGGER:{self._event}:TYPE {self._type}')
self._cached_source = None
self._cached_coupling = None
self._cached_level = None
self._cached_mode = None
[docs]
def clear_caches(self):
'''Resets the local configuration cache so that values will be fetched from
the scope.
This is useful when the scope configuration is (potentially) changed externally.
'''
self._cached_source = None
self._cached_coupling = None
self._cached_level = None
self._cached_mode = None
[docs]
def force(self):
'''Force the trigger to occur immediately'''
self.sc.write('TRIGGER FORCe')
@property
def source(self):
'''The source of the event currently configured as a trigger. Valid values are
``chN`` (Analog channel n) and ``auxiliary``, ``aux``, ``line``
*Cached*
:Getter: Return the current trigger source
:Setter: Set the trigger source
Raises:
ValueError: if value is not one of the allowed strings
'''
if not self._cached_source:
self._cached_source = self.sc.query(f'TRIGGER:{self._event}:{self._type}:SOURCE?').strip()
return self._cached_source
@source.setter
def source(self, src: str):
src = src.lower()
valid = False
if src.lower()[:2] == 'ch':
try:
ch_num = int(src[2:])
if ch_num < self._ch_a_count:
valid = True
except ValueError:
pass
else:
valid = src in MSO4TriggerBase._sources
if not valid:
raise ValueError(f'Invalid trigger source {src}. Valid sources are ch1-ch{self._ch_a_count} and {MSO4TriggerBase._sources}')
if self._cached_source == src:
return
self._cached_source = src
self.sc.write(f'TRIGGER:{self._event}:{self._type}:SOURCE {src}')
@property
def coupling(self) -> str:
'''The coupling of the trigger source. Valid couplings are ``dc``, ``hfrej``, ``lfrej``, ``noiserej``
*Cached*
:Getter: Return the current trigger coupling
:Setter: Set the trigger coupling
Raises:
ValueError: if value is not one of the allowed strings
'''
if not self._cached_coupling:
self._cached_coupling = self.sc.query(f'TRIGGER:{self._event}:{self._type}:COUPLING?').strip()
return self._cached_coupling
@coupling.setter
def coupling(self, coupling: str):
if coupling.lower() not in MSO4TriggerBase._couplings:
raise ValueError(f'Invalid trigger coupling {coupling}. Valid coupling: {MSO4TriggerBase._couplings}')
if self._cached_coupling == coupling:
return
self._cached_coupling = coupling
self.sc.write(f'TRIGGER:{self._event}:{self._type}:COUPLING {coupling}')
@property
def level(self) -> float:
'''The trigger level
*Cached*
:Getter: Return the current trigger level
:Setter: Set the trigger level (int or float)
Raises:
ValueError: if value is not an int or float
'''
if not self._cached_level:
resp = self.sc.query(f'TRIGGER:{self._event}:LEVEL:{self.source}?').strip()
try:
self._cached_level = float(resp)
except ValueError as exc:
raise ValueError(f'Got invalid trigger level from oscilloscope `{resp}`. Must be a float.') from exc
return self._cached_level
@level.setter
def level(self, level: float):
if not isinstance(level, float) and not isinstance(level, int):
raise ValueError(f'Invalid trigger level {level}. Must be a float or an int.')
if self._cached_level == level:
return
self.sc.write(f'TRIGGER:{self._event}:LEVEL:{self.source} {level:.4e}')
self._cached_level = None
self._cached_level = self.level
if self._cached_level != level:
scope_logger.warning('Failed to set trigger level to %f. Got %f instead.', level, self._cached_level)
@property
def mode(self) -> str:
'''The trigger mode (``auto``/``normal``)
*Cached*
:Getter: Return the current trigger mode
:Setter: Set the trigger mode
Raises:
NotImplementedError: if trigger event is not A
ValueError: if value is not one of the allowed strings
'''
if self._event != 'A':
raise NotImplementedError('Trigger mode is only supported for event A.')
if not self._cached_mode:
self._cached_mode = self.sc.query('TRIGGER:A:MODe?').strip()
return self._cached_mode
@mode.setter
def mode(self, mode: str):
if self._event != 'A':
raise NotImplementedError('Trigger mode is only supported for event A.')
if mode.lower() not in MSO4TriggerBase._modes:
raise ValueError(f'Invalid trigger mode {mode}. Valid modes: {MSO4TriggerBase._modes}')
if self._cached_mode == mode:
return
self._cached_mode = mode
self.sc.write(f'TRIGGER:A:MODe {mode}')
[docs]
class MSO4EdgeTrigger(MSO4TriggerBase):
'''Edge trigger'''
_type = 'EDGE'
_slopes = ['rise', 'fall', 'either']
[docs]
def __init__(self, res: pyvisa.resources.MessageBasedResource, ch_a_count: int, event: str = 'A'):
super().__init__(res, ch_a_count, event)
self._cached_edge_slope = None
self.disable_newattr()
[docs]
def clear_caches(self):
super().clear_caches()
self._cached_edge_slope = None
@property
def edge_slope(self) -> str:
'''The edge slope (``rise``/``fall``/``either``)
*Cached*
:Getter: Return the current edge slope
:Setter: Set the edge slope
Raises:
ValueError: if value is not one of the allowed strings
'''
if not self._cached_edge_slope:
self._cached_edge_slope = self.sc.query(f'TRIGGER:{self._event}:EDGE:SLOpe?').strip()
return self._cached_edge_slope
@edge_slope.setter
def edge_slope(self, slope: str):
if slope.lower() not in MSO4EdgeTrigger._slopes:
raise ValueError(f'Invalid edge slope {slope}. Valid slopes: {MSO4EdgeTrigger._slopes}')
if self._cached_edge_slope == slope:
return
self._cached_edge_slope = slope
self.sc.write(f'TRIGGER:{self._event}:EDGE:SLOpe {slope}')
[docs]
class MSO4WidthTrigger(MSO4TriggerBase):
'''Pulse Width trigger
'''
_type = 'WIDth'
_whens = ['lessthan', 'morethan', 'equal', 'unequal', 'within', 'outside']
_polarities = ['positive', 'negative']
_logicqualifications = ['on', 'off']
[docs]
def __init__(self, res: pyvisa.resources.MessageBasedResource, ch_a_count: int, event: str = 'A'):
super().__init__(res, ch_a_count, event)
self._cached_when = None
self._cached_lowlimit = None
self._cached_highlimit = None
self._cached_polarity = None
self._cached_logicqualification = None
self.disable_newattr()
[docs]
def clear_caches(self):
super().clear_caches()
self._cached_when = None
self._cached_lowlimit = None
self._cached_highlimit = None
self._cached_polarity = None
self._cached_logicqualification = None
@property
def lowlimit(self) -> float:
'''The low limit of the pulse width (in seconds)
*Cached*
:Getter: Return the current low limit
:Setter: Set the low limit (int or float)
Raises:
ValueError: if value is not an int or float
'''
if self._cached_lowlimit is None:
resp = self.sc.query(f'TRIGGER:{self._event}:PULSEWidth:LOWLimit?').strip()
try:
self._cached_lowlimit = float(resp)
except ValueError as exc:
raise ValueError(f'Got invalid trigger low limit from oscilloscope `{resp}`. Must be a float.') from exc
return self._cached_lowlimit
@lowlimit.setter
def lowlimit(self, low: float):
if not isinstance(low, float) and not isinstance(low, int):
raise ValueError(f'Invalid trigger low limit {low}. Must be a float or an int.')
if self._cached_lowlimit == low:
return
self._cached_lowlimit = low
self.sc.write(f'TRIGGER:{self._event}:PULSEWidth:LOWLimit {low:.4e}')
@property
def highlimit(self) -> float:
'''The high limit of the pulse width (in seconds)
*Cached*
:Getter: Return the current high limit
:Setter: Set the high limit (int or float)
Raises:
ValueError: if value is not an int or float
'''
if self._cached_highlimit is None:
resp = self.sc.query(f'TRIGGER:{self._event}:PULSEWidth:HIGHLimit?').strip()
try:
self._cached_highlimit = float(resp)
except ValueError as exc:
raise ValueError(f'Got invalid trigger high limit from oscilloscope `{resp}`. Must be a float.') from exc
return self._cached_highlimit
@highlimit.setter
def highlimit(self, high: float):
if not isinstance(high, float) and not isinstance(high, int):
raise ValueError(f'Invalid trigger high limit {high}. Must be a float or an int.')
if self._cached_highlimit == high:
return
self._cached_highlimit = high
self.sc.write(f'TRIGGER:{self._event}:PULSEWidth:HIGHLimit {high:.4e}')
@property
def when(self) -> str:
'''Trigger when a pulse is detected with a width ``lessthan``, ``morethan``,
``equal``, ``unequal`` the width specified with :attr:`~MSO4WidthTrigger.lowlimit`.
When both :attr:`~MSO4WidthTrigger.lowlimit` and :attr:`~MSO4WidthTrigger.highlimit`
are set, the trigger can occur when a pulse is either ``within`` or ``outside``
the specified width range.
*Cached*
:Getter: Return the current when
:Setter: Set the when (str)
Raises:
ValueError: if value is not one of the allowed strings
'''
if self._cached_when is None:
self._cached_when = self.sc.query(f'TRIGGER:{self._event}:PULSEWidth:WHEn?').strip()
return self._cached_when
@when.setter
def when(self, when: str):
if when.lower() not in MSO4WidthTrigger._whens:
raise ValueError(f'Invalid trigger when {when}. Valid when: {MSO4WidthTrigger._whens}')
if self._cached_when == when:
return
self._cached_when = when
self.sc.write(f'TRIGGER:{self._event}:PULSEWidth:WHEn {when}')
@property
def polarity(self) -> str:
'''The polarity of the pulse (``positive``/``negative``)
*Cached*
:Getter: Return the current polarity
:Setter: Set the polarity (str)
Raises:
ValueError: if value is not one of the allowed strings
'''
if self._cached_polarity is None:
self._cached_polarity = self.sc.query(f'TRIGGER:{self._event}:PULSEWidth:POLarity?').strip()
return self._cached_polarity
@polarity.setter
def polarity(self, polarity: str):
if polarity.lower() not in MSO4WidthTrigger._polarities:
raise ValueError(f'Invalid trigger polarity {polarity}. Valid polarity: {MSO4WidthTrigger._polarities}')
if self._cached_polarity == polarity:
return
self._cached_polarity = polarity
self.sc.write(f'TRIGGER:{self._event}:PULSEWidth:POLarity {polarity}')
@property
def logicqualification(self) -> str:
'''The logic qualification (``on``/``off``). See the oscilloscope help (p. 122) for more information.
*Cached*
:Getter: Return the current logic qualification
:Setter: Set the logic qualification (str)
Raises:
ValueError: if value is not one of the allowed strings
'''
if self._cached_logicqualification is None:
self._cached_logicqualification = self.sc.query(f'TRIGGER:{self._event}:PULSEWidth:LOGICQUALification?').strip()
return self._cached_logicqualification
@logicqualification.setter
def logicqualification(self, logic: str):
if logic.lower() not in MSO4WidthTrigger._logicqualifications:
raise ValueError(f'Invalid trigger logic qualification {logic}. Valid logic qualification: {MSO4WidthTrigger._logicqualifications}')
if self._cached_logicqualification == logic:
return
self._cached_logicqualification = logic
self.sc.write(f'TRIGGER:{self._event}:PULSEWidth:LOGICQUALification {logic}')
scope_logger.warning('Logic qualification input define setting are not yet implemented.')
MSO4Triggers = Type[MSO4EdgeTrigger] | Type[MSO4WidthTrigger]