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

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

## 692 statements

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"""

4 This module is a wrapper around libtiepie Oscilloscope devices; see

5 https://www.tiepie.com/en/libtiepie-sdk .

6

7 The device classes adds simplifications for starting of the device (using serial

8 number) and managing mutable configuration of both the device and oscilloscope's

9 channels. This includes extra validation and typing hints support.

10

11 To install libtiepie on Windows:

12 The installation of the Python bindings "python-libtiepie" is done automatically

13 with the dependencies of the hvl_ccb. The additional DLL for Windows is included in

14 that package.

15

16 On a Linux-system additional libraries have to be installed; see

17 https://www.tiepie.com/en/libtiepie-sdk/linux .

18

19 On a Windows system, if you encounter an :code:`OSError` like this::

20

21 ...

22 self._handle = _dlopen(self._name, mode)

23 OSError: [WinError 126] The specified module could not be found

24

25 most likely the python-libtiepie package was installed in your :code:`site-packages/`

26 directory as a :code:`python-libtiepie-*.egg` file via :code:`python setup.py

27 install` or :code:`python setup.py develop` command. In this case uninstall the

28 library and re-install it using :code:`pip`::

29

30 $ pip uninstall python-libtiepie

31 $ pip install python-libtiepie

32

33 This 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

36 extracting from :code:`.egg` archives).

37

38"""

39 from __future__ import annotations

40

41 import array

42 import logging

43 import time

44 from collections import Sequence

45 from functools import wraps

46 from typing import (

47 cast,

48 Callable,

49 Dict,

50 Generator,

51 List,

52 Optional,

53 Tuple,

54 Type,

55 TypeVar,

56 Union,

57)

58

59 from aenum import IntEnum

60

61 try:

62 import numpy as np # type: ignore

63 except ImportError:

64 from collections import namedtuple

65

66 _npt = namedtuple("_npt", "ndarray")

67 np = _npt(ndarray=type(None)) # type: ignore

68 _has_numpy = False

69 else:

70 _has_numpy = True

71

72 import libtiepie as ltp

73 from libtiepie.exceptions import LibTiePieException, InvalidDeviceSerialNumberError

74 from libtiepie import oscilloscope as ltp_osc

75 from libtiepie import generator as ltp_gen

76 from libtiepie import oscilloscopechannel as ltp_osc_ch

77 from libtiepie import i2chost as ltp_i2c

78

79 from .base import SingleCommDevice

80 from ..comm import NullCommunicationProtocol

81 from ..configuration import configdataclass

82 from ..utils.enum import NameEnum

83 from ..utils.typing import Number

84

85

86@configdataclass

87 class TiePieDeviceConfig:

88 """

89 Configuration dataclass for TiePie

90 """

91

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

96

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.")

104

105

106 class TiePieDeviceType(NameEnum):

107 """

108 TiePie device type.

109 """

110

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

115

116

117 class TiePieOscilloscopeTriggerLevelMode(NameEnum):

118 _init_ = "value description"

119 UNKNOWN = ltp.TLM_UNKNOWN, "Unknown"

120 RELATIVE = ltp.TLM_RELATIVE, "Relative"

121 ABSOLUTE = ltp.TLM_ABSOLUTE, "Absolute"

122

123

124 class 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"

130

131

132 class 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"

138

139

140 class 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

150

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

174

175

176 class TiePieOscilloscopeResolution(IntEnum):

177 EIGHT_BIT = 8

178 TWELVE_BIT = 12

179 FOURTEEN_BIT = 14

180 SIXTEEN_BIT = 16

181

182

183 class 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"

193

194

195 class 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"

201

202

203 class TiePieError(Exception):

204 """

205 Error of the class TiePie

206 """

207

208 pass

209

210

211 def 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.

215

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 """

220

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

228

229 return wrapped_func

230

231

232 _LtpDeviceReturnType = TypeVar("_LtpDeviceReturnType")

233"""

234 An auxiliary typing hint of a `libtiepie` device type for return value of

235 the `get_device_by_serial_number` function and the wrapper methods using it.

236"""

237

238

239@wrap_libtiepie_exception

240 def 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

249

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 """

262

263 device_type = TiePieDeviceType(device_type)

264

265 # include network search with ltp.device_list.update()

266 ltp.network.auto_detect_enabled = True

267

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)

282

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

298

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)

306

307 return device_list_item.open_device(device_type.value)

308

309

310 def _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.

317

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

333

334

335 def 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}.")

337

338

