Source code for instruments.gentec_eo.blu

"""Support for Gentec-EO Blu devices."""

# IMPORTS #####################################################################

from enum import Enum
from time import sleep

from instruments.abstract_instruments import Instrument
from instruments.units import ureg as u
from instruments.util_fns import assume_units

# CLASSES #####################################################################


[docs] class Blu(Instrument): """Communicate with Gentec-eo BLU power / energy meter interfaces. These instruments communicate via USB or via bluetooth. The bluetooth sender / receiver that is provided with the instrument is simply emulating a COM port. This routine cannot pair the device with bluetooth, but once it is paired, it can communicate with the port. Alternatively, you can plug the device into the computer using a USB cable. .. warning:: If commands are issued too fast, the device will not answer. Experimentally, a 1 ms delay should be enough to get the device into answering mode. Keep this in mind when issuing many commands at once. No wait time included in this class. .. note:: The instrument also has a possiblity to read a continuous data stream. This is currently not implemented here since it would have to be threaded out. Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.current_value 3.004 W """ def __init__(self, filelike): super().__init__(filelike) # use a terminator for blu, even though none required self.terminator = "\r\n" # define the power mode self._power_mode = None # acknowledgement message self._ack_message = "ACK" def _ack_expected(self, msg=""): """Set up acknowledgement checking.""" return self._ack_message # ENUMS #
[docs] class Scale(Enum): """Available scales for Blu devices. The following list maps available scales of the Blu devices to the respective indexes. All scales are either in watts or joules, depending if power or energy mode is activated. Furthermore, the maximum value that can be measured determines the name of the scale to be set. Prefixes are given in the `enum` class while the unit is omitted since it depends on the mode the head is in. """ max1pico = "00" max3pico = "01" max10pico = "02" max30pico = "03" max100pico = "04" max300pico = "05" max1nano = "06" max3nano = "07" max10nano = "08" max30nano = "09" max100nano = "10" max300nano = "11" max1micro = "12" max3micro = "13" max10micro = "14" max30micro = "15" max100micro = "16" max300micro = "17" max1milli = "18" max3milli = "19" max10milli = "20" max30milli = "21" max100milli = "22" max300milli = "23" max1 = "24" max3 = "25" max10 = "26" max30 = "27" max100 = "28" max300 = "29" max1kilo = "30" max3kilo = "31" max10kilo = "32" max30kilo = "33" max100kilo = "34" max300kilo = "35" max1Mega = "36" max3Mega = "37" max10Mega = "38" max30Mega = "39" max100Mega = "40" max300Mega = "41"
# PROPERTIES # @property def anticipation(self): """Get / Set anticipation. This command is used to enable or disable the anticipation processing when the device is reading from a wattmeter. The anticipation is a software-based acceleration algorithm that provides faster readings using the detector’s calibration. :return: Is anticipation enabled or not. :rtype: bool Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.anticipation True >>> inst.anticipation = False """ return self._value_query("*GAN", tp=int) == 1 @anticipation.setter def anticipation(self, newval): sendval = 1 if newval else 0 self.sendcmd(f"*ANT{sendval}") @property def auto_scale(self): """Get / Set auto scale on the device. :return: Status of auto scale enabled feature. :rtype: bool :raises ValueError: The command was not acknowledged by the device. Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.auto_scale True >>> inst.auto_scale = False """ resp = self._value_query("*GAS", tp=int) return resp == 1 @auto_scale.setter def auto_scale(self, newval): sendval = 1 if newval else 0 self.sendcmd(f"*SAS{sendval}") @property def available_scales(self): """Get available scales from connected device. :return: Scales currently available on device. :rtype: :class:`Blu.Scale` Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.available_scales [<Scale.max100milli: '22'>, <Scale.max300milli: '23'>, <Scale.max1: '24'>, <Scale.max3: '25'>, <Scale.max10: '26'>, <Scale.max30: '27'>, <Scale.max100: '28'>] """ # set no terminator and a 1 second timeout _terminator = self.terminator self.terminator = "" _timeout = self.timeout self.timeout = u.Quantity(1, u.s) try: # get the response resp = self._no_ack_query("*DVS").split("\r\n") finally: # set back terminator and 3 second timeout self.terminator = _terminator self.timeout = _timeout # prepare return retlist = [] # init return list of enums for line in resp: if len(line) > 0: # account for empty lines index = line[line.find("[") + 1 : line.find("]")] retlist.append(self.Scale(index)) return retlist @property def battery_state(self): """Get the charge state of the battery. :return: Charge state of battery :rtype: u.percent Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.battery_state array(100.) * % """ resp = self._no_ack_query("*QSO").rstrip() resp = float(resp[resp.find("=") + 1 : len(resp)]) return u.Quantity(resp, u.percent) @property def current_value(self): """Get the currently measured value (unitful). :return: Currently measured value :rtype: u.Quantity Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.current_value 3.004 W """ if self._power_mode is None: _ = self.measure_mode # determine the power mode sleep(0.01) unit = u.W if self._power_mode else u.J return u.Quantity(float(self._no_ack_query("*CVU")), unit) @property def head_type(self): """Get the head type information. :return: Type of instrument head. :rtype: str Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.head_type 'NIG : 104552, Wattmeter, V1.95' """ return self._no_ack_query("*GFW") @property def measure_mode(self): """Get the current measurement mode. Potential return values are 'power', which inidcates power mode in W and 'sse', indicating single shot energy mode in J. :return: 'power' if in power mode, 'sse' if in single shot energy mode. :rtype: str Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.measure_mode 'power' """ resp = self._value_query("*GMD", tp=int) if resp == 0: self._power_mode = True return "power" else: self._power_mode = False return "sse" @property def new_value_ready(self): """Get status if a new value is ready. This command is used to check whether a new value is available from the device. Though optional, its use is recommended when used with single pulse operation. :return: Is a new value ready? :rtype: bool Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.new_value_ready False """ resp = self._no_ack_query("*NVU") return False if resp.find("Not") > -1 else True @property def scale(self): """Get / Set measurement scale. The measurement scales are chosen from the the `Scale` enum class. Scales are either in watts or joules, depending on what state the power meter is currently in. .. note:: Setting a scale manually will automatically turn of auto scale. :return: Scale that is currently set. :rtype: :class:`Blu.Scale` :raises ValueError: The command was not acknowledged by the device. A scale that is not available might have been selected. Use `available_scales` to display scales that are possible on your device. Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.scale = inst.Scale.max3 >>> inst.scale <Scale.max3: '25'> """ return self.Scale(self._value_query("*GCR")) @scale.setter def scale(self, newval): self.sendcmd(f"*SCS{newval.value}") @property def single_shot_energy_mode(self): """Get / Set single shot energy mode. :return: Is single shot energy mode turned on? :rtype: bool Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.single_shot_energy_mode False >>> inst.single_shot_energy_mode = True """ val = self._value_query("*GSE", tp=int) == 1 self._power_mode = False if val else True return val @single_shot_energy_mode.setter def single_shot_energy_mode(self, newval): sendval = 1 if newval else 0 # set send value self._power_mode = False if newval else True # set power mode self.sendcmd(f"*SSE{sendval}") @property def trigger_level(self): """Get / Set trigger level when in energy mode. The trigger level must be between 0.001 and 0.998. :return: Trigger level (absolute) with respect to the currently set scale :rtype: float :raise ValueError: Trigger level out of range. Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.trigger_level = 0.153 >>> inst.trigger_level 0.153 """ level = self._no_ack_query("*GTL") # get the percent retval = float(level[level.find(":") + 1 : level.find("%")]) / 100 return retval @trigger_level.setter def trigger_level(self, newval): if newval < 0.001 or newval > 0.99: raise ValueError( "Trigger level {} is out of range. It must be " "between 0.001 and 0.998.".format(newval) ) newval = newval * 100.0 if newval >= 10: newval = str(round(newval, 1)).zfill(4) else: newval = str(round(newval, 2)).zfill(4) self.sendcmd(f"*STL{newval}") @property def usb_state(self): """Get status if USB cable is connected. :return: Is a USB cable connected? :rtype: bool Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.usb_state True """ return self._value_query("*USB", tp=int) == 1 @property def user_multiplier(self): """Get / Set user multiplier. :return: User multiplier :rtype: u.Quantity Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.user_multiplier = 10 >>> inst.user_multiplier 10.0 """ return self._value_query("*GUM", tp=float) @user_multiplier.setter def user_multiplier(self, newval): sendval = _format_eight(newval) # sendval: 8 characters long self.sendcmd(f"*MUL{sendval}") @property def user_offset(self): """Get / Set user offset. The user offset can be set unitful in watts or joules and set to the device. :return: User offset :rtype: u.Quantity :raises ValueError: Unit not supported or value for offset is out of range. Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.user_offset = 10 >>> inst.user_offset array(10.) * W """ if self._power_mode is None: _ = self.measure_mode # determine the power mode sleep(0.01) if self._power_mode: return assume_units(self._value_query("*GUO", tp=float), u.W) else: return assume_units(self._value_query("*GUO", tp=float), u.J) @user_offset.setter def user_offset(self, newval): # if unitful, try to rescale and grab magnitude if isinstance(newval, u.Quantity): if newval.is_compatible_with(u.W): newval = newval.to(u.W).magnitude elif newval.is_compatible_with(u.J): newval = newval.to(u.J).magnitude else: raise ValueError( "Value must be given in watts, " "joules, or unitless." ) sendval = _format_eight(newval) # sendval: 8 characters long self.sendcmd(f"*OFF{sendval}") @property def version(self): """Get device information. :return: Version and device type :rtype: str Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.version 'Blu firmware Version 1.95' """ return self._no_ack_query("*VER") @property def wavelength(self): """Get / Set the wavelength. The wavelength can be set unitful. Specifying zero as a wavelength or providing an out-of-bound value as a parameter restores the default settings, typically 1064nm. If no units are provided, nm are assumed. :return: Wavelength in nm :rtype: u.Quantity Example: >>> import instruments as ik >>> import instruments.units as u >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.wavelength = u.Quantity(527, u.nm) >>> inst.wavelength array(527) * nm """ return u.Quantity(self._value_query("*GWL", tp=int), u.nm) @wavelength.setter def wavelength(self, newval): val = round(assume_units(newval, u.nm).to(u.nm).magnitude) if val >= 1000000 or val < 0: # can only send 5 digits val = 0 # out of bound anyway val = str(int(val)).zfill(5) self.sendcmd(f"*PWC{val}") @property def zero_offset(self): """Get / Set zero offset. Gets the status if zero offset is enabled. When set to `True`, the device will read the current level immediately for around three seconds and then set the baseline to the averaged value. If activated and set to `True` again, a new value for the baseline will be established. :return: Is zero offset enabled? :rtype: bool Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.zero_offset True >>> inst.zero_offset = False """ return self._value_query("*GZO", tp=int) == 1 @zero_offset.setter def zero_offset(self, newval): if newval: self.sendcmd("*SOU") else: self.sendcmd("*COU") # METHODS #
[docs] def confirm_connection(self): """Confirm a connection to the device. Turns of bluetooth searching by confirming a connection. Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.confirm_connection() """ self.sendcmd("*RDY")
[docs] def disconnect(self): """Disconnect the device. Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.disconnect() """ self.sendcmd("*BTD")
[docs] def scale_down(self): """Set scale to next lower level. Sets the power meter to the next lower scale. If already at the lowest possible scale, no change will be made. Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.scale_down() """ self.sendcmd("*SSD")
[docs] def scale_up(self): """Set scale to next higher level. Sets the power meter to the next higher scale. If already at the highest possible scale, no change will be made. Example: >>> import instruments as ik >>> inst = ik.gentec_eo.Blu.open_serial('/dev/ttyACM0') >>> inst.scale_up() """ self.sendcmd("*SSU")
# PRIVATE METHODS # def _no_ack_query(self, cmd, size=-1): """Query a value and don't expect an ACK message.""" self._ack_message = None try: value = self.query(cmd, size=size) finally: self._ack_message = "ACK" return value def _value_query(self, cmd, tp=str): """Query one specific value and return it. :param cmd: Command to send to self._no_ack_query. :type cmd: str :param tp: Type of the value to be returned, default: str :type tp: type :return: Single value of query. :rtype: tp (selected type) :raises ValueError: Conversion of response into given type was unsuccessful. """ resp = self._no_ack_query(cmd).rstrip() # strip \r\n resp = resp.split(":")[1] # strip header off resp = resp.replace(" ", "") # strip white space if isinstance(resp, tp): return resp else: return tp(resp)
def _format_eight(value): """Formats a value to eight characters total. :param value: value to be formatted, > -1e100 and < 1e100 :type value: int,float :return: Value formatted to 8 characters :rtype: str """ if abs(value) < 1e-99: return "0".zfill(8) for p in range(8, 1, -1): val = f"{value:.{p}g}".zfill(8) if len(val) == 8: return val