Source code for PyExpLabSys.drivers.fug

# pylint: disable=invalid-name
"""Driver for \"fug NTN 140 - 6,5 17965-01-01\" power supply
    Communication via the Probus V serial interface.

    Written using the two documents:

    1) Interface system Probus V - Documentation for RS232/RS422
       Revision of document 2.4
    2) Probus V - Command Reference
       Base Module
       ADDAT30 Firmware PIC0162 V4.0
       Version of Document V2.22

    Should be freely available from
    http://www.fug-elektronik.de/en/support/download.html
    (Available  August 25 2017)
"""

from __future__ import print_function
import sys
import time
import serial

# Error codes and their interpretations as copied from manuals
ERRORCODES = {
    'E0': 'no error',
    'E1': 'no data available',
    'E2': 'unknown register type',
    'E4': 'invalid argument',
    'E5': 'argument out of range',
    'E6': 'register is read only',
    'E7': 'Receive Overflow',
    'E8': 'EEPROM is write protected',
    'E9': 'adress error',
    'E10': 'unknown SCPI command',
    'E11': 'not allowed Trigger-on-Talk',
    'E12': 'invalid argument in ~Tn command',
    'E13': 'invalid N-value',
    'E14': 'register is write only',
    'E15': 'string too long',
    'E16': 'wrong checksum',
}