339 def _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`.

348

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)

384

385

386 def _validate_bool(x_name: str, x: object) -> None:

387 """

388 Validate if given input `x` is a `bool`.

389

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)

398

399

400 class OscilloscopeParameterLimits:

401 """

402 Default limits for oscilloscope parameters.

403 """

404

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)

410

411

412 class OscilloscopeChannelParameterLimits:

413 """

414 Default limits for oscilloscope channel parameters.

415 """

416

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)

423

424

425 class GeneratorParameterLimits:

426 """

427 Default limits for generator parameters.

428 """

429

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)

434

435

436 class I2CHostParameterLimits:

437 """

438 Default limits for I2C host parameters.

439 """

440

441 def __init__(self, dev_i2c: ltp_i2c.I2CHost) -> None:

442 # I2C Host

443 pass

444

445

446 class 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 """

451

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 `"_"`

456

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)

466

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})"

472

473

474 class TiePieOscilloscopeConfig(PublicPropertiesReprMixin):

475 """

476 Oscilloscope's configuration with cleaning of values in properties setters.

477 """

478

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 )

484

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)

490

491 @property

492 def pre_sample_ratio(self) -> float:

493 return self.dev_osc.pre_sample_ratio

494

495 @pre_sample_ratio.setter

496 def pre_sample_ratio(self, pre_sample_ratio: float) -> None:

497 """

498 Set pre sample ratio

499

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)

506

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 )

511

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 )

518

519 return cast(

520 int,

521 _verify_via_libtiepie(self.dev_osc, "record_length", int(record_length)),

522 )

523

524 @property

525 def record_length(self) -> int:

526 return self.dev_osc.record_length

527

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")

533

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)

541

542 @property

543 def resolution(self) -> TiePieOscilloscopeResolution:

544 return self.dev_osc.resolution

545

546 @resolution.setter

547 def resolution(self, resolution: Union[int, TiePieOscilloscopeResolution]) -> None:

548 """

549 Setter for resolution of the Oscilloscope.

550

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")

557

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)

571

572 @property

573 def auto_resolution_mode(self) -> TiePieOscilloscopeAutoResolutionModes:

574 return TiePieOscilloscopeAutoResolutionModes(self.dev_osc.auto_resolution_mode)

575

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)

582

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)

591

592 @property

593 def sample_frequency(self) -> float:

594 return self.dev_osc.sample_frequency

595

596 @sample_frequency.setter

597 def sample_frequency(self, sample_frequency: float):

598 """

599 Set sample frequency of the oscilloscope.

600

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")

607

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)

618

619 @property

620 def trigger_time_out(self) -> float:

621 return self.dev_osc.trigger_time_out

622

623 @trigger_time_out.setter

624 def trigger_time_out(self, trigger_time_out: float) -> None:

625 """

626 Set trigger time-out.

627

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")

635

636

637 class TiePieGeneratorConfig(PublicPropertiesReprMixin):

638 """

639 Generator's configuration with cleaning of values in properties setters.

640 """

641

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 )

647

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)

652

653 @property

654 def frequency(self) -> float:

655 return self.dev_gen.frequency

656

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")

662

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)

669

670 @property

671 def amplitude(self) -> float:

672 return self.dev_gen.amplitude

673

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")

679

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)

684

685 @property

686 def offset(self) -> float:

687 return self.dev_gen.offset

688

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")

694

695 @staticmethod

696 def clean_signal_type(

697 signal_type: Union[int, TiePieGeneratorSignalType]

698 ) -> TiePieGeneratorSignalType:

699 return TiePieGeneratorSignalType(signal_type)

700

701 @property

702 def signal_type(self) -> TiePieGeneratorSignalType:

703 return TiePieGeneratorSignalType(self.dev_gen.signal_type)

704

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)

709

710 @staticmethod

711 def clean_enabled(enabled: bool) -> bool:

712 _validate_bool("channel enabled", enabled)

713 return enabled

714

715 @property

716 def enabled(self) -> bool:

717 return self.dev_gen.enabled

718

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)

727

728

729 class TiePieI2CHostConfig(PublicPropertiesReprMixin):

730 """

731 I2C Host's configuration with cleaning of values in properties setters.

732 """

733

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)

737

738

739 class 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 """

745

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 )

752

753 @staticmethod

754 def clean_coupling(

755 coupling: Union[str, TiePieOscilloscopeChannelCoupling]

756 ) -> TiePieOscilloscopeChannelCoupling:

757 return TiePieOscilloscopeChannelCoupling(coupling)

758

759 @property # type: ignore

760 @wrap_libtiepie_exception

761 def coupling(self) -> TiePieOscilloscopeChannelCoupling:

762 return TiePieOscilloscopeChannelCoupling(self.channel.coupling)

763

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)

768

769 @staticmethod

770 def clean_enabled(enabled: bool) -> bool:

771 _validate_bool("channel enabled", enabled)

772 return enabled

773

774 @property

775 def enabled(self) -> bool:

776 return self.channel.enabled

777

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)

786

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)

797

798 @property

799 def input_range(self) -> TiePieOscilloscopeRange:

800 return TiePieOscilloscopeRange(self.channel.range)

801

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")

806

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)

810

811 @property

812 def probe_offset(self) -> float:

813 return self.channel.probe_offset

814

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)

819

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

825

826 :return: bool: 1=safe ground available

827 """

828 return self.channel.has_safe_ground

829

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

834

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)

842

843 return self.channel.safe_ground_enabled

844

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

850

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)

856

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)

865

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)

871

872 @property

873 def trigger_hysteresis(self) -> float:

874 return self.channel.trigger.hystereses[0]

875

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)

882

883 @staticmethod

884 def clean_trigger_kind(

885 trigger_kind: Union[str, TiePieOscilloscopeTriggerKind]

886 ) -> TiePieOscilloscopeTriggerKind:

887 return TiePieOscilloscopeTriggerKind(trigger_kind)

888

889 @property

890 def trigger_kind(self) -> TiePieOscilloscopeTriggerKind:

891 return TiePieOscilloscopeTriggerKind(self.channel.trigger.kind)

892

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)

899

900 @staticmethod

901 def clean_trigger_level_mode(

902 level_mode: Union[str, TiePieOscilloscopeTriggerLevelMode]

903 ) -> TiePieOscilloscopeTriggerLevelMode:

904 return TiePieOscilloscopeTriggerLevelMode(level_mode)

905

906 @property

907 def trigger_level_mode(self) -> TiePieOscilloscopeTriggerLevelMode:

908 return TiePieOscilloscopeTriggerLevelMode(self.channel.trigger.level_mode)

909

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)

918

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)

929

930 @property

931 def trigger_level(self) -> float:

932 return self.channel.trigger.levels[0]

933

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")

938

939 @staticmethod

940 def clean_trigger_enabled(trigger_enabled):

941 _validate_bool("Trigger enabled", trigger_enabled)

942 return trigger_enabled

943

944 @property

945 def trigger_enabled(self) -> bool:

946 return self.channel.trigger.enabled

947

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)

956

957

958 def _require_dev_handle(device_type):

959 """

960 Create method decorator to check if the TiePie device handle is available.

961

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 """

965

966 device_type: TiePieDeviceType = TiePieDeviceType(device_type)

967

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.

972

973 :param method: `TiePieDevice` instance method to wrap

974 :return: Whatever wrapped `method` returns

975 """

976

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)

991

992 return wrapped_func

993

994 return wrapper

995

996

997 class TiePieOscilloscope(SingleCommDevice):

998 """

999 TiePie oscilloscope.

1000

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.

1006

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.

1011

1012 The channels use `1..N` numbering (not `0..N-1`), as in, e.g., the Multi Channel

1013 software.

1014 """

1015

1016 @staticmethod

1017 def config_cls() -> Type[TiePieDeviceConfig]:

1018 return TiePieDeviceConfig

1019

1020 @staticmethod

1021 def default_com_cls() -> Type[NullCommunicationProtocol]:

1022 return NullCommunicationProtocol

1023

1024 def __init__(self, com, dev_config) -> None:

1025 """

1026 Constructor for a TiePie device.

1027 """

1028 super().__init__(com, dev_config)

1029

1030 self._osc: Optional[ltp_osc.Oscilloscope] = None

1031

1032 self.config_osc: Optional[TiePieOscilloscopeConfig] = None

1033 """

1034 Oscilloscope's dynamical configuration.

1035 """

1036

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 """

1044

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 )

1056

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 = {}

1063

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

1071

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.

1079

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 )

1088

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")

1097

1098 self._osc = self._get_device_by_serial_number(TiePieDeviceType.OSCILLOSCOPE)

1099

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)

1111

1112 self._osc_config_setup()

1113

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")

1121

1122 self._osc_config_teardown()

1123 self._osc_close()

1124

1125 super().stop()

1126

1127 @staticmethod

1128 @wrap_libtiepie_exception

1129 def list_devices() -> ltp.devicelist.DeviceList:

1130 """

1131 List available TiePie devices.

1132

1133 :return: libtiepie up to date list of devices

1134 """

1135 ltp.device_list.update()

1136 device_list = ltp.device_list

1137

1138 # log devices list

1139 if device_list:

1140 logging.info("Available devices:\n")

1141

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))

1146

1147 else:

1148 logging.info("No devices found!")

1149

1150 return device_list

1151

1152 @wrap_libtiepie_exception

1153 @_require_dev_handle(TiePieDeviceType.OSCILLOSCOPE)

1154 def start_measurement(self) -> None:

1155 """

1156 Start a measurement using set configuration.

1157

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()

1165

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

1171

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

1180

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.

1186

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

1206

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.

1215

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

1231

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`.

