Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\tiepie.py : 45%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (c) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET
2#
3"""
4This module is a wrapper around libtiepie Oscilloscope devices; see
5https://www.tiepie.com/en/libtiepie-sdk .
7The device classes adds simplifications for starting of the device (using serial
8number) and managing mutable configuration of both the device and oscilloscope's
9channels. This includes extra validation and typing hints support.
11To install libtiepie on Windows:
12The installation of the Python bindings "python-libtiepie" is done automatically
13with the dependencies of the hvl_ccb. The additional DLL for Windows is included in
14that package.
16On a Linux-system additional libraries have to be installed; see
17https://www.tiepie.com/en/libtiepie-sdk/linux .
19On a Windows system, if you encounter an :code:`OSError` like this::
21 ...
22 self._handle = _dlopen(self._name, mode)
23 OSError: [WinError 126] The specified module could not be found
25most likely the python-libtiepie package was installed in your :code:`site-packages/`
26directory as a :code:`python-libtiepie-*.egg` file via :code:`python setup.py
27install` or :code:`python setup.py develop` command. In this case uninstall the
28library and re-install it using :code:`pip`::
30 $ pip uninstall python-libtiepie
31 $ pip install python-libtiepie
33This should create :code:`libtiepie/` folder. Alternatively, manually move the folder
34:code:`libtiepie/` from inside of the :code:`.egg` archive file to the containing it
35:code:`site-packages/` directory (PyCharm's Project tool window supports reading and
36extracting from :code:`.egg` archives).
38"""
39from __future__ import annotations
41import array
42import logging
43import time
44from collections import Sequence
45from functools import wraps
46from typing import (
47 cast,
48 Callable,
49 Dict,
50 Generator,
51 List,
52 Optional,
53 Tuple,
54 Type,
55 TypeVar,
56 Union,
57)
59from aenum import IntEnum
61try:
62 import numpy as np # type: ignore
63except ImportError:
64 from collections import namedtuple
66 _npt = namedtuple("_npt", "ndarray")
67 np = _npt(ndarray=type(None)) # type: ignore
68 _has_numpy = False
69else:
70 _has_numpy = True
72import libtiepie as ltp
73from libtiepie.exceptions import LibTiePieException, InvalidDeviceSerialNumberError
74from libtiepie import oscilloscope as ltp_osc
75from libtiepie import generator as ltp_gen
76from libtiepie import oscilloscopechannel as ltp_osc_ch
77from libtiepie import i2chost as ltp_i2c
79from .base import SingleCommDevice
80from ..comm import NullCommunicationProtocol
81from ..configuration import configdataclass
82from ..utils.enum import NameEnum
83from ..utils.typing import Number
86@configdataclass
87class TiePieDeviceConfig:
88 """
89 Configuration dataclass for TiePie
90 """
92 serial_number: int
93 require_block_measurement_support: bool = True
94 n_max_try_get_device: int = 10
95 wait_sec_retry_get_device: Number = 1.0
97 def clean_values(self):
98 if self.serial_number <= 0:
99 raise ValueError("serial_number must be a positive integer.")
100 if self.n_max_try_get_device <= 0:
101 raise ValueError("n_max_try_get_device must be an positive integer.")
102 if self.wait_sec_retry_get_device <= 0:
103 raise ValueError("wait_sec_retry_get_device must be a positive number.")
106class TiePieDeviceType(NameEnum):
107 """
108 TiePie device type.
109 """
111 _init_ = "value ltp_class"
112 OSCILLOSCOPE = ltp.DEVICETYPE_OSCILLOSCOPE, ltp_osc.Oscilloscope
113 I2C = ltp.DEVICETYPE_I2CHOST, ltp_i2c.I2CHost
114 GENERATOR = ltp.DEVICETYPE_GENERATOR, ltp_gen.Generator
117class TiePieOscilloscopeTriggerLevelMode(NameEnum):
118 _init_ = "value description"
119 UNKNOWN = ltp.TLM_UNKNOWN, "Unknown"
120 RELATIVE = ltp.TLM_RELATIVE, "Relative"
121 ABSOLUTE = ltp.TLM_ABSOLUTE, "Absolute"
124class TiePieOscilloscopeChannelCoupling(NameEnum):
125 _init_ = "value description"
126 DCV = ltp.CK_DCV, "DC volt"
127 ACV = ltp.CK_ACV, "AC volt"
128 DCA = ltp.CK_DCA, "DC current"
129 ACA = ltp.CK_ACA, "AC current"
132class TiePieOscilloscopeTriggerKind(NameEnum):
133 _init_ = "value description"
134 RISING = ltp.TK_RISINGEDGE, "Rising"
135 FALLING = ltp.TK_FALLINGEDGE, "Falling"
136 ANY = ltp.TK_ANYEDGE, "Any"
137 RISING_OR_FALLING = ltp.TK_ANYEDGE, "Rising or Falling"
140class TiePieOscilloscopeRange(NameEnum):
141 TWO_HUNDRED_MILLI_VOLT = 0.2
142 FOUR_HUNDRED_MILLI_VOLT = 0.4
143 EIGHT_HUNDRED_MILLI_VOLT = 0.8
144 TWO_VOLT = 2
145 FOUR_VOLT = 4
146 EIGHT_VOLT = 8
147 TWENTY_VOLT = 20
148 FORTY_VOLT = 40
149 EIGHTY_VOLT = 80
151 @staticmethod
152 def suitable_range(value):
153 try:
154 return TiePieOscilloscopeRange(value)
155 except ValueError:
156 attrs = [ra.value for ra in TiePieOscilloscopeRange]
157 chosen_range: Optional[TiePieOscilloscopeRange] = None
158 for attr in attrs:
159 if value < attr:
160 chosen_range = TiePieOscilloscopeRange(attr)
161 logging.warning(
162 f"Desired value ({value} V) not possible."
163 f"Next larger range ({chosen_range.value} V) "
164 f"selected."
165 )
166 break
167 if chosen_range is None:
168 chosen_range = TiePieOscilloscopeRange.EIGHTY_VOLT
169 logging.warning(
170 f"Desired value ({value} V) is over the maximum; "
171 f"largest range ({chosen_range.value} V) selected"
172 )
173 return chosen_range
176class TiePieOscilloscopeResolution(IntEnum):
177 EIGHT_BIT = 8
178 TWELVE_BIT = 12
179 FOURTEEN_BIT = 14
180 SIXTEEN_BIT = 16
183class TiePieGeneratorSignalType(NameEnum):
184 _init_ = "value description"
185 UNKNOWN = ltp.ST_UNKNOWN, "Unknown"
186 SINE = ltp.ST_SINE, "Sine"
187 TRIANGLE = ltp.ST_TRIANGLE, "Triangle"
188 SQUARE = ltp.ST_SQUARE, "Square"
189 DC = ltp.ST_DC, "DC"
190 NOISE = ltp.ST_NOISE, "Noise"
191 ARBITRARY = ltp.ST_ARBITRARY, "Arbitrary"
192 PULSE = ltp.ST_PULSE, "Pulse"
195class TiePieOscilloscopeAutoResolutionModes(NameEnum):
196 _init_ = "value description"
197 UNKNOWN = ltp.AR_UNKNOWN, "Unknown"
198 DISABLED = ltp.AR_DISABLED, "Disabled"
199 NATIVEONLY = ltp.AR_NATIVEONLY, "Native only"
200 ALL = ltp.AR_ALL, "All"
203class TiePieError(Exception):
204 """
205 Error of the class TiePie
206 """
208 pass
211def wrap_libtiepie_exception(func: Callable) -> Callable:
212 """
213 Decorator wrapper for `libtiepie` methods that use
214 `libtiepie.library.check_last_status_raise_on_error()` calls.
216 :param func: Function or method to be wrapped
217 :raises TiePieError: instead of `LibTiePieException` or one of its subtypes.
218 :return: whatever `func` returns
219 """
221 @wraps(func)
222 def wrapped_func(*args, **kwargs):
223 try:
224 return func(*args, **kwargs)
225 except LibTiePieException as e:
226 logging.error(str(e))
227 raise TiePieError from e
229 return wrapped_func
232_LtpDeviceReturnType = TypeVar("_LtpDeviceReturnType")
233"""
234An auxiliary typing hint of a `libtiepie` device type for return value of
235the `get_device_by_serial_number` function and the wrapper methods using it.
236"""
239@wrap_libtiepie_exception
240def get_device_by_serial_number(
241 serial_number: int,
242 # Note: TiePieDeviceType aenum as a tuple to define a return value type
243 device_type: Union[str, Tuple[int, _LtpDeviceReturnType]],
244 n_max_try_get_device: int = 10,
245 wait_sec_retry_get_device: float = 1.0,
246) -> _LtpDeviceReturnType:
247 """
248 Open and return handle of TiePie device with a given serial number
250 :param serial_number: int serial number of the device
251 :param device_type: a `TiePieDeviceType` instance containing device identifier (int
252 number) and its corresponding class, both from `libtiepie`, or a string name
253 of such instance
254 :param n_max_try_get_device: maximal number of device list updates (int number)
255 :param wait_sec_retry_get_device: waiting time in seconds between retries (int
256 number)
257 :return: Instance of a `libtiepie` device class according to the specified
258 `device_type`
259 :raises TiePieError: when there is no device with given serial number
260 :raises ValueError: when `device_type` is not an instance of `TiePieDeviceType`
261 """
263 device_type = TiePieDeviceType(device_type)
265 # include network search with ltp.device_list.update()
266 ltp.network.auto_detect_enabled = True
268 n_try = 0
269 device_list_item: Optional[ltp.devicelistitem.DeviceListItem] = None
270 while device_list_item is None and n_try < n_max_try_get_device:
271 n_try += 1
272 ltp.device_list.update()
273 if not ltp.device_list:
274 msg = f"Searching for device... (attempt #{n_try}/{n_max_try_get_device})"
275 if n_try < n_max_try_get_device:
276 logging.warning(msg)
277 time.sleep(wait_sec_retry_get_device)
278 continue
279 msg = f"No devices found to start (attempt #{n_try}/{n_max_try_get_device})"
280 logging.error(msg)
281 raise TiePieError(msg)
283 # if a device is found
284 try:
285 device_list_item = ltp.device_list.get_item_by_serial_number(serial_number)
286 except InvalidDeviceSerialNumberError as e:
287 msg = (
288 f"The device with serial number {serial_number} is not "
289 f"available; attempt #{n_try}/{n_max_try_get_device}."
290 )
291 if n_try < n_max_try_get_device:
292 logging.warning(msg)
293 time.sleep(wait_sec_retry_get_device)
294 continue
295 logging.error(msg)
296 raise TiePieError from e
297 assert device_list_item is not None
299 if not device_list_item.can_open(device_type.value):
300 msg = (
301 f"The device with serial number {serial_number} has no "
302 f"{device_type} available."
303 )
304 logging.error(msg)
305 raise TiePieError(msg)
307 return device_list_item.open_device(device_type.value)
310def _verify_via_libtiepie(
311 dev_obj: ltp.device.Device, verify_method_suffix: str, value: Number
312) -> Number:
313 """
314 Generic wrapper for `verify_SOMETHING` methods of the `libtiepie` device.
315 Additionally to returning a value that will be actually set,
316 gives an warning.
318 :param dev_obj: TiePie device object, which has the verify_SOMETHING method
319 :param verify_method_suffix: `libtiepie` devices verify_SOMETHING method
320 :param value: numeric value
321 :returns: Value that will be actually set instead of `value`.
322 :raises TiePieError: when status of underlying device gives an error
323 """
324 verify_method = getattr(dev_obj, f"verify_{verify_method_suffix}",)
325 will_have_value = verify_method(value)
326 if will_have_value != value:
327 msg = (
328 f"Can't set {verify_method_suffix} to "
329 f"{value}; instead {will_have_value} will be set."
330 )
331 logging.warning(msg)
332 return will_have_value
335def log_set(prop_name: str, prop_value: object, value_suffix: str = "") -> None:
336 logging.info(f"{prop_name} is set to {prop_value}{value_suffix}.")
339def _validate_number(
340 x_name: str,
341 x: object,
342 limits: Tuple = (None, None),
343 number_type: Union[Type[Number], Tuple[Type[Number], ...]] = (int, float),
344) -> None:
345 """
346 Validate if given input `x` is a number of given `number_type` type, with value
347 between given `limits[0]` and `limits[1]` (inclusive), if not `None`.
349 :param x_name: string name of the validate input, use for the error message
350 :param x: an input object to validate as number of given type within given range
351 :param limits: [lower, upper] limit, with `None` denoting no limit: [-inf, +inf]
352 :param number_type: expected type of a number, by default `int` or `float`
353 :raises TypeError: when the validated input does not have expected type
354 :raises ValueError: when the validated input has correct number type but is not
355 within given range
356 """
357 if limits is None:
358 limits = (None, None)
359 msg = None
360 err_cls: Optional[Type[Exception]] = None
361 if not isinstance(number_type, Sequence):
362 number_type = (number_type,)
363 if not isinstance(x, number_type):
364 msg = (
365 f"{x_name} = {x} has to be of type "
366 f"{' or '.join(nt.__name__ for nt in number_type)}"
367 )
368 err_cls = TypeError
369 elif not (
370 (limits[0] is None or cast(Number, x) >= limits[0])
371 and (limits[1] is None or cast(Number, x) <= limits[1])
372 ):
373 if limits[0] is None:
374 suffix = f"less or equal than {limits[1]}"
375 elif limits[1] is None:
376 suffix = f"greater or equal than {limits[0]}"
377 else:
378 suffix = f"between {limits[0]} and {limits[1]} inclusive"
379 msg = f"{x_name} = {x} has to be " + suffix
380 err_cls = ValueError
381 if err_cls is not None:
382 logging.error(msg)
383 raise err_cls(msg)
386def _validate_bool(x_name: str, x: object) -> None:
387 """
388 Validate if given input `x` is a `bool`.
390 :param x_name: string name of the validate input, use for the error message
391 :param x: an input object to validate as boolean
392 :raises TypeError: when the validated input does not have boolean type
393 """
394 if not isinstance(x, bool):
395 msg = f"{x_name} = {x} has to of type bool"
396 logging.error(msg)
397 raise TypeError(msg)
400class OscilloscopeParameterLimits:
401 """
402 Default limits for oscilloscope parameters.
403 """
405 def __init__(self, dev_osc: ltp_osc.Oscilloscope) -> None:
406 self.record_length = (0, dev_osc.record_length_max)
407 self.sample_frequency = (0, dev_osc.sample_frequency_max) # [samples/s]
408 self.pre_sample_ratio = (0, 1)
409 self.trigger_delay = (0, dev_osc.trigger_delay_max)
412class OscilloscopeChannelParameterLimits:
413 """
414 Default limits for oscilloscope channel parameters.
415 """
417 def __init__(self, osc_channel: ltp_osc_ch.OscilloscopeChannel) -> None:
418 self.input_range = (0, 80) # [V]
419 self.probe_offset = (-1e6, 1e6) # [V], [A] or [Ohm]
420 self.trigger_hysteresis = (0, 1)
421 self.trigger_level_rel = (0, 1)
422 self.trigger_level_abs = (None, None)
425class GeneratorParameterLimits:
426 """
427 Default limits for generator parameters.
428 """
430 def __init__(self, dev_gen: ltp_gen.Generator) -> None:
431 self.frequency = (0, dev_gen.frequency_max)
432 self.amplitude = (0, dev_gen.amplitude_max)
433 self.offset = (None, dev_gen.offset_max)
436class I2CHostParameterLimits:
437 """
438 Default limits for I2C host parameters.
439 """
441 def __init__(self, dev_i2c: ltp_i2c.I2CHost) -> None:
442 # I2C Host
443 pass
446class PublicPropertiesReprMixin:
447 """General purpose utility mixin that overwrites object representation to a one
448 analogous to `dataclass` instances, but using public properties and their values
449 instead of `fields`.
450 """
452 def _public_properties_gen(self):
453 """
454 Generator that returns instance's properties names and their values,
455 for properties that do not start with `"_"`
457 :return: attribute name and value tuples
458 """
459 for name in dir(self):
460 if (
461 not name.startswith("_")
462 and hasattr(self.__class__, name)
463 and isinstance(getattr(self.__class__, name), property)
464 ):
465 yield name, getattr(self, name)
467 def __repr__(self):
468 attrs = ", ".join(
469 [f"{name}={value!r}" for name, value in self._public_properties_gen()]
470 )
471 return f"{self.__class__.__qualname__ }({attrs})"
474class TiePieOscilloscopeConfig(PublicPropertiesReprMixin):
475 """
476 Oscilloscope's configuration with cleaning of values in properties setters.
477 """
479 def __init__(self, dev_osc: ltp_osc.Oscilloscope):
480 self.dev_osc: ltp_osc.Oscilloscope = dev_osc
481 self.param_lim: OscilloscopeParameterLimits = OscilloscopeParameterLimits(
482 dev_osc=dev_osc
483 )
485 def clean_pre_sample_ratio(self, pre_sample_ratio: float) -> float:
486 _validate_number(
487 "pre sample ratio", pre_sample_ratio, self.param_lim.pre_sample_ratio
488 )
489 return float(pre_sample_ratio)
491 @property
492 def pre_sample_ratio(self) -> float:
493 return self.dev_osc.pre_sample_ratio
495 @pre_sample_ratio.setter
496 def pre_sample_ratio(self, pre_sample_ratio: float) -> None:
497 """
498 Set pre sample ratio
500 :param pre_sample_ratio: pre sample ratio numeric value.
501 :raise ValueError: If `pre_sample_ratio` is not a number between 0 and 1
502 (inclusive).
503 """
504 self.dev_osc.pre_sample_ratio = self.clean_pre_sample_ratio(pre_sample_ratio)
505 log_set("Pre-sample ratio", pre_sample_ratio)
507 def clean_record_length(self, record_length: Number) -> int:
508 _validate_number(
509 "record length", record_length, limits=self.param_lim.record_length,
510 )
512 if not (float(record_length).is_integer()):
513 raise ValueError(
514 f"The record_length has to be a value, that can be cast "
515 f"into an integer without significant precision loss; "
516 f"but {record_length} was assigned."
517 )
519 return cast(
520 int,
521 _verify_via_libtiepie(self.dev_osc, "record_length", int(record_length)),
522 )
524 @property
525 def record_length(self) -> int:
526 return self.dev_osc.record_length
528 @record_length.setter
529 def record_length(self, record_length: int) -> None:
530 record_length = self.clean_record_length(record_length)
531 self.dev_osc.record_length = record_length
532 log_set("Record length", record_length, value_suffix=" sample")
534 @staticmethod
535 def clean_resolution(
536 resolution: Union[int, TiePieOscilloscopeResolution]
537 ) -> TiePieOscilloscopeResolution:
538 if not isinstance(resolution, TiePieOscilloscopeRange):
539 _validate_number("resolution", resolution, number_type=int)
540 return TiePieOscilloscopeResolution(resolution)
542 @property
543 def resolution(self) -> TiePieOscilloscopeResolution:
544 return self.dev_osc.resolution
546 @resolution.setter
547 def resolution(self, resolution: Union[int, TiePieOscilloscopeResolution]) -> None:
548 """
549 Setter for resolution of the Oscilloscope.
551 :param resolution: resolution integer.
552 :raises ValueError: if resolution is not one of
553 `TiePieOscilloscopeResolution` instance or integer values
554 """
555 self.dev_osc.resolution = self.clean_resolution(resolution)
556 log_set("Resolution", self.dev_osc.resolution, value_suffix=" bit")
558 @staticmethod
559 def clean_auto_resolution_mode(
560 auto_resolution_mode: Union[int, TiePieOscilloscopeAutoResolutionModes]
561 ) -> TiePieOscilloscopeAutoResolutionModes:
562 if not isinstance(auto_resolution_mode, TiePieOscilloscopeAutoResolutionModes):
563 _validate_number(
564 "auto resolution mode", auto_resolution_mode, number_type=int
565 )
566 if isinstance(auto_resolution_mode, bool):
567 msg = "Auto resolution mode cannot be of boolean type"
568 logging.error(msg)
569 raise TypeError
570 return TiePieOscilloscopeAutoResolutionModes(auto_resolution_mode)
572 @property
573 def auto_resolution_mode(self) -> TiePieOscilloscopeAutoResolutionModes:
574 return TiePieOscilloscopeAutoResolutionModes(self.dev_osc.auto_resolution_mode)
576 @auto_resolution_mode.setter
577 def auto_resolution_mode(self, auto_resolution_mode):
578 self.dev_osc.auto_resolution_mode = self.clean_auto_resolution_mode(
579 auto_resolution_mode
580 ).value
581 log_set("Auto resolution mode", auto_resolution_mode)
583 def clean_sample_frequency(self, sample_frequency: float) -> float:
584 _validate_number(
585 "sample frequency", sample_frequency, self.param_lim.sample_frequency
586 )
587 sample_frequency = _verify_via_libtiepie(
588 self.dev_osc, "sample_frequency", sample_frequency
589 )
590 return float(sample_frequency)
592 @property
593 def sample_frequency(self) -> float:
594 return self.dev_osc.sample_frequency
596 @sample_frequency.setter
597 def sample_frequency(self, sample_frequency: float):
598 """
599 Set sample frequency of the oscilloscope.
601 :param sample_frequency: frequency number to set
602 :raises ValueError: when frequency is not in device range
603 """
604 sample_frequency = self.clean_sample_frequency(sample_frequency)
605 self.dev_osc.sample_frequency = sample_frequency
606 log_set("Sample frequency", f"{sample_frequency:.2e}", value_suffix=" sample/s")
608 def clean_trigger_time_out(self, trigger_time_out: Optional[Number]) -> float:
609 if trigger_time_out in (None, ltp.const.TO_INFINITY):
610 # infinite timeout: `TO_INFINITY = -1` in `libtiepie.const`
611 trigger_time_out = ltp.const.TO_INFINITY
612 else:
613 _validate_number("trigger time-out", trigger_time_out, limits=(0, None))
614 trigger_time_out = _verify_via_libtiepie(
615 self.dev_osc, "trigger_time_out", cast(Number, trigger_time_out)
616 )
617 return float(trigger_time_out)
619 @property
620 def trigger_time_out(self) -> float:
621 return self.dev_osc.trigger_time_out
623 @trigger_time_out.setter
624 def trigger_time_out(self, trigger_time_out: float) -> None:
625 """
626 Set trigger time-out.
628 :param trigger_time_out: Trigger time-out value, in seconds; `0` forces
629 trigger to start immediately after starting a measurement.
630 :raise ValueError: If trigger time-out is not a non-negative real number.
631 """
632 trigger_time_out = self.clean_trigger_time_out(trigger_time_out)
633 self.dev_osc.trigger_time_out = trigger_time_out
634 log_set("Trigger time-out", trigger_time_out, value_suffix=" s")
637class TiePieGeneratorConfig(PublicPropertiesReprMixin):
638 """
639 Generator's configuration with cleaning of values in properties setters.
640 """
642 def __init__(self, dev_gen: ltp_gen.Generator):
643 self.dev_gen: ltp_gen.Generator = dev_gen
644 self.param_lim: GeneratorParameterLimits = GeneratorParameterLimits(
645 dev_gen=dev_gen
646 )
648 def clean_frequency(self, frequency: float) -> float:
649 _validate_number("Frequency", frequency, limits=self.param_lim.frequency)
650 frequency = _verify_via_libtiepie(self.dev_gen, "frequency", frequency)
651 return float(frequency)
653 @property
654 def frequency(self) -> float:
655 return self.dev_gen.frequency
657 @frequency.setter
658 def frequency(self, frequency: float) -> None:
659 frequency = self.clean_frequency(frequency)
660 self.dev_gen.frequency = frequency
661 log_set("Generator frequency", frequency, value_suffix=" Hz")
663 def clean_amplitude(self, amplitude: float) -> float:
664 _validate_number(
665 "Generator amplitude", amplitude, limits=self.param_lim.amplitude
666 )
667 amplitude = _verify_via_libtiepie(self.dev_gen, "amplitude", amplitude)
668 return float(amplitude)
670 @property
671 def amplitude(self) -> float:
672 return self.dev_gen.amplitude
674 @amplitude.setter
675 def amplitude(self, amplitude: float) -> None:
676 amplitude = self.clean_amplitude(amplitude)
677 self.dev_gen.amplitude = amplitude
678 log_set("Generator amplitude", amplitude, value_suffix=" V")
680 def clean_offset(self, offset: float) -> float:
681 _validate_number("Generator offset", offset, limits=self.param_lim.offset)
682 offset = _verify_via_libtiepie(self.dev_gen, "offset", offset)
683 return float(offset)
685 @property
686 def offset(self) -> float:
687 return self.dev_gen.offset
689 @offset.setter
690 def offset(self, offset: float) -> None:
691 offset = self.clean_offset(offset)
692 self.dev_gen.offset = offset
693 log_set("Generator offset", offset, value_suffix=" V")
695 @staticmethod
696 def clean_signal_type(
697 signal_type: Union[int, TiePieGeneratorSignalType]
698 ) -> TiePieGeneratorSignalType:
699 return TiePieGeneratorSignalType(signal_type)
701 @property
702 def signal_type(self) -> TiePieGeneratorSignalType:
703 return TiePieGeneratorSignalType(self.dev_gen.signal_type)
705 @signal_type.setter
706 def signal_type(self, signal_type: Union[int, TiePieGeneratorSignalType]) -> None:
707 self.dev_gen.signal_type = self.clean_signal_type(signal_type).value
708 log_set("Signal type", signal_type)
710 @staticmethod
711 def clean_enabled(enabled: bool) -> bool:
712 _validate_bool("channel enabled", enabled)
713 return enabled
715 @property
716 def enabled(self) -> bool:
717 return self.dev_gen.enabled
719 @enabled.setter
720 def enabled(self, enabled: bool) -> None:
721 self.dev_gen.enabled = self.clean_enabled(enabled)
722 if enabled:
723 msg = "enabled"
724 else:
725 msg = "disabled"
726 log_set("Generator", msg)
729class TiePieI2CHostConfig(PublicPropertiesReprMixin):
730 """
731 I2C Host's configuration with cleaning of values in properties setters.
732 """
734 def __init__(self, dev_i2c: ltp_i2c.I2CHost):
735 self.dev_i2c: ltp_i2c.I2CHost = dev_i2c
736 self.param_lim: I2CHostParameterLimits = I2CHostParameterLimits(dev_i2c=dev_i2c)
739class TiePieOscilloscopeChannelConfig(PublicPropertiesReprMixin):
740 """
741 Oscilloscope's channel configuration, with cleaning of
742 values in properties setters as well as setting and reading them on and
743 from the device's channel.
744 """
746 def __init__(self, ch_number: int, channel: ltp_osc_ch.OscilloscopeChannel):
747 self.ch_number: int = ch_number
748 self.channel: ltp_osc_ch.OscilloscopeChannel = channel
749 self.param_lim: OscilloscopeChannelParameterLimits = (
750 OscilloscopeChannelParameterLimits(osc_channel=channel)
751 )
753 @staticmethod
754 def clean_coupling(
755 coupling: Union[str, TiePieOscilloscopeChannelCoupling]
756 ) -> TiePieOscilloscopeChannelCoupling:
757 return TiePieOscilloscopeChannelCoupling(coupling)
759 @property # type: ignore
760 @wrap_libtiepie_exception
761 def coupling(self) -> TiePieOscilloscopeChannelCoupling:
762 return TiePieOscilloscopeChannelCoupling(self.channel.coupling)
764 @coupling.setter
765 def coupling(self, coupling: Union[str, TiePieOscilloscopeChannelCoupling]) -> None:
766 self.channel.coupling = self.clean_coupling(coupling).value
767 log_set("Coupling", coupling)
769 @staticmethod
770 def clean_enabled(enabled: bool) -> bool:
771 _validate_bool("channel enabled", enabled)
772 return enabled
774 @property
775 def enabled(self) -> bool:
776 return self.channel.enabled
778 @enabled.setter
779 def enabled(self, enabled: bool) -> None:
780 self.channel.enabled = self.clean_enabled(enabled)
781 if enabled:
782 msg = "enabled"
783 else:
784 msg = "disabled"
785 log_set("Channel {}".format(self.ch_number), msg)
787 def clean_input_range(
788 self, input_range: Union[float, TiePieOscilloscopeRange]
789 ) -> TiePieOscilloscopeRange:
790 if not isinstance(input_range, TiePieOscilloscopeRange):
791 _validate_number(
792 "input range",
793 TiePieOscilloscopeRange.suitable_range(input_range).value,
794 self.param_lim.input_range,
795 )
796 return TiePieOscilloscopeRange.suitable_range(input_range)
798 @property
799 def input_range(self) -> TiePieOscilloscopeRange:
800 return TiePieOscilloscopeRange(self.channel.range)
802 @input_range.setter
803 def input_range(self, input_range: Union[float, TiePieOscilloscopeRange]) -> None:
804 self.channel.range = self.clean_input_range(input_range).value
805 log_set("input range", self.channel.range, value_suffix=" V")
807 def clean_probe_offset(self, probe_offset: float) -> float:
808 _validate_number("probe offset", probe_offset, self.param_lim.probe_offset)
809 return float(probe_offset)
811 @property
812 def probe_offset(self) -> float:
813 return self.channel.probe_offset
815 @probe_offset.setter
816 def probe_offset(self, probe_offset: float) -> None:
817 self.channel.probe_offset = self.clean_probe_offset(probe_offset)
818 log_set("Probe offset", probe_offset)
820 @property # type: ignore
821 @wrap_libtiepie_exception
822 def has_safe_ground(self) -> bool:
823 """
824 Check whether bound oscilloscope device has "safe ground" option
826 :return: bool: 1=safe ground available
827 """
828 return self.channel.has_safe_ground
830 @staticmethod
831 def clean_safe_ground_enabled(safe_ground_enabled: bool) -> bool:
832 _validate_bool("safe ground enabled", safe_ground_enabled)
833 return safe_ground_enabled
835 @property # type:ignore
836 @wrap_libtiepie_exception
837 def safe_ground_enabled(self) -> Optional[bool]:
838 if not self.has_safe_ground:
839 msg = "The oscilloscope has no safe ground option."
840 logging.error(msg)
841 raise TiePieError(msg)
843 return self.channel.safe_ground_enabled
845 @safe_ground_enabled.setter # type:ignore
846 @wrap_libtiepie_exception
847 def safe_ground_enabled(self, safe_ground_enabled: bool) -> None:
848 """
849 Safe ground enable or disable
851 :param safe_ground_enabled: enable / disable safe ground for channel
852 """
853 if not self.has_safe_ground:
854 msg = "The oscilloscope has no safe ground option."
855 raise TiePieError(msg)
857 self.channel.safe_ground_enabled = self.clean_safe_ground_enabled(
858 safe_ground_enabled
859 )
860 if safe_ground_enabled:
861 msg = "enabled"
862 else:
863 msg = "disabled"
864 log_set("Safe ground", msg)
866 def clean_trigger_hysteresis(self, trigger_hysteresis: float) -> float:
867 _validate_number(
868 "trigger hysteresis", trigger_hysteresis, self.param_lim.trigger_hysteresis
869 )
870 return float(trigger_hysteresis)
872 @property
873 def trigger_hysteresis(self) -> float:
874 return self.channel.trigger.hystereses[0]
876 @trigger_hysteresis.setter
877 def trigger_hysteresis(self, trigger_hysteresis: float) -> None:
878 self.channel.trigger.hystereses[0] = self.clean_trigger_hysteresis(
879 trigger_hysteresis
880 )
881 log_set("Trigger hysteresis", trigger_hysteresis)
883 @staticmethod
884 def clean_trigger_kind(
885 trigger_kind: Union[str, TiePieOscilloscopeTriggerKind]
886 ) -> TiePieOscilloscopeTriggerKind:
887 return TiePieOscilloscopeTriggerKind(trigger_kind)
889 @property
890 def trigger_kind(self) -> TiePieOscilloscopeTriggerKind:
891 return TiePieOscilloscopeTriggerKind(self.channel.trigger.kind)
893 @trigger_kind.setter
894 def trigger_kind(
895 self, trigger_kind: Union[str, TiePieOscilloscopeTriggerKind]
896 ) -> None:
897 self.channel.trigger.kind = self.clean_trigger_kind(trigger_kind).value
898 log_set("Trigger kind", trigger_kind)
900 @staticmethod
901 def clean_trigger_level_mode(
902 level_mode: Union[str, TiePieOscilloscopeTriggerLevelMode]
903 ) -> TiePieOscilloscopeTriggerLevelMode:
904 return TiePieOscilloscopeTriggerLevelMode(level_mode)
906 @property
907 def trigger_level_mode(self) -> TiePieOscilloscopeTriggerLevelMode:
908 return TiePieOscilloscopeTriggerLevelMode(self.channel.trigger.level_mode)
910 @trigger_level_mode.setter
911 def trigger_level_mode(
912 self, level_mode: Union[str, TiePieOscilloscopeTriggerLevelMode]
913 ) -> None:
914 self.channel.trigger.level_mode = self.clean_trigger_level_mode(
915 level_mode
916 ).value
917 log_set("Level mode", level_mode)
919 def clean_trigger_level(self, trigger_level: float) -> float:
920 if self.channel.trigger.level_mode == 1: # RELATIVE
921 _validate_number(
922 "trigger level", trigger_level, self.param_lim.trigger_level_rel, float
923 )
924 if self.channel.trigger.level_mode == 2: # ABSOLUTE
925 _validate_number(
926 "trigger level", trigger_level, self.param_lim.trigger_level_abs, float
927 )
928 return float(trigger_level)
930 @property
931 def trigger_level(self) -> float:
932 return self.channel.trigger.levels[0]
934 @trigger_level.setter
935 def trigger_level(self, trigger_level: float) -> None:
936 self.channel.trigger.levels[0] = self.clean_trigger_level(trigger_level)
937 log_set("Trigger level", trigger_level, value_suffix=" V")
939 @staticmethod
940 def clean_trigger_enabled(trigger_enabled):
941 _validate_bool("Trigger enabled", trigger_enabled)
942 return trigger_enabled
944 @property
945 def trigger_enabled(self) -> bool:
946 return self.channel.trigger.enabled
948 @trigger_enabled.setter
949 def trigger_enabled(self, trigger_enabled: bool) -> None:
950 self.channel.trigger.enabled = self.clean_trigger_enabled(trigger_enabled)
951 if trigger_enabled:
952 msg = "enabled"
953 else:
954 msg = "disabled"
955 log_set("Trigger", msg)
958def _require_dev_handle(device_type):
959 """
960 Create method decorator to check if the TiePie device handle is available.
962 :param device_type: the TiePie device type which device handle is required
963 :raises ValueError: when `device_type` is not an instance of `TiePieDeviceType`
964 """
966 device_type: TiePieDeviceType = TiePieDeviceType(device_type)
968 def wrapper(method):
969 """
970 Method decorator to check if a TiePie device handle is available; raises
971 `TiePieError` if hand is not available.
973 :param method: `TiePieDevice` instance method to wrap
974 :return: Whatever wrapped `method` returns
975 """
977 @wraps(method)
978 def wrapped_func(self, *args, **kwargs):
979 dev_str = None
980 if device_type is TiePieDeviceType.OSCILLOSCOPE and self._osc is None:
981 dev_str = "oscilloscope"
982 if device_type is TiePieDeviceType.GENERATOR and self._gen is None:
983 dev_str = "generator"
984 if device_type is TiePieDeviceType.I2C and self._i2c is None:
985 dev_str = "I2C host"
986 if dev_str is not None:
987 msg = f"The {dev_str} handle is not available; call `.start()` first."
988 logging.error(msg)
989 raise TiePieError(msg)
990 return method(self, *args, **kwargs)
992 return wrapped_func
994 return wrapper
997class TiePieOscilloscope(SingleCommDevice):
998 """
999 TiePie oscilloscope.
1001 A wrapper for TiePie oscilloscopes, based on the class
1002 `libtiepie.osilloscope.Oscilloscope` with simplifications for starting of the
1003 device (using serial number) and managing mutable configuration of both the
1004 device and its channels, including extra validation and typing hints support for
1005 configurations.
1007 Note that, in contrast to `libtiepie` library, since all physical TiePie devices
1008 include an oscilloscope, this is the base class for all physical TiePie devices.
1009 The additional TiePie sub-devices: "Generator" and "I2CHost", are mixed-in to this
1010 base class in subclasses.
1012 The channels use `1..N` numbering (not `0..N-1`), as in, e.g., the Multi Channel
1013 software.
1014 """
1016 @staticmethod
1017 def config_cls() -> Type[TiePieDeviceConfig]:
1018 return TiePieDeviceConfig
1020 @staticmethod
1021 def default_com_cls() -> Type[NullCommunicationProtocol]:
1022 return NullCommunicationProtocol
1024 def __init__(self, com, dev_config) -> None:
1025 """
1026 Constructor for a TiePie device.
1027 """
1028 super().__init__(com, dev_config)
1030 self._osc: Optional[ltp_osc.Oscilloscope] = None
1032 self.config_osc: Optional[TiePieOscilloscopeConfig] = None
1033 """
1034 Oscilloscope's dynamical configuration.
1035 """
1037 self.config_osc_channel_dict: Dict[int, TiePieOscilloscopeChannelConfig] = {}
1038 """
1039 Channel configuration.
1040 A `dict` mapping actual channel number, numbered `1..N`, to channel
1041 configuration. The channel info is dynamically read from the device only on
1042 the first `start()`; beforehand the `dict` is empty.
1043 """
1045 @_require_dev_handle(TiePieDeviceType.OSCILLOSCOPE)
1046 def _osc_config_setup(self) -> None:
1047 """
1048 Setup dynamical configuration for the connected oscilloscope.
1049 """
1050 assert self._osc is not None
1051 self.config_osc = TiePieOscilloscopeConfig(dev_osc=self._osc,)
1052 for n in range(1, self.n_channels + 1):
1053 self.config_osc_channel_dict[n] = TiePieOscilloscopeChannelConfig(
1054 ch_number=n, channel=self._osc.channels[n - 1],
1055 )
1057 def _osc_config_teardown(self) -> None:
1058 """
1059 Teardown dynamical configuration for the oscilloscope.
1060 """
1061 self.config_osc = None
1062 self.config_osc_channel_dict = {}
1064 def _osc_close(self) -> None:
1065 """
1066 Close the wrapped `libtiepie` oscilloscope.
1067 """
1068 if self._osc is not None:
1069 del self._osc
1070 self._osc = None
1072 def _get_device_by_serial_number(
1073 self,
1074 # Note: TiePieDeviceType aenum as a tuple to define a return value type
1075 ltp_device_type: Tuple[int, _LtpDeviceReturnType],
1076 ) -> _LtpDeviceReturnType:
1077 """
1078 Wrapper around `get_device_by_serial_number` using this device's config options.
1080 :return: A `libtiepie` device object specific to a class it is called on.
1081 """
1082 return get_device_by_serial_number(
1083 self.config.serial_number,
1084 ltp_device_type,
1085 n_max_try_get_device=self.config.n_max_try_get_device,
1086 wait_sec_retry_get_device=self.config.wait_sec_retry_get_device,
1087 )
1089 @wrap_libtiepie_exception
1090 def start(self) -> None: # type: ignore
1091 """
1092 Start the oscilloscope.
1093 """
1094 logging.info(f"Starting {self}")
1095 super().start()
1096 logging.info("Starting oscilloscope")
1098 self._osc = self._get_device_by_serial_number(TiePieDeviceType.OSCILLOSCOPE)
1100 # Check for block measurement support if required
1101 if self.config.require_block_measurement_support and not (
1102 self._osc.measure_modes & ltp.MM_BLOCK # type: ignore
1103 ):
1104 self._osc_close()
1105 msg = (
1106 f"Oscilloscope with serial number {self.config.serial_number} does not "
1107 f"have required block measurement support."
1108 )
1109 logging.error(msg)
1110 raise TiePieError(msg)
1112 self._osc_config_setup()
1114 @wrap_libtiepie_exception
1115 def stop(self) -> None: # type: ignore
1116 """
1117 Stop the oscilloscope.
1118 """
1119 logging.info(f"Stopping {self}")
1120 logging.info("Stopping oscilloscope")
1122 self._osc_config_teardown()
1123 self._osc_close()
1125 super().stop()
1127 @staticmethod
1128 @wrap_libtiepie_exception
1129 def list_devices() -> ltp.devicelist.DeviceList:
1130 """
1131 List available TiePie devices.
1133 :return: libtiepie up to date list of devices
1134 """
1135 ltp.device_list.update()
1136 device_list = ltp.device_list
1138 # log devices list
1139 if device_list:
1140 logging.info("Available devices:\n")
1142 for item in ltp.device_list:
1143 logging.info(" Name: " + item.name)
1144 logging.info(" Serial number: " + str(item.serial_number))
1145 logging.info(" Available types: " + ltp.device_type_str(item.types))
1147 else:
1148 logging.info("No devices found!")
1150 return device_list
1152 @wrap_libtiepie_exception
1153 @_require_dev_handle(TiePieDeviceType.OSCILLOSCOPE)
1154 def start_measurement(self) -> None:
1155 """
1156 Start a measurement using set configuration.
1158 :raises TiePieError: when device is not started or status of underlying device
1159 gives an error
1160 """
1161 # make mypy happy w/ assert; `is None` check is already done in the
1162 # `_require_started` method decorator
1163 assert self._osc is not None
1164 self._osc.start()
1166 @wrap_libtiepie_exception
1167 @_require_dev_handle(TiePieDeviceType.OSCILLOSCOPE)
1168 def is_data_ready(self) -> bool:
1169 """
1170 Reports if TiePie has triggered and the data is ready to collect
1172 :return: if the data is ready to collect.
1173 :raises TiePieError: when device is not started or status of underlying device
1174 gives an error
1175 """
1176 # make mypy happy w/ assert; `is None` check is already done in the
1177 # `_require_started` method decorator
1178 assert self._osc is not None
1179 return self._osc.is_data_ready
1181 @wrap_libtiepie_exception
1182 @_require_dev_handle(TiePieDeviceType.OSCILLOSCOPE)
1183 def collect_data(self) -> Union[List[array.array], List[np.ndarray], None]:
1184 """
1185 Collect the data from TiePie.
1187 :return: Measurement data of only enabled channels in a `list` of either
1188 `numpy.ndarray` or `array.array` (fallback) with float sample data. The
1189 returned `list` items correspond to data from channel numbers as
1190 returned by `self.channels_enabled`.
1191 :raises TiePieError: when device is not started or status of underlying device
1192 gives an error
1193 """
1194 # make mypy happy w/ assert; `is None` check is already done in the
1195 # `_require_started` method decorator
1196 assert self._osc is not None
1197 if not self._osc.is_data_ready:
1198 logging.warning("Data from TiePie is not ready to collect.")
1199 return None
1200 data = self._osc.get_data()
1201 # filter-out disabled channels entries
1202 data = [ch_data for ch_data in data if ch_data is not None]
1203 if _has_numpy:
1204 data = self._measurement_data_to_numpy_array(data)
1205 return data
1207 @wrap_libtiepie_exception
1208 @_require_dev_handle(TiePieDeviceType.OSCILLOSCOPE)
1209 def measure_and_collect(self) -> Union[List[array.array], List[np.ndarray], None]:
1210 """
1211 Starts measurement, waits for a trigger event till data is available
1212 and returns collected data.
1213 Take care to get a trigger event or set a trigger timeout to prevent running
1214 your code in an infinite loop blocking your program.
1216 :return: Measurement data of only enabled channels in a `list` of either
1217 `numpy.ndarray` or `array.array` (fallback) with float sample data. The
1218 returned `list` items correspond to data from channel numbers as
1219 returned by `self.channels_enabled`.
1220 :raises TiePieError: when device is not started or status of underlying device
1221 gives an error
1222 """
1223 # make mypy happy w/ assert; `is None` check is already done in the
1224 # `_require_started` method decorator
1225 self.start_measurement()
1226 while not self.is_data_ready():
1227 # 10 ms delay to save CPU time
1228 time.sleep(0.01) # pragma: no cover
1229 data = self.collect_data()
1230 return data
1232 @staticmethod
1233 def _measurement_data_to_numpy_array(data: List[array.array]) -> List[np.ndarray]:
1234 """
1235 Converts the measurement data from list of `array.array` to list of
1236 `numpy.ndarray`.
1238 :param data: measurement data for enabled channels as a `list` of `array.array`
1239 :return: measurement data for enabled channels as a `list` of `numpy.ndarray`
1240 :raises ImportError: when `numpy` is not available
1241 """
1242 if not _has_numpy:
1243 raise ImportError("numpy is required to do this")
1244 return [np.array(ch_data) for ch_data in data]
1246 @property # type: ignore
1247 @_require_dev_handle(TiePieDeviceType.OSCILLOSCOPE)
1248 @wrap_libtiepie_exception
1249 def n_channels(self):
1250 """
1251 Number of channels in the oscilloscope.
1253 :return: Number of channels.
1254 """
1255 return len(self._osc.channels)
1257 @property
1258 def channels_enabled(self) -> Generator[int, None, None]:
1259 """
1260 Yield numbers of enabled channels.
1262 :return: Numbers of enabled channels
1263 """
1264 for (ch_nr, ch_config) in self.config_osc_channel_dict.items():
1265 if ch_config.enabled:
1266 yield ch_nr
1269class TiePieGeneratorMixin:
1270 """
1271 TiePie Generator sub-device.
1273 A wrapper for the `libtiepie.generator.Generator` class. To be mixed in with
1274 `TiePieOscilloscope` base class.
1275 """
1277 def __init__(self, com, dev_config):
1278 super().__init__(com, dev_config)
1279 self._gen: Optional[ltp_gen.Generator] = None
1281 self.config_gen: Optional[TiePieGeneratorConfig] = None
1282 """
1283 Generator's dynamical configuration.
1284 """
1286 @_require_dev_handle(TiePieDeviceType.GENERATOR)
1287 def _gen_config_setup(self) -> None:
1288 """
1289 Setup dynamical configuration for the connected generator.
1290 """
1291 self.config_gen = TiePieGeneratorConfig(dev_gen=self._gen,)
1293 def _gen_config_teardown(self) -> None:
1294 self.config_gen = None
1296 def _gen_close(self) -> None:
1297 if self._gen is not None:
1298 del self._gen
1299 self._gen = None
1301 def start(self) -> None:
1302 """
1303 Start the Generator.
1304 """
1305 super().start() # type: ignore
1306 logging.info("Starting generator")
1308 self._gen = cast(TiePieOscilloscope, self)._get_device_by_serial_number(
1309 TiePieDeviceType.GENERATOR
1310 )
1311 self._gen_config_setup()
1313 @wrap_libtiepie_exception
1314 def stop(self) -> None:
1315 """
1316 Stop the generator.
1317 """
1318 logging.info("Stopping generator")
1320 self._gen_config_teardown()
1321 self._gen_close()
1323 super().stop() # type: ignore
1325 @wrap_libtiepie_exception
1326 @_require_dev_handle(TiePieDeviceType.GENERATOR)
1327 def generator_start(self):
1328 """
1329 Start signal generation.
1330 """
1331 self._gen.start()
1332 logging.info("Starting signal generation")
1334 @wrap_libtiepie_exception
1335 @_require_dev_handle(TiePieDeviceType.GENERATOR)
1336 def generator_stop(self):
1337 """
1338 Stop signal generation.
1339 """
1340 self._gen.stop()
1341 logging.info("Stopping signal generation")
1344class TiePieI2CHostMixin:
1345 """
1346 TiePie I2CHost sub-device.
1348 A wrapper for the `libtiepie.i2chost.I2CHost` class. To be mixed in with
1349 `TiePieOscilloscope` base class.
1350 """
1352 def __init__(self, com, dev_config):
1353 super().__init__(com, dev_config)
1354 self._i2c: Optional[ltp_i2c.I2CHost] = None
1356 self.config_i2c: Optional[TiePieI2CHostConfig] = None
1357 """
1358 I2C host's dynamical configuration.
1359 """
1361 @_require_dev_handle(TiePieDeviceType.I2C)
1362 def _i2c_config_setup(self) -> None:
1363 """
1364 Setup dynamical configuration for the connected I2C host.
1365 """
1366 self.config_i2c = TiePieI2CHostConfig(dev_i2c=self._i2c,)
1368 def _i2c_config_teardown(self) -> None:
1369 """
1370 Teardown dynamical configuration for the I2C Host.
1371 """
1372 self.config_i2c = None
1374 def _i2c_close(self) -> None:
1375 if self._i2c is not None:
1376 del self._i2c
1377 self._i2c = None
1379 def start(self) -> None:
1380 """
1381 Start the I2C Host.
1382 """
1383 super().start() # type: ignore
1384 logging.info("Starting I2C host")
1386 self._i2c = cast(TiePieOscilloscope, self)._get_device_by_serial_number(
1387 TiePieDeviceType.I2C
1388 )
1389 self._i2c_config_setup()
1391 @wrap_libtiepie_exception
1392 def stop(self) -> None:
1393 """
1394 Stop the I2C host.
1395 """
1396 logging.info("Stopping I2C host")
1398 self._i2c_config_teardown()
1399 self._i2c_close()
1401 super().stop() # type: ignore
1404class TiePieWS5(TiePieI2CHostMixin, TiePieGeneratorMixin, TiePieOscilloscope):
1405 """
1406 TiePie WS5 device.
1407 """
1410class TiePieHS5(TiePieI2CHostMixin, TiePieGeneratorMixin, TiePieOscilloscope):
1411 """
1412 TiePie HS5 device.
1413 """
1416class TiePieHS6(TiePieOscilloscope):
1417 """
1418 TiePie HS6 DIFF device.
1419 """