Hide keyboard shortcuts

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 . 

6 

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. 

10 

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. 

15 

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

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

18 

19On 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 

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

29 

30 $ pip uninstall python-libtiepie 

31 $ pip install python-libtiepie 

32 

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

37 

38""" 

39from __future__ import annotations 

40 

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) 

58 

59from aenum import IntEnum 

60 

61try: 

62 import numpy as np # type: ignore 

63except ImportError: 

64 from collections import namedtuple 

65 

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

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

68 _has_numpy = False 

69else: 

70 _has_numpy = True 

71 

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 

78 

79from .base import SingleCommDevice 

80from ..comm import NullCommunicationProtocol 

81from ..configuration import configdataclass 

82from ..utils.enum import NameEnum 

83from ..utils.typing import Number 

84 

85 

86@configdataclass 

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

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

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" 

122 

123 

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" 

130 

131 

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" 

138 

139 

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 

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 

176class TiePieOscilloscopeResolution(IntEnum): 

177 EIGHT_BIT = 8 

178 TWELVE_BIT = 12 

179 FOURTEEN_BIT = 14 

180 SIXTEEN_BIT = 16 

181 

182 

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" 

193 

194 

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" 

201 

202 

203class TiePieError(Exception): 

204 """ 

205 Error of the class TiePie 

206 """ 

207 

208 pass 

209 

210 

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. 

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

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

237 

238 

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 

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 

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. 

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 

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

337 

338 

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

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 

386def _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 

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

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

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

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

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

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 

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

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

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

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

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 

958def _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 

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

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

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

1404class TiePieWS5(TiePieI2CHostMixin, TiePieGeneratorMixin, TiePieOscilloscope): 

1405 """ 

1406 TiePie WS5 device. 

1407 """ 

1408 

1409 

1410class TiePieHS5(TiePieI2CHostMixin, TiePieGeneratorMixin, TiePieOscilloscope): 

1411 """ 

1412 TiePie HS5 device. 

1413 """ 

1414 

1415 

1416class TiePieHS6(TiePieOscilloscope): 

1417 """ 

1418 TiePie HS6 DIFF device. 

1419 """