1237

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]

1245

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.

1252

1253 :return: Number of channels.

1254 """

1255 return len(self._osc.channels)

1256

1257 @property

1258 def channels_enabled(self) -> Generator[int, None, None]:

1259 """

1260 Yield numbers of enabled channels.

1261

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

1267

1268

1269 class TiePieGeneratorMixin:

1270 """

1271 TiePie Generator sub-device.

1272

1273 A wrapper for the `libtiepie.generator.Generator` class. To be mixed in with

1274 `TiePieOscilloscope` base class.

1275 """

1276

1277 def __init__(self, com, dev_config):

1278 super().__init__(com, dev_config)

1279 self._gen: Optional[ltp_gen.Generator] = None

1280

1281 self.config_gen: Optional[TiePieGeneratorConfig] = None

1282 """

1283 Generator's dynamical configuration.

1284 """

1285

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,)

1292

1293 def _gen_config_teardown(self) -> None:

1294 self.config_gen = None

1295

1296 def _gen_close(self) -> None:

1297 if self._gen is not None:

1298 del self._gen

1299 self._gen = None

1300

1301 def start(self) -> None:

1302 """

1303 Start the Generator.

1304 """

1305 super().start() # type: ignore

1306 logging.info("Starting generator")

1307

1308 self._gen = cast(TiePieOscilloscope, self)._get_device_by_serial_number(

1309 TiePieDeviceType.GENERATOR

1310 )