[docs]class FUGNTN140Driver(object): """Driver for fug NTN 140 power supply **Methods** * **Private** * __init__ * _check_answer * _flush_answer * _get_answer * _write_register * _read_register * **Public** * reset() * stop() * is_on() * output(state=True/False) * get_state() * identification_string() * --- * set_voltage(value) * get_voltage() * monitor_voltage() * ramp_voltage(value, program=0) * ramp_voltage_running() * --- * set_current(value) * get_current() * monitor_current() * ramp_current(value, program=0) * ramp_current_running() """
[docs] def __init__( # pylint: disable=too-many-arguments self, port='/dev/ttyUSB0', baudrate=9600, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, device_reset=True, V_max=6.5, I_max=10, ): """Initialize object variables For settings port, baudrate, parity, stopbits, bytesize, see the pyserial documentation. Args: device_reset (bool): If true, resets all device parameters to default values """ # Open a simple serial connection to port timeout_counter = 0 while timeout_counter < 10: timeout_counter += 1 self.ser = serial.Serial( port=port, baudrate=baudrate, parity=parity, stopbits=stopbits, bytesize=bytesize, timeout=1, ) time.sleep(1) try: if self.ser.isOpen(): break except AttributeError: print('Attempt #{}\n'.format(timeout_counter)) else: print('Connection timeout') sys.exit() # End character self.end = '\x00' if not self.ser.isOpen(): raise IOError('Connection to device is not open') else: print('Connected to device: {}'.format(self.identification_string())) if device_reset: self.reset() self.V_max = V_max self.I_max = I_max
# Answer string handling def _check_answer(self): """Verify correct answer string (neglect previous answers) """ string = self.ser.readline() if string.decode('ascii').strip() == 'E0': return True else: self.stop() raise IOError(string.strip() + ' : ' + ERRORCODES[string.strip()]) def _flush_answer(self, print_answer=False): """Flush answer bytes in waiting (should probably not be used) """ if print_answer: while self.ser.inWaiting() > 0: print(repr(self.ser.read(1))) else: self.ser.read(self.ser.inWaiting()) def _get_answer(self): """Get waiting answer string """ string = self.ser.readline() string = string.decode('ascii') return string # Register handlers def _write_register(self, register, value): """Alters the value of a register """ command = '>' + register + ' ' + str(value) + self.end self.ser.write(command.encode()) self._check_answer() def _read_register(self, register, value_type=float): """Queries a register and returns its value """ command = '>' + register + '?' + self.end self.ser.write(command.encode()) string = self._get_answer() if value_type == float: # Interpret answer as 'float' return float(string.split(register + ':')[-1]) elif value_type == int: # Interpret answer as 'int' return int(string.split(register + ':')[-1]) elif value_type == str: # Return entire answer string return string elif value_type == bool: # Interpret answer as 'boolean' (through 'int') return bool(int(string.split(register + ':')[-1])) else: raise TypeError('Wrong input value_type') # Termination functions
[docs] def reset(self): """Resets device """ command = '=' + self.end self.ser.write(command.encode()) self._check_answer()
[docs] def stop(self, reset=True): """Closes device properly before exit """ if reset: self.reset() self.ser.close()
# Output interpreters
[docs] def is_on(self): """Checks if output is ON (>DON) Returns True if ON """ return self._read_register('DON', bool)
[docs] def output(self, state=False): """Set output ON (>BON) """ if state is True: register = 'F1' elif state is False: register = 'F0' command = register + self.end self.ser.write(command.encode()) self._check_answer()
[docs] def get_state(self): """Checks whether unit is in CV or CC mode (>DVR/>DIR) """ if self._read_register('DVR', bool): return 'CV' elif self._read_register('DIR', bool): return 'CC' else: return 'No regulation mode detected'
[docs] def identification_string(self): """Output serial number of device""" return self._read_register('CFN', str)
# Voltage interpreters
[docs] def set_voltage(self, value): """Sets the voltage (>S0) """ # Minimum voltage if value < 0.0: value = 0.0 # Maximum voltage if value > self.V_max: value = self.V_max self._write_register('S0', value)
[docs] def get_voltage(self): """Reads the set point voltage (>S0A) """ return self._read_register('S0A', float)
[docs] def monitor_voltage(self): """Reads the actual (monitor) voltage (>M0) """ V = self._read_register('M0', float) # Correct analog zero if V < 1e-3: V = 0.0 return V
[docs] def ramp_voltage(self, value, program=0): """Activates ramp function for voltage value : ramp value in volts/second (>S0R) +---------+--------------------------------------------------------------------+ | program | setvalue behaviour | +=========+====================================================================+ | 0 | (default) no ramp function. Setpoint is implemented immediately | +---------+--------------------------------------------------------------------+ | 1 | >S0A follows the value in >S0 with the adjusted ramp rate | | | upwards and downwards | +---------+--------------------------------------------------------------------+ | 2 | >S0A follows the value in >S0 with the adjusted ramp rate only | | | upwards. When programming downwards, >S0A follows >S0 immediately. | +---------+--------------------------------------------------------------------+ | 3 | >S0A follows the value in >S0 with a special ramp function only | | | upwards. When programming downwards, >S0A follows >S0 immediately. | | | Ramp between 0..1 with 11.11E-3 per second. Above 1 : with >S0R | +---------+--------------------------------------------------------------------+ | 4 | Same as 2, but >S0 as well as >S0A are set to zero if >DON is 0 | +---------+--------------------------------------------------------------------+ """ if program != -1: self._write_register('S0B', program) self._write_register('S0R', value)
[docs] def ramp_voltage_running(self): """Return status of voltage ramp. True: still ramping False: ramp complete """ return self._read_register('S0S', bool)
# Current interpreters
[docs] def set_current(self, value): """Sets the current (>S1) """ # Minimum current if value < 0: value = 0 # Maximum current if value > self.I_max: value = self.I_max # Set current self._write_register('S1', value)
[docs] def get_current(self): """Reads the set point current (>S1A) """ return self._read_register('S1A', float)
[docs] def monitor_current(self): """Reads the actual (monitor) current (>M1) """ I = self._read_register('M1', float) # Correct analog zero if I < 1e-3: I = 0.0 return I
[docs] def ramp_current(self, value, program=0): """Activates ramp function for current. See ramp_voltage() for description.""" if program != -1: self._write_register('S1B', program) self._write_register('S1R', value)
[docs] def ramp_current_running(self): """Return status of current ramp. True: still ramping False: ramp complete """ return self._read_register('S1S', bool)
[docs] def read_H1(self, ret=False): """Read H1 FIXME not yet done""" t0 = time.time() command = '>H1?' + self.end self.ser.write(command.encode()) bytes_ = self.ser.read(36) bytes_ = bytes_[3:-1].decode() # Byte 01 voltage = ( int.from_bytes(bytes.fromhex(bytes_[0:4]), byteorder='little') / 65535 * 12.5 ) # Byte 23 current = ( int.from_bytes(bytes.fromhex(bytes_[4:8]), byteorder='little') / 65535 * 8 ) if ret is True: return voltage, current # Byte 4 print('Byte 4: ', end='') byte = bytes.fromhex(bytes_[8:10]) bits = bin(int.from_bytes(byte, byteorder='big'))[2:].zfill(2) print(bits) print( 'Power supply is {}digitally controlled'.format( 'not ' if bits[-1] == '0' else '' ) ) print( 'Power supply is {}analogue controlled'.format( 'not ' if bits[-2] == '0' else '' ) ) print( 'Power supply is {}in calibration mode'.format( 'not ' if bits[-3] == '0' else '' ) ) print('X-STAT: {}'.format(bits[-4])) print('3-REG: {}'.format(bits[-5])) print('Output is {}'.format('ON' if bits[-6] == '1' else 'OFF')) if bits[:2] == '01': mode = 'is in CV mode' elif bits[:2] == '10': mode = 'is in CC mode' elif bits[:2] == '00': mode = 'is not regulated' elif bits[:2] == '11': mode = 'appears to be in both CV and CC mode' print('Power supply {}'.format(mode)) # Byte 5 print('Byte 5: ', end='') byte = bytes.fromhex(bytes_[10:12]) bits = bin(int.from_bytes(byte, byteorder='big'))[2:].zfill(8) print(bits) print( 'Polarity of voltage: {}'.format( 'positive' if bits[-1] == '0' else 'negative' ) ) print( 'Polarity of current: {}'.format( 'positive' if bits[-2] == '0' else 'negative' ) ) print() # UNUSED 6789 # Byte 10 11 12 13 print('Serial number: ', end='') byte = bytes.fromhex(bytes_[20:28]) print(int.from_bytes(byte, byteorder='big')) # Byte 14 byte = bytes.fromhex(bytes_[28:30]) code = int.from_bytes(byte, byteorder='big') print('Last error code: {}\n'.format(code)) print('{:6.4} V - {:6.4} A '.format(voltage, current)) # while self.ser.inWaiting() > 0: # bytes_.append(self.ser.read(1)) # bytes_.append(self.ser.read(32) print('Command time: {} s'.format(time.time() - t0)) return bytes_
[docs] def print_states(self, t0=0): """Print the current state of the power supply""" t = time.time() V = self.monitor_voltage() I = self.monitor_current() state = self.get_state() deltat = time.time() - t print( '{:8.2f} s ; {:>6.2f} W ; {:>8.4} V ; {:>8.4} A ; {:4.3f} s ; {}'.format( t - t0, V * I, V, I, deltat, state ) )
[docs]def test(): """Module test function""" try: power = FUGNTN140Driver(port='/dev/ttyUSB2', device_reset=True) return power power.output(True) power.ramp_current(value=0.2, program=1) power.ramp_voltage(value=0.2, program=1) t0 = time.time() power.print_states(t0) power.set_voltage(3) return power.set_current(2.5) while power.ramp_voltage_running(): power.print_states(t0) print('Ramp criteria fulfilled') power.ramp_voltage(0) power.ramp_current(0) power.set_voltage(6.5) power.set_current(3.5) for _ in range(10): time.sleep(0.8) power.print_states(t0) power.set_current(4.5) for _ in range(10): time.sleep(0.8) power.print_states(t0) power.stop() except KeyboardInterrupt: power.stop()
if __name__ == '__main__': ps = test()