diff --git a/.coveragerc b/.coveragerc index fea3cf0..ffb4b96 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ omit = *i3py/version.py */__init__.py + *i3py/drivers/* [report] # Regexes for lines to exclude from consideration diff --git a/i3py/core/base_driver.py b/i3py/core/base_driver.py index 0e0e1d2..119777f 100644 --- a/i3py/core/base_driver.py +++ b/i3py/core/base_driver.py @@ -19,7 +19,7 @@ from .has_features import HasFeatures -class InstrumentSigleton(type): +class InstrumentSingleton(type): """Metaclass ensuring that a single driver is created per instrument. """ @@ -54,7 +54,7 @@ def __call__(cls, *args, **kwargs) -> 'BaseDriver': cache = cls._instances_cache[cls] driver_id = cls.compute_id(args, kwargs) # type: ignore if driver_id not in cache: - dr = super(InstrumentSigleton, cls).__call__(*args, **kwargs) + dr = super(InstrumentSingleton, cls).__call__(*args, **kwargs) cache[driver_id] = dr else: @@ -64,7 +64,7 @@ def __call__(cls, *args, **kwargs) -> 'BaseDriver': return dr -class BaseDriver(HasFeatures, metaclass=InstrumentSigleton): +class BaseDriver(HasFeatures, metaclass=InstrumentSingleton): """ Base class of all instrument drivers in I3py. This class defines the common interface drivers are expected to implement diff --git a/i3py/core/base_subsystem.py b/i3py/core/base_subsystem.py index 07389c5..0e5c756 100644 --- a/i3py/core/base_subsystem.py +++ b/i3py/core/base_subsystem.py @@ -19,7 +19,7 @@ class SubSystem(HasFeatures): - """SubSystem allow to split the implementation of a driver into multiple + """SubSystem allows one to split the implementation of a driver into multiple parts. This mechanism allow to avoid crowding the instrument namespace with very diff --git a/i3py/drivers/__init__.py b/i3py/drivers/__init__.py new file mode 100644 index 0000000..0174c7f --- /dev/null +++ b/i3py/drivers/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package storing all the implemented drivers by manufacturer. + +The package contains also the standards definitions and common utilities such +as support for IEEE488 and SCPI commands. + +""" diff --git a/i3py/drivers/agilent/__init__.py b/i3py/drivers/agilent/__init__.py new file mode 100644 index 0000000..3f55077 --- /dev/null +++ b/i3py/drivers/agilent/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Alias package for the Keysight package. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +from .. import keysight + +sys.modules[__name__] = LazyPackage({}, __name__, __doc__, locals()) diff --git a/i3py/drivers/alazar_tech/__init__.py b/i3py/drivers/alazar_tech/__init__.py new file mode 100644 index 0000000..49a26a2 --- /dev/null +++ b/i3py/drivers/alazar_tech/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Alazar Tech instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/anritsu/__init__.py b/i3py/drivers/anritsu/__init__.py new file mode 100644 index 0000000..d4b66db --- /dev/null +++ b/i3py/drivers/anritsu/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Anritsu instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/base/dc_sources.py b/i3py/drivers/base/dc_sources.py new file mode 100644 index 0000000..7670cc9 --- /dev/null +++ b/i3py/drivers/base/dc_sources.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""I3py standard for DC sources. + +""" +from i3py.core import HasFeatures, SubSystem, channel +from i3py.core.unit import FLOAT_QUANTITY +from i3py.core.actions import Action +from i3py.core.features import Bool, Float, Str, constant + + +class DCPowerSource(HasFeatures): + """Standard interface expected from all DC Power sources. + + """ + + #: Outputs of the source. By default we declare a single output on index 0. + outputs = channel((0,)) + + with outputs as o: + + #: Is the output on or off. + #: Care should be taken that this value may not be up to date if a + #: failure occurred. To know the current status of the output use + #: read_output_status, this feature only store the target setting. + o.enabled = Bool(aliases={True: ['On', 'ON', 'on'], + False: ['Off', 'OFF', 'off']}) + + #: Target voltage for the output. If the source is a "current" source + #: this will likely be a fixed value. + o.voltage = Float(unit='V') + + #: Range in which the voltage can be set. + o.voltage_range = Float(unit='V') + + #: How does the source behave if it cannot reach the target voltage + #: because it reached the target current first. + #: - regulate: we stop at the reached voltage when the target current + #: is reached. + #: - trip: the output is disabled if the current reaches or gets + #: greater than the specified current. + o.current_limit_behavior = Str(constant('regulate'), + values=('regulate', 'trip')) + + #: Target voltage for the output. If the source is a "voltage" source + #: this will likely be a fixed value. + o.current = Float(unit='A') + + #: Range in which the current can be set. + o.current_range = Float(unit='A') + + #: How does the source behave if it cannot reach the target current + #: because it reached the target voltage first. + #: - regulate: we stop at the reached voltage when the target current + #: is reached. + #: - trip: the output is disabled if the voltage reaches or gets + #: greater than the specified voltage. + o.voltage_limit_behavior = Str(constant('regulate'), + values=('regulate', 'trip')) + + @o + @Action() + def read_output_status(self) -> str: + """Determine the status of the output. + + The generic format of the status is status:reason, if the reason is + not known used 'unknown'. The following values correspond to usual + situations. + + Returns + ------- + status : {'disabled', + 'enabled:constant-voltage', + 'enabled:constant-current', + 'tripped:over-voltage', + 'tripped:over-current', + 'unregulated'} + The possible values for the output status are the following. + - 'disabled': the output is currently disabled + - 'enabled:constant-voltage': the target voltage was reached + before the target current and voltage_limit_behavior is + 'regulate'. + - 'enabled:constant-current': the target current was reached + before the target voltage and current_limit_behavior is + 'regulate'. + - 'tripped:over-voltage': the output tripped after reaching the + voltage limit. + - 'tripped:over-current': the output tripped after reaching the + current limit. + - 'unregulated': The output of the instrument is not stable. + + """ + raise NotImplementedError() + + +class DCPowerSourceWithMeasure(DCPowerSource): + """DC power source supporting to measure the output current/voltage. + + """ + #: Outputs of the source. By default we declare a single output on index 0. + outputs = channel((0,)) + + with outputs as o: + + @o + @Action() + def measure(self, quantity: str, **kwargs) -> FLOAT_QUANTITY: + """Measure the output voltage/current. + + Parameters + ---------- + quantity : str, {'voltage', 'current'} + Quantity to measure. + + **kwargs : + Optional kwargs to specify the conditions of the measure + (integration time, averages, etc) if applicable. + + Returns + ------- + value : float or pint.Quantity + Measured value. If units are supported the value is a Quantity + object. + + """ + raise NotImplementedError() + + +class DCSourceTriggerSubsystem(SubSystem): + """Subsystem handing the usual triggering mechanism of DC sources. + + It should be added to a DCPowerSource subclass under the name trigger. + + """ + #: Working mode for the trigger. This usually defines how the instrument + #: will answer to a trigger event. + mode = Str(values=('disabled',)) + + #: Possible origin for trigger events. + source = Str(values=('immediate', 'software')) # Will extend later + + #: Delay between the trigger and the time at which the instrument start to + #: modify its output. + delay = Float(unit='s') + + @Action() + def arm(self): + """Make the system ready to receive a trigger event. + + """ + pass + + +class DCSourceProtectionSubsystem(SubSystem): + """Interface for DC source protection. + + """ + #: Is the protection enabled. + enabled = Bool(aliases={True: ['On', 'ON', 'On'], + False: ['Off', 'OFF', 'off']}) + + #: How the output behaves when the low/limit is reached. + behavior = Str(constant('trip')) + + #: Lower limit below which the setting is not allowed to go. + low_level = Float() + + #: Higher limit above which the setting is not allowed to go. + high_level = Float() + + @Action() + def read_status(self) -> str: + """Read the current status of the protection. + + Returns + ------- + status : {'working', 'tripped'} + + """ + pass + + @Action() + def reset(self) -> None: + """Reset the protection after an issue. + + """ + pass diff --git a/i3py/drivers/base/identity.py b/i3py/drivers/base/identity.py new file mode 100644 index 0000000..88aea0c --- /dev/null +++ b/i3py/drivers/base/identity.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Standard interface of the identity subsystem. + +""" +from i3py.core.base_subsystem import SubSystem +from i3py.core.features import Str + + +class Identity(SubSystem): + """Standard subsystem defining the expected identity info. + + This should be used as a base class for the identity subsystem of + instruments providing identity information. + + Notes + ----- + Some of those info might not be available for a given instrument. In such + a case the Feature should return ''. + + """ + #: Manufacturer as returned by the instrument. + manufacturer = Str(True) + + #: Model name as returned by the instrument. + model = Str(True) + + #: Instrument serial number. + serial = Str(True) + + #: Version of the installed firmware. + firmware = Str(True) diff --git a/i3py/drivers/common/__init__.py b/i3py/drivers/common/__init__.py new file mode 100644 index 0000000..bedd3cd --- /dev/null +++ b/i3py/drivers/common/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Common utility to deal with drivers implementation. + +""" + +# This package is always needed so there is no point making it lazy diff --git a/i3py/drivers/common/ieee488.py b/i3py/drivers/common/ieee488.py new file mode 100644 index 0000000..9a75b21 --- /dev/null +++ b/i3py/drivers/common/ieee488.py @@ -0,0 +1,498 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Base classes for instruments supporting standards command such *IDN?. + +A lot of those class are heavily inspired from the Slave package. + +The standards specifies that if one command of a group is implemented all +commends should be implemented. However this is not always enforced. The +base classes subdivide a bit more the commands to take this fact into +account. + +Reporting Commands + * `*CLS` - Clears the data status structure [#]_ . + * `*ESE` - Write the event status enable register [#]_ . + * `*ESE?` - Query the event status enable register [#]_ . + * `*ESR?` - Query the standard event status register [#]_ . + * `*SRE` - Write the status enable register [#]_ . + * `*SRE?` - Query the status enable register [#]_ . + * `*STB` - Query the status register [#]_ . + +Internal operation commands + * `*IDN?` - Identification query [#]_ . + * `*RST` - Perform a device reset [#]_ . + * `*TST?` - Perform internal self-test [#]_ . + +Synchronization commands + * `*OPC` - Set operation complete flag high [#]_ . + * `*OPC?` - Query operation complete flag [#]_ . + * `*WAI` - Wait to continue [#]_ . + +Power on common commands + * `*PSC` - Set the power-on status clear bit [#]_ . + * `*PSC?` - Query the power-on status clear bit [#]_ . + +Parallel poll common commands NOT IMPLEMENTED + * `*IST?` - Query the individual status message bit [#]_ . + * `*PRE` - Set the parallel poll enable register [#]_ . + * `*PRE?` - Query the parallel poll enable register [#]_ . + +Resource description common commands + * `*RDT` - Store the resource description in the device [#]_ . + * `*RDT?` - Query the stored resource description [#]_ . + +Protected user data commands + * `*PUD` - Store protected user data in the device [#]_ . + * `*PUD?` - Query the protected user data [#]_ . + +Calibration command + * `*CAL?` - Perform internal self calibration [#]_ . + +Trigger command + * `*TRG` - Execute trigger command [#]_ . + +Trigger macro commands + * `*DDT` - Define device trigger [#]_ . + * `*DDT?` - Define device trigger query [#]_ . + +Macro Commands NOT IMPLEMENTED + * `*DMC` - Define device trigger [#]_ . + * `*EMC` - Define device trigger query [#]_ . + * `*EMC?` - Define device trigger [#]_ . + * `*GMC?` - Define device trigger query [#]_ . + * `*LMC?` - Define device trigger [#]_ . + * `*PMC` - Define device trigger query [#]_ . + +Option Identification command + * `*OPT?` - Option identification query [#]_ . + +Stored settings commands + * `*RCL` - Restore device settings from local memory [#]_ . + * `*SAV` - Store current settings of the device in local memory [#]_ . + +Learn command NOT IMPLEMENTED + * `*LRN?` - Learn device setup query [#]_ . + +System configuration commands NOT IMPLEMENTED + * `*AAD` - Accept address command [#]_ . + * `*DLF` - Disable listener function command [#]_ . + +Passing control command NOT IMPLEMENTED + * `*PCB` - Pass control back [#]_ . + +Reference: + +.. [#] IEC 60488-2:2004(E) section 10.3 +.. [#] IEC 60488-2:2004(E) section 10.10 +.. [#] IEC 60488-2:2004(E) section 10.11 +.. [#] IEC 60488-2:2004(E) section 10.12 +.. [#] IEC 60488-2:2004(E) section 10.34 +.. [#] IEC 60488-2:2004(E) section 10.35 +.. [#] IEC 60488-2:2004(E) section 10.36 +.. [#] IEC 60488-2:2004(E) section 10.14 +.. [#] IEC 60488-2:2004(E) section 10.32 +.. [#] IEC 60488-2:2004(E) section 10.38 +.. [#] IEC 60488-2:2004(E) section 10.18 +.. [#] IEC 60488-2:2004(E) section 10.19 +.. [#] IEC 60488-2:2004(E) section 10.39 +.. [#] IEC 60488-2:2004(E) section 10.25 +.. [#] IEC 60488-2:2004(E) section 10.26 +.. [#] IEC 60488-2:2004(E) section 10.15 +.. [#] IEC 60488-2:2004(E) section 10.23 +.. [#] IEC 60488-2:2004(E) section 10.24 +.. [#] IEC 60488-2:2004(E) section 10.30 +.. [#] IEC 60488-2:2004(E) section 10.31 +.. [#] IEC 60488-2:2004(E) section 10.27 +.. [#] IEC 60488-2:2004(E) section 10.28 +.. [#] IEC 60488-2:2004(E) section 10.2 +.. [#] IEC 60488-2:2004(E) section 10.37 +.. [#] IEC 60488-2:2004(E) section 10.4 +.. [#] IEC 60488-2:2004(E) section 10.5 +.. [#] IEC 60488-2:2004(E) section 10.7 +.. [#] IEC 60488-2:2004(E) section 10.8 +.. [#] IEC 60488-2:2004(E) section 10.9 +.. [#] IEC 60488-2:2004(E) section 10.13 +.. [#] IEC 60488-2:2004(E) section 10.16 +.. [#] IEC 60488-2:2004(E) section 10.22 +.. [#] IEC 60488-2:2004(E) section 10.20 +.. [#] IEC 60488-2:2004(E) section 10.29 +.. [#] IEC 60488-2:2004(E) section 10.33 +.. [#] IEC 60488-2:2004(E) section 10.17 +.. [#] IEC 60488-2:2004(E) section 10.1 +.. [#] IEC 60488-2:2004(E) section 10.6 +.. [#] IEC 60488-2:2004(E) section 10.21 + +.. _IEC 60488-2: http://dx.doi.org/10.1109/IEEESTD.2004.95390 + +""" +from time import sleep +from typing import ClassVar, Dict + +from i3py.backends.visa import VisaMessageDriver +from i3py.core import subsystem, customize, set_feat +from i3py.core.actions import Action, RegisterAction +from i3py.core.features import Bool, Options, Register, Str +from stringparser import Parser + +from ..base.identity import Identity + + +# ============================================================================= +# --- Status reporting -------------------------------------------------------- +# ============================================================================= + +class IEEEStatusReporting(VisaMessageDriver): + """Class implementing the status reporting commands. + + * `*ESE` - See IEC 60488-2:2004(E) section 10.10 + * `*ESE?` - See IEC 60488-2:2004(E) section 10.11 + * `*ESR?` - See IEC 60488-2:2004(E) section 10.12 + * `*SRE` - See IEC 60488-2:2004(E) section 10.34 + * `*SRE?` - See IEC 60488-2:2004(E) section 10.35 + + """ + #: Define which bits of the status byte cause a service request. + service_request_enabled = Register('*SRE?', '*SRE {}') + + #: Define which bits contribute to the event status in the status byte. + event_status_enabled = Register('*ESE?', '*ESE {}') + + @RegisterAction(('operation_complete', 'request_control', 'query_error', + 'device_dependent_error', 'execution_error', + 'command_error', 'user_request', 'power_on',)) + def read_event_status_register(self) -> int: + """Read and clear the event register. + + """ + return int(self.visa_resource.query('*ESR?')) + + +# ============================================================================= +# --- Internal operations ----------------------------------------------------- +# ============================================================================= + +class IEEEIdentity(VisaMessageDriver): + """Class implementing the identification command. + + The identity susbsytem feature values are extracted by default from the + answer to the *IDN? command. Its format can be specified by overriding + the idn_format of the subsystem. + + """ + identity = subsystem(Identity) + + with identity as i: + + #: Format string specifying the format of the IDN query answer and + #: allowing to extract the following information: + #: - manufacturer: name of the instrument manufacturer + #: - model: name of the instrument model + #: - serial: serial number of the instrument + #: - firmware: firmware revision + #: ex {manufacturer},<{model}>,SN{serial}, Firmware revision {firmware} + i.IEEE_IDN_FORMAT = '{manufacturer},{model},{serial},{firmware}' + + i.manufacturer = set_feat(getter='*IDN?') + i.model = set_feat(getter='*IDN?') + i.serial = set_feat(getter='*IDN?') + i.firmware = set_feat(getter='*IDN?') + + def _post_getter(feat, driver, value): + """Get the identity info from the *IDN?. + + """ + infos = Parser(driver.IEEE_IDN_FORMAT)(value) + driver._cache.update(infos) + return infos.get(feat.name, '') + + for f in ('manufacturer', 'model', 'serial', 'firmware'): + setattr(i, '_post_get_' + f, + customize(f, 'post_get')(_post_getter)) + + def is_connected(self) -> bool: + try: + self.visa_resource.query('*IDN?') + except Exception: + return False + + return True + + +class IEEESelfTest(VisaMessageDriver): + """Class implementing the self-test command. + + """ + #: Meaning of the self test result. + IEEE_SELF_TEST: ClassVar[Dict[int, str]] = {0: 'Normal completion'} + + @Action() + def perform_self_test(self) -> str: + """Run the self test routine. + + """ + return self.IEEE_SELF_TEST.get(int(self.visa_resource.query('*TST?')), + 'Unknown error') + + +class IEEEReset(VisaMessageDriver): + """Class implementing the reset command. + + """ + IEEE_RESET_WAIT: ClassVar[int] = 1 + + @Action() + def reset(self) -> None: + """Initialize the instrument settings. + + After running this you might need to wait a bit before sending new + commands to the instrument. + + """ + self.visa_resource.write('*RST') + self.clear_cache() + sleep(self.IEEE_RESET_WAIT) + + +class IEEEInternalOperations(IEEEReset, IEEESelfTest, IEEEIdentity): + """Class implementing all the internal operations. + + """ + pass + + +# ============================================================================= +# --- Synchronization --------------------------------------------------------- +# ============================================================================= + +class IEEEOperationComplete(VisaMessageDriver): + """A mixin class implementing the operation complete commands. + + * `*OPC` - See IEC 60488-2:2004(E) section 10.18 + * `*OPC?` - See IEC 60488-2:2004(E) section 10.19 + + """ + + @Action() + def complete_operation(self) -> None: + """Sets the operation complete bit high of the event status byte. + + """ + self.visa_resource.write('*OPC') + + @Action() + def is_operation_completed(self) -> bool: + """Check whether or not the instrument has completed all pending + operations. + + """ + return bool(int(self.visa_resource.query('*OPC?'))) + + +class IEEEWaitToContinue(VisaMessageDriver): + """A mixin class implementing the wait command. + + * `*WAI` - See IEC 60488-2:2004(E) section 10.39 + + """ + @Action() + def wait_to_continue(self) -> None: + """Prevents the device from executing any further commands or queries + until the no operation flag is `True`. + + Notes + ----- + In devices implementing only sequential commands, the no-operation + flag is always True. + + """ + self.visa_resource.write('*WAI') + + +class IEEESynchronisation(IEEEWaitToContinue, IEEEOperationComplete): + """A mixin class implementing all synchronization methods. + + """ + pass + + +# ============================================================================= +# --- Power on ---------------------------------------------------------------- +# ============================================================================= + +class IEEEPowerOn(VisaMessageDriver): + """A mixin class, implementing the optional power-on common commands. + + The IEC 60488-2:2004(E) defines the following optional power-on common + commands: + + * `*PSC` - See IEC 60488-2:2004(E) section 10.25 + * `*PSC?` - See IEC 60488-2:2004(E) section 10.26 + + """ + #: Represents the power-on status clear flag. If it is `False` the event + #: status enable, service request enable and serial poll enable registers + #: will retain their status when power is restored to the device and will + #: be cleared if it is set to `True`. + poweron_status_clear = Bool('*PSC?', '*PSC {}', + mapping={True: '1', False: '0'}) + + +# ============================================================================= +# --- Resource description ---------------------------------------------------- +# ============================================================================= + +class IEEEResourceDescription(VisaMessageDriver): + """A class implementing the resource description common commands. + + * `*RDT` - See IEC 60488-2:2004(E) section 10.30 + * `*RDT?` - See IEC 60488-2:2004(E) section 10.31 + + """ + #: Description of the resource. The formatting is not checked. + resource_description = Str('*RDT?', '*RDT {}') + + +# ============================================================================= +# --- Protected user data ----------------------------------------------------- +# ============================================================================= + +class IEEEProtectedUserData(VisaMessageDriver): + """A class implementing the protected user data common commands. + + * `*RDT` - See IEC 60488-2:2004(E) section 10.30 + * `*RDT?` - See IEC 60488-2:2004(E) section 10.31 + + """ + #: Protected user data. The validity of the passed string is not checked. + protected_user_data = Str('*PUD?', '*PUD {}') + + +# ============================================================================= +# --- Calibration ------------------------------------------------------------- +# ============================================================================= + +class IEEECalibration(object): + """A class implementing the optional calibration command. + + * `*CAL?` - See IEC 60488-2:2004(E) section 10.2 + + """ + CALIBRATION: ClassVar[Dict[int, str]] = {0: 'Calibration completed'} + + @Action() + def calibrate(self) -> str: + """Performs a internal self-calibration. + + """ + return self.CALIBRATION.get(int(self.visa_resource.query('*CAL?')), + 'Unknown error') + + +# ============================================================================= +# --- Triggering -------------------------------------------------------------- +# ============================================================================= + +class IEEETrigger(VisaMessageDriver): + """A class implementing the optional trigger command. + + * `*TRG` - See IEC 60488-2:2004(E) section 10.37 + + It is mandatory for devices implementing the DT1 subset. + + """ + + @Action() + def fire_trigger(self) -> None: + """Creates a trigger event. + + """ + self.visa_resource.write('*TRG') + + +# ============================================================================= +# --- Macro trigger ----------------------------------------------------------- +# ============================================================================= + +class IEEETriggerMacro(IEEETrigger): + """A class implementing the optional trigger macro commands. + + * `*DDT` - See IEC 60488-2:2004(E) section 10.4 + * `*DDT?` - See IEC 60488-2:2004(E) section 10.5 + + """ + #: Sequence of commands to execute when receiving a trigger. + trigger_macro = Str('*DDT?', 'DDT {}') + + +# ============================================================================= +# --- Option identification --------------------------------------------------- +# ============================================================================= + +class IEEEOptionsIdentification(VisaMessageDriver): + """A class implementing the option identification command. + + * `*OPT?` - See IEC 60488-2:2004(E) section 10.20 + + """ + #: Mapping between the value returned by the instrument (as a comma + #: separated list) and the names presented to the user. + #: When writing a driver this class variable should be updated and used + #: to generate the names of the feature using for example. + #: instr_options = set_default(names=dict.fromkeys(INSTR_OPTIONS_MAP, + #: bool)) + INSTR_OPTIONS_MAP: ClassVar[Dict[str, str]] = {} + + instr_options = Options('*OPT?', names={'example': bool}) + + @customize('instr_options', 'post_get') + def _convert_options(feat, driver, value): + """Split the returned value and identify the options. + + """ + options = dict.fromkeys(feat.names, False) + options.update({driver.INSTR_OPTIONS_MAP[k]: True + for k in value.split(',')}) + return options + + +# ============================================================================= +# --- Stored settings --------------------------------------------------------- +# ============================================================================= + +class IEEEStoredSettings(VisaMessageDriver): + """A class implementing the stored setting commands. + + * `*RCL` - See IEC 60488-2:2004(E) section 10.29 + * `*SAV` - See IEC 60488-2:2004(E) section 10.33 + + """ + @Action() + def recall(self, idx) -> None: + """Restores the current settings from a copy stored in local memory. + + Parameters + --------- + idx : int + Specifies the memory slot. + + """ + self.visa_resource.write('*RCL {}'.format(idx)) + self.clear_cache() + + @Action() + def save(self, idx) -> None: + """Stores the current settings of a device in local memory. + + Parameters + ---------- + idx : int + Specifies the memory slot. + + """ + self.visa_resource.write('*SAV {}'.format(idx)) diff --git a/i3py/drivers/common/rs232.py b/i3py/drivers/common/rs232.py new file mode 100644 index 0000000..9473f76 --- /dev/null +++ b/i3py/drivers/common/rs232.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Base class for driver supporting the VISA RS232 communication protocol. + +This class ensures that the instrument is always in remote mode before +sending any other command. + +""" +from types import MethodType + +from i3py.backends.visa import VisaMessageDriver +from pyvisa.resources.serial import SerialInstrument + + +class VisaRS232(VisaMessageDriver): + """Base class for all instruments supporting the RS232 interface. + + The specifity of the RS232 interface is that the device need to be switched + to remote mode before sending any command. This class wrapps the low-level + write method of the ressource when the connection is opened in RS232 mode + and prepend the RS232_HEADER string to the message. + + """ + #: Header to add to the message to switch the instrument in remote mode + #: This HAVE TO BE A BYTE STRING and should include the character + #: separating the two messages. + RS232_HEADER = b'' + + def initialize(self): + """Initialize the driver and if pertinent wrap low level so that + RS232_HEADER is prepended to messages. + + """ + super(VisaRS232, self).initialize() + if isinstance(self._resource, SerialInstrument) and self.RS232_HEADER: + write_raw = self._resource.write_raw.__func__ + + def new_write(self, message): + return write_raw(self, self.RS232_HEADER + message) + self._resource.write_raw = MethodType(new_write, self._resource) diff --git a/i3py/drivers/common/scpi/__init__.py b/i3py/drivers/common/scpi/__init__.py new file mode 100644 index 0000000..595678b --- /dev/null +++ b/i3py/drivers/common/scpi/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Common utility to deal with drivers based on SCPI protocol. + +""" diff --git a/i3py/drivers/common/scpi/error_reading.py b/i3py/drivers/common/scpi/error_reading.py new file mode 100644 index 0000000..44b2151 --- /dev/null +++ b/i3py/drivers/common/scpi/error_reading.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Base driver for instrument implementing SCPI error reporting commands. + +""" +from typing import Tuple + +from i3py.core.actions import Action +from i3py.backends.visa import VisaMessageDriver + + +class SCPIErrorReading(VisaMessageDriver): + """Base class for all instruments implementing 'SYST:ERR?'. + + """ + + @Action() + def read_error(self) -> Tuple[int, str]: + """Read the first error in the error queue. + + If an unhandled error occurs, the error queue should be polled till it + is empty. + + """ + code, msg = self.visa_resource.query('SYST:ERR?').split(',', 1) + return int(code), msg + + def default_check_operation(self, feat, value, i_value, response): + """Check if an error is present in the error queue. + + """ + code, msg = self.read_error() + return not bool(code), msg diff --git a/i3py/drivers/common/scpi/rs232.py b/i3py/drivers/common/scpi/rs232.py new file mode 100644 index 0000000..971752f --- /dev/null +++ b/i3py/drivers/common/scpi/rs232.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Base class for SCPI instruments supporting RS232-based communication. + +""" +from ..rs232 import VisaRS232 + + +class SCPIRS232(VisaRS232): + """Base class for SCPI compliant instruments supporting the RS232 protocol. + + """ + + RS232_HEADER = b'SYST:REM:;' diff --git a/i3py/drivers/hp/__init__.py b/i3py/drivers/hp/__init__.py new file mode 100644 index 0000000..3f55077 --- /dev/null +++ b/i3py/drivers/hp/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Alias package for the Keysight package. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +from .. import keysight + +sys.modules[__name__] = LazyPackage({}, __name__, __doc__, locals()) diff --git a/i3py/drivers/itest/__init__.py b/i3py/drivers/itest/__init__.py new file mode 100644 index 0000000..0edaf6b --- /dev/null +++ b/i3py/drivers/itest/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Itest instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {'BN100': 'racks.BN100', 'BN101': 'racks.BN101', + 'BN103': 'racks.BN103', 'BN105': 'racks.BN105'} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/itest/modules/__init__.py b/i3py/drivers/itest/modules/__init__.py new file mode 100644 index 0000000..4b598c8 --- /dev/null +++ b/i3py/drivers/itest/modules/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of ITest modules. + +""" +import sys +from i3py.core.lazy_package import LazyPackage +from .common import make_card_detector + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/itest/modules/be21xx.py b/i3py/drivers/itest/modules/be21xx.py new file mode 100644 index 0000000..cccf575 --- /dev/null +++ b/i3py/drivers/itest/modules/be21xx.py @@ -0,0 +1,496 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the Itest BE21xx voltage source card. + +""" +from typing import Callable, Optional + +from i3py.core import (FloatLimitsValidator, channel, customize, set_feat, + subsystem, limit) +from i3py.core.actions import Action +from i3py.core.features import Float, Str, Bool, constant +from i3py.core.job import InstrJob +from i3py.core.unit import FLOAT_QUANTITY, to_float +from stringparser import Parser + +from ...base.dc_sources import (DCPowerSourceWithMeasure, + DCSourceTriggerSubsystem) +from ...base.identity import Identity +from .common import BiltModule + + +class BE21xx(BiltModule, DCPowerSourceWithMeasure): + """Driver for the Bilt BE2100 high precision dc voltage source. + + """ + __version__ = '0.1.0' + + #: Identity support (we do not use IEEEIdentity because we are not on the + #: root level). + identity = subsystem(Identity) + + with identity as i: + + #: Format string specifying the format of the IDN query answer and + #: allowing to extract the following information: + #: - manufacturer: name of the instrument manufacturer + #: - model: name of the instrument model + #: - serial: serial number of the instrument + #: - firmware: firmware revision + #: ex {manufacturer},<{model}>,SN{serial}, Firmware revision {firmware} + i.IEEE_IDN_FORMAT = ('{_:d},"{manufacturer:s} {model:s}BB/{_:s}' + '/SN{serial:s}\\{_:s} LC{_:s} VL{firmware:s}' + '\\{_:d}"') + + i.manufacturer = set_feat(getter='*IDN?') + i.model = set_feat(getter='*IDN?') + i.serial = set_feat(getter='*IDN?') + i.firmware = set_feat(getter='*IDN?') + + def _post_getter(feat, driver, value): + """Get the identity info from the *IDN?. + + """ + infos = Parser(driver.IEEE_IDN_FORMAT)(value) + driver._cache.update(infos) + return infos.get(feat.name, '') + + for f in ('manufacturer', 'model', 'serial', 'firmware'): + setattr(i, '_post_get_' + f, + customize(f, 'post_get')(_post_getter)) + + #: DC outputs + outputs = channel((0,)) + + with outputs as o: + + o.enabled = set_feat(getter='OUTP?', setter='OUTP {}', + mapping={False: '0', True: '1'} + ) + + o.voltage = set_feat(getter='VOLT?', setter='VOLT {:E}', + limits='voltage') + + o.voltage_range = set_feat(getter='VOLT:RANG?', setter='VOLT:RANG {}', + values=(1.2, 12), extract='{},{_}', + checks=(None, 'not driver.enabled'), + discard={'features': ('voltage',), + 'limits': ('voltage',)}) + + #: Specify stricter voltage limitations than the ones linked to the + #: range. + o.voltage_saturation = subsystem() + with o.voltage_saturation as vs: + + #: Is the low voltage limit enabled. If this conflict with the + #: current voltage, the voltage will clipped to th smallest allowed + #: value. + vs.low_enabled = Bool('VOLT:SAT:NEG?', 'VOLT:SAT:NEG {}', + discard={'features': ('.voltage',), + 'limits': ('.voltage',)}) + + @vs + @customize('low_enabled', 'post_get', ('prepend',)) + def _convert_low_answer(feat, driver, value): + return not value == 'MIN' + + @vs + @customize('low_enabled', 'pre_set', ('append',)) + def _prepare_low_answer(feat, driver, value): + if value: + return driver.low + else: + return 'MIN' + + #: Is the high voltage limit enabled. If this conflict with the + #: current voltage, the voltage will clipped to th largest allowed + #: value. + vs.high_enabled = Bool('VOLT:SAT:POS?', 'VOLT:SAT:POS {}', + discard={'features': ('.voltage',), + 'limits': ('.voltage',)}) + + @vs + @customize('high_enabled', 'post_get', ('prepend',)) + def _convert_high_answer(feat, driver, value): + return not value == 'MAX' + + @vs + @customize('high_enabled', 'pre_set', ('append',)) + def _prepare_high_answer(feat, driver, value): + if value: + return driver.high + else: + return 'MAX' + + #: Lowest allowed voltage. If this conflict with the current + #: voltage, the voltage will clipped to th smallest allowed + #: value. + vs.low = Float('VOLT:SAT:NEG?', 'VOLT:SAT:NEG {}', unit='V', + limits=(-12, 0), + discard={'features': ('.voltage',), + 'limits': ('.voltage',)}) + + #: Highest allowed voltage. If this conflict with the current + #: voltage, the voltage will clipped to th smallest allowed + #: value. + vs.high = Float('VOLT:SAT:POS?', 'VOLT:SAT:POS {}', unit='V', + limits=(0, 12), + discard={'features': ('.voltage',), + 'limits': ('.voltage',)}) + + @vs + @customize('low', 'post_get', ('prepend',)) + def _convert_min(feat, driver, value): + if value == 'MIN': + value = '-12' + return value + + @vs + @customize('high', 'post_get', ('prepend',)) + def _convert_max(feat, driver, value): + if value == 'MAX': + value = '12' + return value + + o.current = set_feat(getter=constant(0.2)) + + o.current_range = set_feat(getter=constant(0.2)) + + #: Subsystem handling triggering and reaction to triggering. + o.trigger = subsystem(DCSourceTriggerSubsystem) + with o.trigger as tr: + #: Type of response to triggering : + #: - disabled : immediate update of voltage every time the voltage + #: feature is updated. + #: - slope : update after receiving a trigger based on the slope + #: value. + #: - stair : update after receiving a trigger using step_amplitude + #: and step_width. + #: - step : increment by one step_amplitude till target value for + #: each triggering. + #: - auto : update after receiving a trigger by steps but + #: determining when to move to next step based on voltage + #: sensing. + tr.mode = Str('TRIG:IN?', 'TRIG:IN {}', + mapping={'disabled': '0', 'slope': '1', + 'stair': '2', 'step': '4', 'auto': '5'}) + + #: The only valid source for the trigger is software trigger. + tr.source = Str(constant('software')) + + #: Delay to wait after receiving a trigger event before reacting. + tr.delay = set_feat(getter='TRIG:IN:DEL?', setter='TRIG:IN:DEL {}', + unit='ms', limits=(0, 60000, 1)) + + #: Voltage slope to use in slope mode. + tr.slope = Float('VOLT:SLOP?', 'VOLT:SLOP {}', unit='V/ms', + limits=(1.2e-6, 1)) + + #: High of each update in stair and step mode. + tr.step_amplitude = Float('VOLT:ST:AMPL?', 'VOLT:ST:AMPL {}', + unit='V', limits='voltage') + + #: Width of each step in stair mode. + tr.step_width = Float('VOLT:ST:WID?', 'VOLT:ST:WID {}', unit='ms', + limits=(100, 60000, 1)) + + #: Absolute threshold value of the settling tracking comparator. + tr.ready_amplitude = Float('TRIG:READY:AMPL?', + 'TRIG:READY:AMPL {}', + unit='V', limits=(1.2e-6, 1)) + + @tr + @Action(retries=1) + def fire(self): + """Send a software trigger. + + """ + msg = self.parent._header_() + 'TRIG:IN:INIT' + self.root.visa_resource.write(msg) + + @tr + @Action(retries=1) + def is_trigger_ready(self) -> bool: + """Check if the output is within ready_amplitude of the target + value. + + """ + msg = self.parent._header_() + 'TRIG:READY?' + return bool(int(self.root.visa_resource.query(msg))) + + #: Status of the output. Save for the first one, they are all related + #: to dire issues that lead to switching off the output. + o.OUTPUT_STATES = {0: 'enabled:constant-voltage', + 5: 'tripped:main-failure', + 6: 'tripped:system-failure', + 7: 'tripped:temperature-failure', + 8: 'unregulated'} + + @o + @Action(retries=1) + def read_output_status(self) -> str: + """Determine the current status of the output. + + """ + if not self.enabled: + return 'disabled' + msg = self._header_() + 'LIM:FAIL?' + answer = int(self.root.visa_resource.query(msg)) + # If a failure occurred the whole card switches off. + if answer != 0: + for o in self.parent.output: + del self.enabled + return self.OUTPUT_STATES.get(answer, f'unknown({answer})') + + @o + @Action(retries=1) + def clear_output_status(self) -> None: + """Clear the error condition of the output. + + This must be called after a failure before switching the output + back on + + """ + self.root.visa_resource.write(self._header_() + 'LIM:CLEAR') + new_status = self.read_output_status() + if 'tripped' in new_status or 'unregulated' in new_status: + _, err = self.root.read_error() + raise RuntimeError('Failed to clear output status. ' + f'Current status is {new_status}, ' + f'the error message is {err}.') + + @o + @Action(retries=1, checks='driver.trigger.mode != "disabled"') + def read_voltage_status(self) -> str: + """Progression of the current voltage update. + + This action return meaningful values if we use a triggered setting + of the output. + + Returns + ------- + status: {'waiting', 'settled', 'changing'} + Status of the output voltage. + + """ + msg = self._header_() + 'VOLT:STAT?' + status = float(self.root.visa_resource.query(msg)) + if status == 1: + return 'settled' + elif status == 0: + return 'waiting' + else: + return 'changing' + + @o + @Action(unit=((None, None, None), 'V'), + values={'quantity': ('voltage',)}, retries=1) + def measure(self, quantity, **kwargs) -> FLOAT_QUANTITY: + """Measure the output voltage. + + """ + msg = self._header_() + 'MEAS:VOLT?' + return float(self.root.visa_resource.query(msg)) + + @o + @Action(checks=('not (method == "voltage_status" and' + ' self.trigger.mode == "disabled")'), + values={'method': ('measure', 'trigger_ready', + 'voltage_status')} + ) + def wait_for_settling(self, + method: str='measure', + stop_on_break: bool=True, + break_condition_callable: + Optional[Callable[[], bool]]=None, + timeout: float=15, + refresh_time: float=1, + tolerance: float=1e-5) -> bool: + """Wait for the output to settle. + + Parameters + ---------- + method : {'measure', 'trigger_ready', 'voltage_status'} + Method used to estimate that the target voltage was reached. + - 'measure': measure the output voltage and compare to target + within tolerance (see tolerance) + - 'trigger_ready': rely on the trigger ready status. + - 'voltage_status': rely on the voltage status reading, this + does work only for triggered settings. + + stop_on_break : bool, optional + Should the ramp be stopped if the break condition is met. This + is achieved through a quick measurement of the output followed + by a setting and a trigger. + + break_condition_callable : Callable, optional + Callable indicating that we should stop waiting. + + timeout : float, optional + Time to wait in seconds in addition to the expected condition + time before breaking. + + refresh_time : float, optional + Time interval at which to check the break condition. + + tolerance : float, optional + Tolerance used to determine that the target was reached when + using the measure method. + + Returns + ------- + result : bool + Boolean indicating if the wait succeeded of was interrupted. + + """ + def stop_ramp(): + # We round to ensure that we never get any range issue + self.voltage = round(self.measure('voltage'), 4) + self.trigger.fire() + + if method == "measure": + def has_reached_target(): + if 'tripped' in self.read_output_status(): + raise RuntimeError(f'Output {self.id} tripped') + return abs(self.voltage - self.measure('voltage')) + elif method == "trigger_ready": + def has_reached_target(): + if 'tripped' in self.read_output_status(): + raise RuntimeError(f'Output {self.id} tripped') + return self.trigger.is_trigger_ready() + else: + def has_reached_target(): + if 'tripped' in self.read_output_status(): + raise RuntimeError(f'Output {self.id} tripped') + return self.read_voltage_status() == 'settled' + + job = InstrJob(has_reached_target, 1, cancel=stop_ramp) + result = job.wait_for_completion(break_condition_callable, + timeout, refresh_time) + if not result and stop_on_break: + job.cancel() + return result + + # ===================================================================== + # --- Private API ----------------------------------------------------- + # ===================================================================== + + @o + @limit('voltage') + def _limits_voltage(self): + """Compute the voltage limits based on range and saturation. + + """ + rng = to_float(self.voltage_range) + low = max(-rng, + float(self.voltage_saturation.low) + if self.voltage_saturation.low_enabled else -15) + high = min(rng, float(self.voltage_saturation.high) + if self.voltage_saturation.high_enabled else 15) + + return FloatLimitsValidator(low, high, unit='V') + + @o + def _header_(self): + return f'I{self.parent.id};' + + +class BE210x(BE21xx): + """Driver for the Bilt BE2100 high precision dc voltage source. + + """ + __version__ = '0.1.0' + + outputs = channel((0,)) + + with outputs as o: + #: Set the voltage settling filter. Slow 100 ms, Fast 10 ms + o.voltage_filter = Str('VOLT:FIL?', 'VOLT:FIL {}', + mapping={'Slow': '0', 'Fast': '1'}, + checks=(None, 'driver.enabled == False')) + + #: Is the remote sensing of the voltage enabled. + o.remote_sensing = Bool('VOLT:REM?', 'VOLT:REM {}', + mapping={True: '1', False: '0'}, + aliases={True: ('ON', 'On', 'on'), + False: ('OFF', 'Off', 'off')}) + + +class BE2101(BE210x): + """Driver for the Bilt BE2100 high precision dc voltage source. + + """ + __version__ = '0.1.0' + + +class BE214x(BE21xx): + """Driver for the Bilt BE2100 high precision dc voltage source. + + """ + __version__ = '0.1.0' + + #: Identity support (we do not use IEEEIdentity because we are not on the + #: root level). + identity = subsystem() + + with identity as i: + + #: Format string specifying the format of the IDN query answer and + #: allowing to extract the following information: + #: - manufacturer: name of the instrument manufacturer + #: - model: name of the instrument model + #: - serial: serial number of the instrument + #: - firmware: firmware revision + #: ex {manufacturer},<{model}>,SN{serial}, Firmware revision {firmware} + i.IEEE_IDN_FORMAT = ('{_:d},"{manufacturer:s} {model:s}B/{_:s}' + '/SN{serial:s} LC{_:s} VL{firmware:s}' + '\\{_:d}"') + + outputs = channel((0, 1, 2, 3)) + + with outputs as o: + + o.OUTPUT_STATES = {0: 'enabled:constant-voltage', + 11: 'tripped:main-failure', + 12: 'tripped:system-failure', + 17: 'unregulated', + 18: 'tripped:over-current'} + + o.current_limit_behavior = set_feat(getter=constant('trip')) + + def default_get_feature(self, feat, cmd, *args, **kwargs): + """Prepend output selection to command. + + """ + cmd = f'C{self.id + 1};' + cmd + return self.parent.default_get_feature(feat, cmd, *args, **kwargs) + + def default_set_feature(self, feat, cmd, *args, **kwargs): + """Prepend output selection to command. + + """ + cmd = f'C{self.id + 1};' + cmd + return self.parent.default_set_feature(feat, cmd, *args, **kwargs) + + o.trigger = subsystem() + with o.trigger as tr: + # HINT The BE2141 requires the triggering to be disabled before + # changing the triggering mode when the output is enabled. + tr.mode = set_feat(setter='TRIG:IN 0\nTRIG:IN {}') + + @o + def _header_(self): + return f'I{self.parent.id};C{self.id + 1};' + + +class BE2141(BE214x): + """Driver for the Bilt BE2100 high precision dc voltage source. + + """ + __version__ = '0.1.0' diff --git a/i3py/drivers/itest/modules/common.py b/i3py/drivers/itest/modules/common.py new file mode 100644 index 0000000..6b0035f --- /dev/null +++ b/i3py/drivers/itest/modules/common.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Common tools for the Bilt module instruments. + +""" +from typing import Any, Callable, List, Union + +from i3py.core import Channel, subsystem + +from ...base.identity import Identity + + +def make_card_detector(model_id: Union[str, List[str]] + ) -> Callable[[Any], List[str]]: + """Create a function listing the available card of a given model. + + Parameters + ---------- + model_id : str or list of str + Id or ids of the model. ex BE2101 + + """ + if not isinstance(model_id, list): + model_id = [model_id] + + # We strip the leading BE + model_id = set(m.strip('BE') for m in model_id) + + def list_channel(driver): + """Query all the cards fitted on the rack and filter based on the model + + """ + card_list = driver.visa_resource.query('I:L?').split(';') + cards = {int(i): id + for i, id in [card.split(',') for card in card_list]} + + return [index for index in cards if cards[index] in model_id] + + return list_channel + + +class BiltModule(Channel): + """Base driver for module used with the Bilt chassis. + + """ + CHANNEL_ID = 'module_id' + + identity = subsystem(Identity) + + with identity as i: + pass + + def default_get_feature(self, feat, cmd, *args, **kwargs): + """Prepend module selection to command. + + """ + cmd = f'I{self.id};' + cmd + return self.parent.default_get_feature(feat, cmd, *args, **kwargs) + + def default_set_feature(self, feat, cmd, *args, **kwargs): + """Prepend module selection to command. + + """ + cmd = f'I{self.id};' + cmd + return self.parent.default_set_feature(feat, cmd, *args, **kwargs) diff --git a/i3py/drivers/itest/racks.py b/i3py/drivers/itest/racks.py new file mode 100644 index 0000000..9ddac30 --- /dev/null +++ b/i3py/drivers/itest/racks.py @@ -0,0 +1,77 @@ +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Driver for the Itest rack instruments + +""" +from i3py.core import channel + +from ..common.ieee488 import IEEEReset +from ..common.scpi.error_reading import SCPIErrorReading +from .modules.be21xx import BE2101, BE2141 +from .modules import make_card_detector + + +class BiltMainframe(IEEEReset, SCPIErrorReading): + """Driver for the Itest BN100 chassis. + + """ + PROTOCOLS = {'TCPIP': {'resource_class': 'SOCKET', + 'port': '5025'}, + 'GPIB': {'resource_class': 'INSTR'}, + 'ASRL': {'resource_class': 'INSTR'} + } + + DEFAULTS = {'COMMON': {'read_termination': '\n', + 'write_termination': '\n'} + } + + IEEE_RESET_WAIT = 4 + + #: Support for the BE2101 card + be2101 = channel('_list_be2101', BE2101) + + #: Support for the BE2141 card + be2141 = channel('_list_be2141', BE2141) + + def initialize(self): + """Make sure the communication parameters are correctly sets. + + """ + super().initialize() + self.visa_resource.write('SYST:VERB 0') + + _list_be2101 = make_card_detector(['BE2101']) + _list_be2141 = make_card_detector(['BE2141']) + + +class BN100(BiltMainframe): + """Driver for the BN100 Bilt rack. + + """ + __version__ = '0.1.0' + + +class BN101(BiltMainframe): + """Driver for the BN101 Bilt rack. + + """ + __version__ = '0.1.0' + + +class BN103(BiltMainframe): + """Driver for the BN103 Bilt rack. + + """ + __version__ = '0.1.0' + + +class BN105(BiltMainframe): + """Driver for the BN105 Bilt rack. + + """ + __version__ = '0.1.0' diff --git a/i3py/drivers/keysight/E363XA.py b/i3py/drivers/keysight/E363XA.py new file mode 100644 index 0000000..83f26b0 --- /dev/null +++ b/i3py/drivers/keysight/E363XA.py @@ -0,0 +1,588 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Driver for the Keysight E3631A, E3633A and E3634A DC power source. + +Notes +----- + +WARNING : The drivers implemented in this module have been only very lightly +tested. + +""" +from i3py.core import (FloatLimitsValidator, I3pyError, channel, customize, + limit, set_feat, subsystem) +from i3py.core.actions import Action +from i3py.core.features import Alias, Bool, Feature, conditional +from i3py.core.unit import to_float, to_quantity + +from ..base.dc_sources import (DCPowerSourceWithMeasure, + DCSourceProtectionSubsystem, + DCSourceTriggerSubsystem) +from ..common.ieee488 import (IEEEInternalOperations, IEEEPowerOn, + IEEEStatusReporting, IEEEStoredSettings, + IEEESynchronisation, IEEETrigger) +from ..common.scpi.error_reading import SCPIErrorReading + + +class E363xA(DCPowerSourceWithMeasure, IEEEInternalOperations, + IEEEStatusReporting, IEEEStoredSettings, IEEETrigger, + IEEESynchronisation, IEEEPowerOn, SCPIErrorReading): + """Driver for the Keysight E3631A DC power source. + + """ + __version__ = '0.1.0' + + PROTOCOLS = {'GPIB': [{'resource_class': 'INSTR'}], + 'ASRL': [{'resource_class': 'INSTR'}] + } + + DEFAULTS = {'COMMON': {'write_termination': '\n', + 'read_termination': '\n'}} + + outputs = channel((0,)) + + with outputs as o: + + o.enabled = set_feat(getter='OUTP?', setter='OUTP {:d}') + + o.voltage = set_feat( + getter=conditional(('"VOLT?" if driver.trigger.mode != "enabled"' + ' else "VOLT:TRIG?"'), default=True), + setter=conditional(('"VOLT {}" if driver.trigger.mode != "enabled"' + ' else "VOLT:TRIG {}"'), default=True), + limits='voltage') + + o.voltage_range = set_feat(getter='VOLT:RANGE?', + setter='VOLT:RANGE {}') + + o.current = set_feat( + getter=conditional(('"CURR?" if driver.trigger.mode != "enabled"' + ' else "CURR:TRIG?"'), default=True), + setter=conditional(('"CURR {}" if driver.trigger.mode != "enabled"' + ' else "CURR:TRIG {}"'), default=True), + limits='current') + + o.current_range = set_feat(getter='CURR:RANGE?', + setter='CURR:RANGE {}') + + @o + @Action(values={'quantity': ("voltage", "current")}, + lock=True) + def measure(self, quantity, **kwargs): + """Measure the output voltage/current. + + Parameters + ---------- + quantity : str, {'voltage', 'current'} + Quantity to measure. + + **kwargs : + This instrument recognize no optional parameters. + + Returns + ------- + value : float or pint.Quantity + Measured value. If units are supported the value is a Quantity + object. + + """ + cmd = 'MEAS:' + ('VOLT?' if quantity != 'current' else 'CURR?') + value = float(self.parent.visa_resource.query(cmd)) + value = to_quantity(value, 'V' if quantity != 'current' else 'A') + + return value + + @o + @Action(unit=(None, (None, 'V', 'A')), + limits={'voltage': 'voltage', 'current': 'current'}, + discard=('voltage', 'current'), + lock=True) + def apply(self, voltage, current): + """Set both the voltage and current limit. + + """ + self.parent.visa_resource.write(f'APPLY {voltage}, {current}') + res, msg = self.parent.read_error() + if res != 0: + err = 'Failed to apply {}V, {}A to output {} :\n{}' + raise I3pyError(err.format(voltage, current, self.id, msg)) + + o.trigger = subsystem(DCSourceTriggerSubsystem) + + with o.trigger as t: + + # HINT this is a soft feature !!! + t.mode = set_feat(getter=True, setter=True, + values=('disabled', 'enabled')) + + t.source = set_feat(getter='TRIG:SOUR?', setter='TRIG:SOUR {}', + mapping={'immediate': 'IMM', 'bus': 'BUS'}) + + t.delay = set_feat(getter='TRIG:DEL?', setter='TRIG:DEL {}', + limits=(1, 3600, 1)) + + @o + @Action() + def arm(self): + """Prepare the channel to receive a trigger. + + If the trigger mode is immediate the update occurs as soon as + the command is processed. + + """ + with self.lock: + self.write('INIT') + res, msg = self.root.read_error() + if res: + err = 'Failed to arm the trigger for output {}:\n{}' + raise I3pyError(err.format(self.id, msg)) + + # HINT mode is a "soft" feature meaning it has no reality for the + # the instrument. As a consequence having a default value is enough + # the caching does the rest for us. + @t + @customize('mode', 'get') + def _get_mode(feat, driver): + return 'disabled' + + @t + @customize('mode', 'set') + def _set_mode(feat, driver, value): + vrsc = driver.root.visa_resource + vrsc.write(f'VOLT:TRIG {driver.parent.voltage}') + vrsc.write(f'CURR:TRIG {driver.parent.current}') + res, msg = driver.root.read_error() + if res: + err = ('Failed to set the triggered values for voltage ' + 'and current {}:\n{}') + raise I3pyError(err.format(driver.id, msg)) + + +VOLTAGE_RANGES = {0: 6, 1: 25, 2: -25} + +CURRENT_RANGES = {0: 5, 1: 1, 2: 1} + + +class E3631A(E363xA): + """Driver for the Keysight E3631A DC power source. + + """ + __version__ = '0.1.0' + + #: In this model, outputs are always enabled together. + outputs_enabled = Bool('OUTP?', 'OUTP {}', + mapping={True: '1', False: '0'}, + aliases={True: ['On', 'ON', 'On'], + False: ['Off', 'OFF', 'off']}) + + #: Whether to couple together the output triggers, causing a trigger + #: received on one to update the other values. + #: The value is a tuple containing the indexes of the outputs for which the + #: triggers are coupled. + coupled_triggers = Feature('INST:COUP?', 'INST:COUP {}', + checks=(None, ('value is False or ' + 'not driver.outputs_tracking')) + ) + + @customize('coupled_triggers', 'post_get', ('append',)) + def _post_get_coupled_triggers(feat, driver, value): + """Get the currently coupled triggers. + + """ + if value == 'NONE': + return () + elif value == 'ALL': + return (0, 1, 2) + else: + return tuple(i for i, id in enumerate(('P6V', 'P25V', 'N25V')) + if id in value) + + @customize('coupled_triggers', 'pre_set', ('append',)) + def _pre_set_coupled_triggers(feat, driver, value): + """Properly format the value for setting the coupled triggers. + + """ + aliases = driver.outputs.aliases + names = [] + if len(value) != len(set(value)): + raise ValueError('Impossible to couple to identical outputs ' + f'({value})') + for index in value: + if index not in aliases: + raise ValueError(f'Invalid output index: {index}') + names.append(aliases[index]) + + if not names: + return 'NONE' + elif len(names) == 3: + return 'ALL' + else: + return ','.join(names) + + #: Activate tracking between the P25V and the N25V output. In tracking + #: one have P25V.voltage = - N25V + outputs_tracking = Bool('OUTP:TRAC?', + 'OUTP:TRAC {}', + mapping={True: '1', False: '0'}, + aliases={True: ['On', 'ON', 'On'], + False: ['Off', 'OFF', 'off']}, + checks=(None, + ('value is False or ' + 'driver.coupled_triggers is None or ' + '1 not in driver.coupled_triggers or ' + '2 not in driver.coupled_triggers'))) + + outputs = channel((0, 1, 2), + aliases={0: 'P6V', 1: 'P25V', 2: 'N25V'}) + + with outputs as o: + + o.enabled = Alias('.outputs_enabled') # should this be settable ? + + o.voltage_range = set_feat(getter=True) + + o.current_range = set_feat(getter=True) + + @o + @Action(lock=True, values={'quantity': ('voltage', 'current')}) + def measure(self, quantity, **kwargs): + """Measure the output voltage/current. + + Parameters + ---------- + quantity : str, {'voltage', 'current'} + Quantity to measure. + + **kwargs : + This instrument recognize no optional parameters. + + Returns + ------- + value : float or pint.Quantity + Measured value. If units are supported the value is a Quantity + object. + + """ + self.parent.visa_resource.write(f'INST:NSEL {self.id + 1}') + return super(E3631A.outputs, self).measure(quantity, **kwargs) + + @o + @Action(unit=(None, (None, 'V', 'A')), + limits={'voltage': 'voltage', 'current': 'current'}, + discard=('voltage', 'current'), + lock=True) + def apply(self, voltage, current): + """Set both the voltage and current limit. + + """ + self.parent.visa_resource.write(f'INST:NSEL {self.id + 1}') + super(E3631A.outputs, self).apply(voltage, current) + + @o + @Action() + def read_output_status(self): + """Read the status of the output. + + Returns + ------- + status : str, {'disabled', + 'enabled:constant-voltage', + 'enabled:constant-current', + 'tripped:over-voltage', + 'tripped:over-current', + 'unregulated'} + + """ + if not self.enabled: + return 'disabled' + status = int(self.parent.visa_resource( + f'STAT:QUES:INST:ISUM{self.id + 1}?')) + if status & 1: + return 'enabled:constant-voltage' + if status & 2: + return 'enabled:constant-current' + return 'unregulated' + + o.trigger = subsystem(DCSourceTriggerSubsystem) + + with o.trigger as t: + + @o + @Action(lock=True) + def arm(self): + """Prepare the channel to receive a trigger. + + If the trigger mode is immediate the update occurs as soon as + the command is processed. + + """ + self.root.visa_resource.write(f'INSTR:NSEL {self.id + 1}') + super(E3631A.outputs.trigger).arm() + + @o + def default_get_feature(self, feat, cmd, *args, **kwargs): + """Always select the channel before getting. + + """ + self.root.visa_resource.write(f'INST:NSEL {self.id + 1}') + return super(E3631A.outputs, + self).default_get_feature(feat, cmd, *args, **kwargs) + + @o + def default_set_feature(self, feat, cmd, *args, **kwargs): + """Always select the channel before getting. + + """ + self.root.visa_resource.write(f'INST:NSEL {self.id + 1}') + return super(E3631A.outputs, + self).default_set_feature(feat, cmd, *args, **kwargs) + + @o + @customize('voltage', 'post_set', ('append',)) + def _post_setattr_voltage(feat, driver, value, i_value, response): + """Make sure that in tracking mode the voltage cache is correct. + + """ + if driver.id != 0: + del driver.parent.outputs[1].voltage + del driver.parent.outputs[2].voltage + + @o + @customize('voltage_range', 'get') + def _get_voltage_range(feat, driver): + """Get the voltage range. + + """ + return VOLTAGE_RANGES[driver.id] + + @o + @customize('current_range', 'get') + def _get_current_range(feat, driver): + """Get the current range. + + """ + return CURRENT_RANGES[driver.id] + + @o + @limit('voltage') + def _limits_voltage(self): + """Build the voltage limits matching the output. + + """ + if self.id == 'P6V': + return FloatLimitsValidator(0, 6.18, 1e-3, unit='V') + elif self.id == 'P25V': + return FloatLimitsValidator(0, 25.75, 1e-2, unit='V') + else: + return FloatLimitsValidator(-25.75, 0, 1e-2, unit='V') + + @o + @limit('current') + def _limits_current(self): + """Build the current limits matching the output. + + """ + if self.id == 'P6V': + return FloatLimitsValidator(0, 5.15, 1e-3, unit='A') + elif self.id == 'P25V': + return FloatLimitsValidator(0, 1.03, 1e-3, unit='A') + else: + return FloatLimitsValidator(0, 1.03, 1e-3, unit='A') + + +class E3633A(E363xA): + """Driver for the Keysight E3633A DC power source. + + """ + __version__ = '0.1.0' + + outputs = channel((0,)) + + with outputs as o: + + o.voltage_range = set_feat(values=(8, 20)) + + o.current_range = set_feat(values=(20, 10)) + + o.over_voltage_protection = subsystem(DCSourceProtectionSubsystem) + + with o.over_voltage_protection as ovp: + + ovp.enabled = set_feat(getter='VOLT:PROC:STAT?', + setter='VOLT:PROC:STAT {:d}') + + ovp.high_level = set_feat(getter='VOLT:PROT:LEV?', + setter='VOLT:PROT:LEV {}') + + ovp.low_level = set_feat(getter=True, setter=True) + + @ovp + @Action() + def read_status(self) -> str: + """Read the status of the voltage protection + + """ + return ('tripped' + if self.root.visa_resource.query('VOLT:PROT:TRIP?') + else 'working') + + @ovp + @Action() + def clear(self) -> None: + """Clear the voltage protection status. + + """ + root = self.root + root.visa_resource.write('VOLT:PROT:CLEAR') + res, msg = root.read_error() + if res: + raise I3pyError( + f'Failed to clear voltage protection: {msg}') + + @ovp + @customize('low_level', 'get') + def _get_low_level(feat, driver): + return - driver.high_level + + @ovp + @customize('low_level', 'set') + def _set_low_level(feat, driver, value): + driver.high_level = - value + + o.over_current_protection = subsystem(DCSourceProtectionSubsystem) + + with o.over_current_protection as ocp: + + ovp.enabled = set_feat(getter='CURR:PROC:STAT?', + setter='CURR:PROC:STAT {:d}') + + ovp.high_level = set_feat(getter='CURR:PROT:LEV?', + setter='CURR:PROT:LEV {}') + + ovp.low_level = set_feat(getter=True, setter=True) + + @ovp + @Action() + def read_status(self) -> str: + """Read the status of the voltage protection + + """ + return ('tripped' + if self.root.visa_resource.query('CURR:PROT:TRIP?') + else 'working') + + @ovp + @Action() + def clear(self) -> None: + """Clear the voltage protection status. + + """ + root = self.root + root.visa_resource.write('CURR:PROT:CLEAR') + res, msg = root.read_error() + if res: + raise I3pyError( + f'Failed to clear voltage protection: {msg}') + + @ovp + @customize('low_level', 'get') + def _get_low_level(feat, driver): + return - driver.high_level + + @ovp + @customize('low_level', 'set') + def _set_low_level(feat, driver, value): + driver.high_level = - value + + @o + @Action() + def read_output_status(self): + """Read the status of the output. + + Returns + ------- + status : str, {'disabled', + 'enabled:constant-voltage', + 'enabled:constant-current', + 'tripped:over-voltage', + 'tripped:over-current', + 'unregulated'} + + """ + status = self.parent.visa_resource.query('STAT:QUES:COND?') + if status == '0': + return 'disabled' if not self.enabled else 'unregulated' + elif status == '1': + return 'enabled:constant-voltage' + elif status == '2': + return 'enabled:constant-current' + else: + if self.over_voltage_protection.read_status() == 'tripped': + return 'tripped:over-voltage' + else: + return 'tripped:over-current' + + @o + @limit('voltage') + def _limits_voltage(self): + """Build the voltage limits. + + """ + if to_float(self.voltage_range) == 8: + return FloatLimitsValidator(0, 8.24, 1e-3, unit='V') + else: + return FloatLimitsValidator(0, 20.6, 1e-2, unit='V') + + @o + @limit('current') + def _limits_current(self): + """Build the current limits. + + """ + if to_float(self.current_range) == 20: + return FloatLimitsValidator(0, 20.60, 1e-3, unit='A') + else: + return FloatLimitsValidator(0, 10.3, 1e-3, unit='A') + + +class E3634A(E3633A): + """Driver for the Keysight E3634A DC power source. + + """ + __version__ = '0.1.0' + + outputs = channel((0,)) + + with outputs as o: + + o.voltage_range = set_feat(values=(25, 50)) + + o.current_range = set_feat(values=(7, 4)) + + @o + @limit('voltage') + def _limits_voltage(self): + """Build the voltage limits based on the range. + + """ + if to_float(self.voltage_range) == 25: + return FloatLimitsValidator(0, 25.75, 1e-3, unit='V') + else: + return FloatLimitsValidator(0, 51.5, 1e-3, unit='V') + + @o + @limit('current') + def _limits_current(self): + """Build the current limits based on the range. + + """ + if to_float(self.current_range) == 7: + return FloatLimitsValidator(0, 7.21, 1e-3, unit='A') + else: + return FloatLimitsValidator(0, 4.12, 1e-3, unit='A') diff --git a/i3py/drivers/keysight/__init__.py b/i3py/drivers/keysight/__init__.py new file mode 100644 index 0000000..c28f606 --- /dev/null +++ b/i3py/drivers/keysight/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Keysight/Agilent/HP instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {'E3631A': 'E363XA.E3631A', 'E3633A': 'E363XA.E3633A', + 'E3634A': 'E363XA.E3634A'} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/oxford/__init__.py b/i3py/drivers/oxford/__init__.py new file mode 100644 index 0000000..4701e3d --- /dev/null +++ b/i3py/drivers/oxford/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Oxford instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/rigol/__init__.py b/i3py/drivers/rigol/__init__.py new file mode 100644 index 0000000..126a56a --- /dev/null +++ b/i3py/drivers/rigol/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Rigol instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/rohde_schwarz/__init__.py b/i3py/drivers/rohde_schwarz/__init__.py new file mode 100644 index 0000000..86b6af4 --- /dev/null +++ b/i3py/drivers/rohde_schwarz/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Rohde&Schwarz instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/signal_recovery/__init__.py b/i3py/drivers/signal_recovery/__init__.py new file mode 100644 index 0000000..2e1702e --- /dev/null +++ b/i3py/drivers/signal_recovery/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Signal Recovery instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/sp_devices/__init__.py b/i3py/drivers/sp_devices/__init__.py new file mode 100644 index 0000000..62e4405 --- /dev/null +++ b/i3py/drivers/sp_devices/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of SP devices instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/stanford_research/__init__.py b/i3py/drivers/stanford_research/__init__.py new file mode 100644 index 0000000..8b338bf --- /dev/null +++ b/i3py/drivers/stanford_research/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Stanford research instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/tektronix/__init__.py b/i3py/drivers/tektronix/__init__.py new file mode 100644 index 0000000..ef2867e --- /dev/null +++ b/i3py/drivers/tektronix/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Tektronix instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/yokogawa/__init__.py b/i3py/drivers/yokogawa/__init__.py new file mode 100644 index 0000000..3703e71 --- /dev/null +++ b/i3py/drivers/yokogawa/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Package for the drivers of Yokogawa instruments. + +""" +import sys +from i3py.core.lazy_package import LazyPackage + +DRIVERS = {'GS200': 'gs200.GS200', 'Model7651': 'model_7651.Model7651'} + +sys.modules[__name__] = LazyPackage(DRIVERS, __name__, __doc__, locals()) diff --git a/i3py/drivers/yokogawa/gs200.py b/i3py/drivers/yokogawa/gs200.py new file mode 100644 index 0000000..2600075 --- /dev/null +++ b/i3py/drivers/yokogawa/gs200.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Driver for the Yokogawa GS200 DC power source. + +""" +from i3py.core import FloatLimitsValidator, channel, customize, limit, set_feat +from i3py.core.actions import Action +from i3py.core.features import Str, conditional, constant +from i3py.core.unit import to_float + +from ..base.dc_sources import DCPowerSource +from ..common.ieee488 import (IEEEInternalOperations, IEEEOperationComplete, + IEEEOptionsIdentification, IEEEStatusReporting, + IEEEStoredSettings) +from ..common.scpi.error_reading import SCPIErrorReading + +VOLTAGE_RESOLUTION = {10e-3: 1e-7, + 100e-3: 1e-6, + 1.0: 1e-5, + 10: 1e-4, + 30: 1e-3} + +CURRENT_RESOLUTION = {1e-3: 1e-8, + 10e-3: 1e-7, + 100e-3: 1e-6, + 200e-3: 1e-6} + + +class GS200(DCPowerSource, IEEEInternalOperations, + IEEEStatusReporting, IEEEOperationComplete, + IEEEOptionsIdentification, IEEEStoredSettings, + SCPIErrorReading): + """Driver for the Yokogawa GS200 DC power source. + + Because the over-voltage (current) protection is always enabled in + current (voltage) mode, they basically act as limits and fullfill the same + role as target voltage (current) for a power source lacking mode selection. + As a consequence they are implemented in the same way. + + Notes + ----- + - the measurement option is not yet supported. + - add support for programs + - add RS232 support + + """ + __version__ = '0.1.0' + + PROTOCOLS = {'GPIB': [{'resource_class': 'INSTR'}], + 'USB': [{'resource_class': 'INSTR', + 'manufacturer_id': '0xB21', + 'model_code': '0x39'}], + 'TCPIP': [{'resource_class': 'INSTR'}] + } + + DEFAULTS = {'COMMON': {'read_termination': '\n', + 'write_termination': '\n'}} + + outputs = channel((0,)) + + with outputs as o: + #: Preferential working mode for the source. In voltage mode, the + #: source tries to work as a voltage source, the current settings is + #: simply used to protect the sample. In current mode it is the + #: opposite. Changing the mode cause the output to be disabled. + o.mode = Str(getter=':SOUR:FUNC?', + setter=':SOUR:FUNC {}', + mapping={'voltage': 'VOLT', 'current': 'CURR'}, + discard={'features': ('enabled', + 'voltage', 'voltage_range', + 'current', 'current_range'), + 'limits': ('voltage', 'current')}) + + o.enabled = set_feat(getter=':OUTP?', setter=':OUTP {}', + mapping={False: '0', True: '1'}) + + o.voltage = set_feat( + getter=conditional('":SOUR:LEV?" if driver.mode == "voltage" ' + 'else ":SOUR:PROT:VOLT?"', default=True), + setter=conditional('":SOUR:LEV {}" if driver.mode == "voltage" ' + 'else ":SOUR:PROT:VOLT {}"', default=True), + limits='voltage') + + o.voltage_range = set_feat(getter=True, + setter=':SOUR:RANG {}', + checks=(None, 'driver.mode == "voltage"'), + values=tuple(VOLTAGE_RESOLUTION.keys()), + discard={'limits': ('voltage',)}) + + o.current_limit_behavior = set_feat(getter=constant("regulate")) + + o.current = set_feat(getter=True, + setter=True, + limits='current') + + o.current_range = set_feat(getter=True, + setter=':SOUR:RANG {}', + values=tuple(CURRENT_RESOLUTION.keys()), + discard={'limits': 'current'}) + + o.voltage_limit_behavior = set_feat(getter=constant("regulate")) + + @o + @Action() + def read_output_status(self): + """Determine the status of the output. + + Returns + ------- + status : str, {'disabled', + 'enabled:constant-voltage', + 'enabled:constant-voltage', + 'tripped:unknown', 'unregulated'} + + """ + if not self.enabled: + return 'disabled' + event = int(self.root.visa_resource.query(':STAT:EVENT?')) + if event & 2**12: + del self.enabled + return 'tripped:unknown' + elif (event & 2**11) or (event & 2**10): + if self.mode == 'voltage': + return 'enabled:constant-current' + else: + return 'enabled:constant-voltage' + else: + if self.mode == 'voltage': + return 'enabled:constant-voltage' + else: + return 'enabled:constant-current' + + # TODO add support for options and measuring subsystem (change + # inheritance) + + # ===================================================================== + # --- Private API ----------------------------------------------------- + # ===================================================================== + + @o + @customize('current', 'get') + def _get_current(feat, driver): + """Get the target/limit current. + + """ + if driver.mode != 'current': + if to_float(driver.voltage_range) in (10e-3, 100e-3): + return 0.2 + else: + return driver.default_get_feature(feat, ':SOUR:PROT:CURR?') + return driver.default_get_feature(feat, ':SOUR:LEV?') + + @o + @customize('current', 'set') + def _set_current(feat, driver, value): + """Set the target/limit current. + + In voltage mode this is only possible if the range is 1V or greater + + """ + if driver.mode != 'current': + if to_float(driver.voltage_range) in (10e-3, 100e-3): + raise ValueError('Cannot set the current limit for ranges ' + '10mV and 100mV') + else: + return driver.default_set_feature(feat, + ':SOUR:PROT:CURR {}', + value) + return driver.default_set_feature(feat, ':SOUR:LEV {}', value) + + @o + @customize('voltage_range', 'get') + def _get_voltage_range(feat, driver): + """Get the voltage range depending on the mode. + + """ + if driver.mode == 'voltage': + return driver.default_get_feature(feat, ':SOUR:RANG?') + return '30' + + @o + @customize('current_range', 'get') + def _get_current_range(feat, driver): + """Get the current range depending on the mode. + + """ + if driver.mode == 'current': + return driver.default_get_feature(feat, ':SOUR:RANG?') + return '0.2' + + @o + @limit('voltage') + def _limits_voltage(self): + """Determine the voltage limits based on the currently selected + range. + + """ + if self.mode == 'voltage': + ran = to_float(self.voltage_range) + res = VOLTAGE_RESOLUTION[ran] + if ran != 30.0: + ran *= 1.2 + else: + ran = 32.0 + return FloatLimitsValidator(-ran, ran, res, 'V') + else: + return FloatLimitsValidator(1, 30, 1, 'V') + + @o + @limit('current') + def _limits_current(self): + """Determine the current limits based on the currently selected + range. + + """ + if self.mode == 'current': + ran = to_float(self.current_range) # Casting handling Quantity + res = CURRENT_RESOLUTION[ran] + if ran != 200e-3: + ran *= 1.2 + else: + ran = 220e-3 + return FloatLimitsValidator(-ran, ran, res, 'A') + else: + return FloatLimitsValidator(1e-3, 0.2, 1e-3, 'A') diff --git a/i3py/drivers/yokogawa/model_7651.py b/i3py/drivers/yokogawa/model_7651.py new file mode 100644 index 0000000..9982ded --- /dev/null +++ b/i3py/drivers/yokogawa/model_7651.py @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Driver for the Yokogawa 7651 DC power source. + +""" +from i3py.core import (set_feat, set_action, channel, subsystem, + limit, customize, FloatLimitsValidator, I3pyError) +from i3py.core.features import Str, conditional, constant +from i3py.core.unit import to_float +from i3py.core.actions import Action, RegisterAction +from i3py.backends.visa import VisaMessageDriver +from stringparser import Parser + +from ..base.dc_sources import DCPowerSource +from ..base.identity import Identity + + +VOLTAGE_RESOLUTION = {10e-3: 1e-7, + 100e-3: 1e-6, + 1.0: 1e-5, + 10: 1e-4, + 30: 1e-3} + +CURRENT_RESOLUTION = {1e-3: 1e-8, + 10e-3: 1e-7, + 100e-3: 1e-6} + + +class Model7651(VisaMessageDriver, DCPowerSource): + """Driver for the Yokogawa 7651 DC power source. + + This driver can also be used on Yokogawa GS200 used in compatibility mode. + + Because the over-voltage (current) protection is always enabled in + current (voltage) mode, they basically act as limits and fully the same + role as target voltage (current) for a power source lacking mode selection. + As a consequence they are implemented in the same way. + + Notes + ----- + - we should check the mode on startup (is it only possible ?) + - ideally we should not keep the VisaSession opened on GPIB + - add support for programs + - add RS232 support + + """ + __version__ = '0.1.0' + + PROTOCOLS = {'GPIB': [{'resource_class': 'INSTR'}]} + + DEFAULTS = {'COMMON': {'read_termination': '\r\n'}, + 'ASRL': {'write_termination': '\r\n'}} + + def initialize(self): + """Set the data termination. + + """ + print(self.resource_name) + try: + super().initialize() + # Choose the termination character + self.visa_resource.write('DL0') + # Unmask the status byte by default. + self.visa_resource.write('MS31') + # Clear the status byte + self.read_status_byte() + except Exception as e: + raise I3pyError('Connection failed to open. One possible reason ' + 'is because the instrument is configured to write ' + 'on the memory card.') from e + + @RegisterAction(('program_setting', # Program is currently edited + 'program_execution', # Program under execution + 'error', # Previous command error + 'output_unstable', + 'output_on', + 'calibration mode', + 'ic_memory_card', + 'cal_switch')) + def read_status_code(self): # Should this live in a subsystem ? + """Read the status code. + + """ + # The return format is STS1={value} + return int(self.visa_resource.query('OC')[5:]) + + read_status_byte = set_action(names=('end_of_output_change', + 'srq_key_on', + 'syntax_error', + 'limit_error', + 'program_end', + 'error', + 'request', + None)) + + def is_connected(self): + """Check whether or not the connection is opened. + + """ + try: + self.visa_resource.query('OC') + except Exception: + return False + + return True + + identity = subsystem(Identity) + + with identity as i: + + i.manufacturer = set_feat(getter=constant('Yokogawa')) + + i.model = set_feat(getter=True) + + i.serial = set_feat(getter=constant('xxx')) + + i.firmware = set_feat(getter=True) + + @i + def _get_from_os(driver, index): + """Read the requested info from OS command. + + """ + parser = Parser('MDL{}REV{}') + visa_rsc = driver.parent.visa_resource + mes = visa_rsc.query('OS') + visa_rsc.read() + visa_rsc.read() + visa_rsc.read() + visa_rsc.read() + return parser(mes)[index] + + @i + @customize('model', 'get') + def _get_model(feat, driver): + return driver._get_from_os(0) + + @i + @customize('firmware', 'get') + def _get_firmware(feat, driver): + return driver._get_from_os(1) + + outputs = channel((0,)) + + with outputs as o: + o.mode = Str('OD', 'F{}E', + mapping=({'voltage': '1', 'current': '5'}, + {'V': 'voltage', 'A': 'current'}), + extract='{_}DC{}{_:+E}', + discard={'features': ('enabled', 'voltage', 'current', + 'voltage_range', 'current_range'), + 'limits': ('current', 'voltage')}) + + o.enabled = set_feat(getter=True, + setter='O{}E', + mapping=({True: 1, False: 0}, None)) + + o.voltage = set_feat( + getter=True, + setter=conditional('"S{:+E}E" if driver.mode == "voltage" ' + 'else "LV{:.0f}"', default=True), + limits='voltage') + + o.voltage_range = set_feat(getter=True, + setter='R{}E', + extract='F1R{:d}S{_}', + checks=(None, 'driver.mode == "voltage"'), + mapping={10e-3: 2, 100e-3: 3, 1.0: 4, + 10.0: 5, 30.0: 6}, + discard={'features': ('current',), + 'limits': ('voltage',)}) + + o.current = set_feat(getter=True, + setter=True, + limits='current') + + o.current_range = set_feat(getter=True, + setter='R{}E', + extract='F5R{:d}S{_}', + checks=(None, 'driver.mode == "current"'), + mapping={1e-3: 4, 10e-3: 5, 100e-3: 6}, + discard={'limits': ('current',)}) + + @o + @Action() + def read_output_status(self): + """Determine the status of the output. + + Returns + ------- + status : str, {'disabled', + 'enabled:constant-voltage', + 'enabled:constant-voltage', + 'tripped:unknown', 'unregulated'} + + """ + if not self.enabled: + return 'disabled' + sc = self.parent.read_status_code() + if sc & sc.output_unstable: + return 'unregulated' + elif not (sc & sc.output_on): + return 'tripped:unknown' + if self.parent.visa_resource.query('OD')[0] == 'E': + if self.mode == 'voltage': + return 'enabled:constant-current' + else: + return 'enabled:constant-voltage' + if self.mode == 'voltage': + return 'enabled:constant-voltage' + else: + return 'enabled:constant-current' + + # ===================================================================== + # --- Private API ----------------------------------------------------- + # ===================================================================== + + @o + def default_check_operation(self, feat, value, i_value, state=None): + """Check that the operation did not result in any error. + + """ + stb = self.parent.read_status_byte() + if stb & stb.error: + return False, ('Syntax error' if stb & stb.syntax_error else + 'Limit error') + + return True, None + + @o + @limit('voltage') + def _limits_voltage(self): + """Determine the voltage limits based on the currently selected + range. + + """ + if self.mode == 'voltage': + ran = to_float(self.voltage_range) + res = VOLTAGE_RESOLUTION[ran] + if ran != 30.0: + ran *= 1.2 + else: + ran = 32.0 + return FloatLimitsValidator(-ran, ran, res, 'V') + else: + return FloatLimitsValidator(1, 30, 1, 'V') + + @o + @limit('current') + def _limits_current(self): + """Determine the current limits based on the currently selected + range. + + """ + if self.mode == 'current': + ran = float(self.current_range) # Casting handling Quantity + res = CURRENT_RESOLUTION[ran] + if ran != 200e-3: + ran *= 1.2 + else: + ran = 220e-3 + return FloatLimitsValidator(-ran, ran, res, 'A') + else: + return FloatLimitsValidator(5e-3, 120e-3, 1e-3, 'A') + + @o + @customize('enabled', 'get') + def _get_enabled(feat, driver): + """Read the output current status byte and extract the output state + + """ + sc = driver.parent.read_status_code() + return bool(sc & sc.output_on) + + o._OD_PARSER = Parser('{_}DC{_}{:E+}') + + o._VOLT_LIM_PARSER = Parser('LV{}LA{_}') + + o._CURR_LIM_PARSER = Parser('LV{_}LA{}') + + @o + @customize('voltage', 'get') + def _get_voltage(feat, driver): + """Get the voltage in voltage mode and return the maximum voltage + in current mode. + + """ + if driver.mode != 'voltage': + return driver._VOLT_LIM_PARSER(driver._get_limiter_value()) + return driver._OD_PARSER(driver.default_get_feature(feat, 'OD')) + + @o + @customize('current', 'get') + def _get_current(feat, driver): + """Get the current in current mode and return the maximum current + in voltage mode. + + """ + if driver.mode != 'voltage': + if to_float(driver.voltage_range) in (10e-3, 100e-3): + return 0.12 + answer = driver._get_limiter_value() + return float(driver._CURR_LIM_PARSER(answer))*1e3 + return driver._OD_PARSER(driver.default_get_feature(feat, 'OD')) + + @o + @customize('current', 'set') + def _set_current(feat, driver, value): + """Set the target/limit current. + + In voltage mode this is only possible if the range is 1V or greater + + """ + if driver.mode != 'current': + if to_float(driver.voltage_range) in (10e-3, 100e-3): + raise ValueError('Cannot set the current limit for ranges ' + '10mV and 100mV') + else: + return driver.default_set_feature(feat, 'LA{:d}', + int(round(value*1e3))) + return driver.default_set_feature(feat, 'S{:+E}E', value) + + @o + def _get_limiter_value(self): + """Helper function reading the limiter value. + + Used to read the voltage/current target. + + """ + visa_rsc = self.parent.visa_resource + visa_rsc.write('OS') + visa_rsc.read() # Model and software version + visa_rsc.read() # Function, range, output data + visa_rsc.read() # Program parameters + return visa_rsc.read() # Limits + + @o + def _get_range(driver, kind): + """Read the range. + + """ + visa_rsc = driver.parent.visa_resource + if driver.mode == kind: + visa_rsc.write('OS') + visa_rsc.read() # Model and software version + msg = visa_rsc.read() # Function, range, output data + visa_rsc.read() # Program parameters + visa_rsc.read() # Limits + return msg + else: + return 'F{}R6S1E+0'.format(1 if kind == 'voltage' else 5) + + @o + @customize('voltage_range', 'get') + def _get_voltage_range(feat, driver): + return driver._get_range('voltage') + + @o + @customize('current_range', 'get') + def _get_current_range(feat, driver): + return driver._get_range('current') diff --git a/tests/drivers/__init__.py b/tests/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/drivers/itest/t_be2101.py b/tests/drivers/itest/t_be2101.py new file mode 100644 index 0000000..73c2bdf --- /dev/null +++ b/tests/drivers/itest/t_be2101.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""This file is meant to check the working of the driver for the BE2101. + +The rack is expected to have a BE2101 in one slot, whose output can be safely +switched on and off and whose output value can vary (and has a large impedance) + +""" +# Visa connection info +VISA_RESOURCE_NAME = 'TCPIP::192.168.0.10::5025::SOCKET' + +# Index of the slot in which the BE2101 can be found (starting from 1) +MODULE_INDEX = 1 + +from i3py.core.errors import I3pyFailedCall, I3pyFailedSet +from i3py.drivers.itest import BN100 + +with BN100(VISA_RESOURCE_NAME) as rack: + + # Test reading all features + print('Available modules', rack.be2101.available) + + module = rack.be2101[MODULE_INDEX] + print('Manufacturer', module.identity.manufacturer) + print('Model', module.identity.model) + print('Serial number', module.identity.serial) + print('Firmware', module.identity.firmware) + + print('Testing output') + output = module.outputs[0] + for f_name in output.__feats__: + print(' ', f_name, getattr(output, f_name)) + delattr(output, f_name) + + for sub_name in output.__subsystems__: + print(' Testing ', sub_name) + sub = getattr(output, sub_name) + for f_name in sub.__feats__: + print(' ', f_name, getattr(sub, f_name)) + delattr(sub, f_name) + + # Test action reading basic status + print('Output status', output.read_output_status()) + output.clear_output_status() + print('Measured output voltage', output.measure('voltage')) + + # Test settings and general behavior + print('Setting outputs') + output.enabled = False + output.trigger.mode = 'disabled' + output.voltage = 0 + output.wait_for_settling() + output.voltage_saturation.low_enabled = False + output.voltage_saturation.high_enabled = False + print('Known limits', output.declared_limits) + output.voltage_range = 1.2 + output.voltage_filter = ('Slow' if output.voltage_filter == 'Fast' else + 'Fast') + output.remote_sensing = True + del output.remote_sensing + print('Remote sensing is ', output.remote_sensing) + output.remote_sensing = False + output.enabled = True + + output.voltage = 1.0 + try: + output.read_voltage_status() + except I3pyFailedCall: + print('Cannot read voltage status in non-triggered mode') + output.wait_for_settling() + + # TODO test the other trigger modes and other sync methods + output.trigger.mode = 'slope' + output.trigger.slope = 0.01 + output.voltage = 0.5 + output.trigger.fire() + output.wait_for_settling() + print(f'Measured output is {output.measure("voltage")}, ' + f'target is {output.voltage}') + + output.voltage_saturation.low_enabled = True + output.voltage_saturation.high_enabled = True + output.voltage_saturation.low = -0.5 + output.voltage_saturation.high = 0.4 + print('New voltage', output.voltage) + try: + output.voltage = -0.6 + except I3pyFailedSet: + print('New restriction on the gate voltage') diff --git a/tests/drivers/itest/t_be2141.py b/tests/drivers/itest/t_be2141.py new file mode 100644 index 0000000..6ac8f4d --- /dev/null +++ b/tests/drivers/itest/t_be2141.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""This file is meant to check the working of the driver for the BE2141. + +The rack is expected to have a BE2101 in one slot, whose output can be safely +switched on and off and whose output value can vary (and has a large impedance) + +""" +# Visa connection info +VISA_RESOURCE_NAME = 'TCPIP::192.168.0.10::5025::SOCKET' + +# Index of the slot in which the BE2101 can be found (starting from 1) +MODULE_INDEX = 1 + +from i3py.core.errors import I3pyFailedCall, I3pyFailedSet +from i3py.drivers.itest import BN100 + +with BN100(VISA_RESOURCE_NAME) as rack: + + # Test reading all features + print('Available modules', rack.be2141.available) + + module = rack.be2141[MODULE_INDEX] + print('Manufacturer', module.identity.manufacturer) + print('Model', module.identity.model) + print('Serial number', module.identity.serial) + print('Firmware', module.identity.firmware) + + print('Testing output') + output = module.outputs[0] + for f_name in output.__feats__: + print(' ', f_name, getattr(output, f_name)) + delattr(output, f_name) + + for sub_name in output.__subsystems__: + print(' Testing ', sub_name) + sub = getattr(output, sub_name) + for f_name in sub.__feats__: + print(' ', f_name, getattr(sub, f_name)) + delattr(sub, f_name) + + # Test action reading basic status + print('Output status', output.read_output_status()) + output.clear_output_status() + print('Measured output voltage', output.measure('voltage')) + + # Test settings and general behavior + print('Setting outputs') + output.voltage_saturation.low_enabled = False + output.voltage_saturation.high_enabled = False + print('Known limits', output.declared_limits) + output.trigger.mode = 'disabled' + output.voltage_range = 1.2 + output.enabled = True + output.voltage = 1.0 + try: + output.read_voltage_status() + except I3pyFailedCall: + print('Cannot read voltage status in non-triggered mode') + output.wait_for_settling() + + # TODO test the other trigger modes and other sync methods + output.trigger.mode = 'slope' + output.trigger.slope = 0.01 + output.voltage = 0.5 + output.trigger.fire() + output.wait_for_settling() + print(f'Measured output is {output.measure("voltage")}, ' + f'target is {output.voltage}') + + output.voltage_saturation.low_enabled = True + output.voltage_saturation.high_enabled = True + output.voltage_saturation.low = -0.5 + output.voltage_saturation.high = 0.4 + print('New voltage', output.voltage) + try: + output.voltage = -0.6 + except I3pyFailedSet: + print('New restriction on the gate voltage') diff --git a/tests/drivers/keysight/__init__.py b/tests/drivers/keysight/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/drivers/keysight/t_E3631A.py b/tests/drivers/keysight/t_E3631A.py new file mode 100644 index 0000000..641b0fc --- /dev/null +++ b/tests/drivers/keysight/t_E3631A.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""This file is meant to check the working of the driver for the E3631A. + +The instrument is expected to be in a situation where output can be safely +switched on and off and whose output value can vary (and has a large impedance) + +""" +# Visa connection info +VISA_RESOURCE_NAME = 'GPIB::2::INSTR' + +from i3py.core.errors import I3pyFailedCall, I3pyFailedSet +from i3py.drivers.keysight import E3631A + +with E3631A(VISA_RESOURCE_NAME) as driver: + + # Test reading all features + print('Manufacturer', driver.identity.manufacturer) + print('Model', driver.identity.model) + print('Serial number', driver.identity.serial) + print('Firmware', driver.identity.firmware) + + print('Outputs enabled', driver.outputs_enabled) + print('Coupled triggers', driver.coupled_triggers) + print('Outputs tracking', driver.outputs_tracking) + + print('Testing output') + for output in driver.outputs: + for f_name in output.__feats__: + print(' ', f_name, getattr(output, f_name)) + + for sub_name in output.__subsystems__: + print(' Testing ', sub_name) + sub = getattr(output, sub_name) + for f_name in sub.__feats__: + print(' ', f_name, getattr(sub, f_name)) + + # Test action reading basic status + print('Output status', output.read_output_status()) + print('Measured output voltage', output.measure('voltage')) + print('Measured output current', output.measure('current')) + + # TODO add more comprehensive tests diff --git a/tests/drivers/yokogawa/__init__.py b/tests/drivers/yokogawa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/drivers/yokogawa/t_gs200.py b/tests/drivers/yokogawa/t_gs200.py new file mode 100644 index 0000000..616ba68 --- /dev/null +++ b/tests/drivers/yokogawa/t_gs200.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""This file is meant to check the working of the driver for the GS200. + +The instrument is expected to be in a situation where output can be safely +switched on and off and whose output value can vary (and has a large impedance) + +""" +# Visa connection info +VISA_RESOURCE_NAME = 'USB::0x0B21::0x0039::91N326145::INSTR' + +from i3py.core.errors import I3pyFailedCall, I3pyFailedSet +from i3py.drivers.yokogawa import GS200 + +with GS200(VISA_RESOURCE_NAME) as driver: + + # Test reading all features + print('Manufacturer', driver.identity.manufacturer) + print('Model', driver.identity.model) + print('Serial number', driver.identity.serial) + print('Firmware', driver.identity.firmware) + + print('Testing output') + output = driver.outputs[0] + for f_name in output.__feats__: + print(' ', f_name, getattr(output, f_name)) + + for sub_name in output.__subsystems__: + print(' Testing ', sub_name) + sub = getattr(output, sub_name) + for f_name in sub.__feats__: + print(' ', f_name, getattr(sub, f_name)) + + # Test action reading basic status + print('Output status', output.read_output_status()) + + # Test voltage mode + print('Voltage mode') + output.enabled = False + output.mode = 'voltage' + print(output.check_cache()) + print('Known limits', output.declared_limits) + output.voltage_range = 1 + output.enabled = True + output.voltage = 1.0 + output.current = 0.2 + assert output.read_output_status() == 'enabled:constant-voltage' + + # Test current mode + print('Current mode') + output.mode = 'current' + print(output.check_cache()) + # del output.enabled + assert not output.enabled + output.enabled = True + output.current_range = 0.2 + output.voltage = 10 + output.current = 0.1 + assert output.read_output_status() == 'enabled:constant-voltage' + + driver.visa_resource.write('x_x') + driver.read_error() diff --git a/tests/drivers/yokogawa/t_model7651.py b/tests/drivers/yokogawa/t_model7651.py new file mode 100644 index 0000000..03546c3 --- /dev/null +++ b/tests/drivers/yokogawa/t_model7651.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2018 by I3py Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""This file is meant to check the working of the driver for the 7651. + +The instrument is expected to be in a situation where output can be safely +switched on and off and whose output value can vary (and has a large impedance) + +""" +# Visa connection info +VISA_RESOURCE_NAME = 'GPIB0::5::INSTR' + +from time import sleep +from i3py.core.errors import I3pyFailedCall, I3pyFailedSet +from i3py.drivers.yokogawa import Model7651 + +with Model7651(VISA_RESOURCE_NAME) as driver: + + # Test reading all features + assert driver.is_connected() + print('Manufacturer', driver.identity.manufacturer) + print('Model', driver.identity.model) + print('Serial number', driver.identity.serial) + print('Firmware', driver.identity.firmware) + + print('Testing output') + output = driver.outputs[0] + for f_name in output.__feats__: + print(' ', f_name, getattr(output, f_name)) + + for sub_name in output.__subsystems__: + print(' Testing ', sub_name) + sub = getattr(output, sub_name) + for f_name in sub.__feats__: + print(' ', f_name, getattr(sub, f_name)) + + # Test action reading basic status + print('Output status', output.read_output_status()) + + # Test voltage mode + print('Voltage mode') + output.enabled = False + output.mode = 'voltage' + print(output.check_cache()) + print('Known limits', output.declared_limits) + output.voltage_range = 1 + output.enabled = True + output.voltage = 1.0 + output.current = 0.1 + sleep(1) + assert output.read_output_status() == 'enabled:constant-voltage' + + # Test current mode + print('Current mode') + output.mode = 'current' + print(output.check_cache()) + assert not output.enabled + output.current = 0 + output.enabled = True + output.current_range = 0.1 + output.voltage = 10 + output.current = 0.05 + sleep(1) + assert output.read_output_status() == 'enabled:constant-voltage'