1311 self._gen_config_setup()

1312

1313 @wrap_libtiepie_exception

1314 def stop(self) -> None:

1315 """

1316 Stop the generator.

1317 """

1318 logging.info("Stopping generator")

1319

1320 self._gen_config_teardown()

1321 self._gen_close()

1322

1323 super().stop() # type: ignore

1324

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")

1333

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")

1342

1343

1344 class TiePieI2CHostMixin:

1345 """

1346 TiePie I2CHost sub-device.

1347

1348 A wrapper for the `libtiepie.i2chost.I2CHost` class. To be mixed in with

1349 `TiePieOscilloscope` base class.

1350 """

1351

1352 def __init__(self, com, dev_config):

1353 super().__init__(com, dev_config)

1354 self._i2c: Optional[ltp_i2c.I2CHost] = None

1355

1356 self.config_i2c: Optional[TiePieI2CHostConfig] = None

1357 """

1358 I2C host's dynamical configuration.

1359 """

1360

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,)

1367

1368 def _i2c_config_teardown(self) -> None:

1369 """

1370 Teardown dynamical configuration for the I2C Host.

1371 """

1372 self.config_i2c = None

1373

1374 def _i2c_close(self) -> None:

1375 if self._i2c is not None:

1376 del self._i2c

1377 self._i2c = None

1378

1379 def start(self) -> None:

1380 """

1381 Start the I2C Host.

1382 """

1383 super().start() # type: ignore

1384 logging.info("Starting I2C host")

1385

1386 self._i2c = cast(TiePieOscilloscope, self)._get_device_by_serial_number(

1387 TiePieDeviceType.I2C

1388 )

1389 self._i2c_config_setup()

1390

1391 @wrap_libtiepie_exception

1392 def stop(self) -> None:

1393 """

1394 Stop the I2C host.

1395 """

1396 logging.info("Stopping I2C host")

1397

1398 self._i2c_config_teardown()

1399 self._i2c_close()

1400

1401 super().stop() # type: ignore

1402

1403

1404 class TiePieWS5(TiePieI2CHostMixin, TiePieGeneratorMixin, TiePieOscilloscope):

1405 """

1406 TiePie WS5 device.

1407 """

1408

1409

1410 class TiePieHS5(TiePieI2CHostMixin, TiePieGeneratorMixin, TiePieOscilloscope):

1411 """

1412 TiePie HS5 device.

1413 """

1414

1415

1416 class TiePieHS6(TiePieOscilloscope):

1417 """

1418 TiePie HS6 DIFF device.

1419 """