hvps_lib

   1# py-hvps-interface of the Project Peta-pico-Voltron
   2# petapicovoltron.com
   3# Copyright 2017-2024 Samuel Rosset
   4# Distributed under the terms of the GNU General Public License GNU GPLv3
   5
   6
   7# This file is part of py-hvps-interface.
   8
   9#    py-hvps-interface is free software: you can redistribute it and/or modify
  10#    it under the terms of the GNU General Public License as published by
  11#    the Free Software Foundation, either version 3 of the License, or
  12#    (at your option) any later version.
  13#
  14#    py-hvps-interface is distributed in the hope that it will be useful,
  15#    but WITHOUT ANY WARRANTY; without even the implied warranty of
  16#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17#    GNU General Public License for more details.
  18#
  19#    You should have received a copy of the GNU General Public License
  20#    along with py-hvps-interface.  If not, see  <http://www.gnu.org/licenses/>
  21
  22from time import sleep
  23
  24try:
  25    import serial
  26    import serial.tools.list_ports
  27    import struct
  28    import numpy as np
  29    from datetime import datetime
  30    import configparser
  31    import ctypes
  32    import json
  33    import os
  34    from sys import exit
  35except ImportError:
  36    serial = None
  37    struct = None
  38    np = None
  39    datetime = None
  40    configparser = None
  41    ctypes = None
  42    json = None
  43    os = None
  44    print("Some of the required packages are missing. Execute \'pip install -r requirements.txt\' "
  45          "from the interface folder")
  46    sleep(4)
  47    exit()
  48
  49DEBUG = False
  50LIB_VER = "1.2"  # Version of this library
  51LIB_NAME = 'Release the kraken'
  52HVPSX_PID = 0x0632
  53
  54# compatibility dictionary:
  55compatibility_dict = {
  56    'Lib-1.0': ['Fw-1.0'],
  57    'Lib-1.1': ['Fw-1.1'],
  58    'Lib-1.2': ['Fw-1.2'],
  59    'Fw-1.0': ['Hw-1.1', 'Hw-1.2', 'Hw-1.3'],
  60    'Fw-1.1': ['Hw-1.1', 'Hw-1.2', 'Hw-1.3', 'Hw-1.4'],
  61    'Fw-1.2': ['Hw-1.1', 'Hw-1.2', 'Hw-1.3', 'Hw-1.4']
  62}
  63# Switching mode constants: off=0, on-DC=1, on-Switching=2, on-waveform=3
  64SWMODE_OFF = 0
  65SWMODE_DC = 1  # only SHVPS
  66SWMODE_SW = 2
  67SWMODE_WFRM = 3
  68SWMODE_HIGH = 4  # only hvps-x
  69SWMODE_LOW = 5  # only hvps-x
  70
  71# Switching source constants: Timer=0,External=1, Button=2,
  72SWSRC_TMR = 0
  73SWSRC_EXT = 1
  74SWSRC_BTTN = 2
  75
  76# Voltage control mode constants: Regulated=0, External=1, Open loop=2
  77VMODE_R = 0
  78VMODE_EXT = 1
  79VMODE_O = 2
  80
  81# Stroboscobic illumination mode: Off=0 Fixed position=1, sweeping mode=2
  82STMODE_OFF = 0
  83STMODE_FIXED = 1
  84STMODE_SWEEP = 2
  85
  86# look-up tables
  87LOOKUP_VOUT = 0
  88LOOKUP_ADC_P = 1
  89LOOKUP_ADC_O = 2
  90
  91# Error bits
  92ERR_FIRM = 0b1  # Incompatibility between library-firmware-hardware
  93ERR_TYPE = 0b10  # The connected device is not a SHVPS or a hvpsx
  94ERR_JACK = 0b100  # Unused
  95ERR_COM = 0b1000  # Communication error: Cannot communicate with connected device
  96ERR_CONF = 0b10000  # Configuration error: the HVPS running the hvps software, but appears to be unconfigured
  97ERR_PORT = 0b100000  # port cannot be open. Used by another process
  98ERR_CMD = 0b1000000  # serial command not understood
  99
 100# HVPS-X related constants
 101# Serial commands and codes
 102SERIAL_ERROR = 0x21727245  # int value of 'Err!'
 103SERIAL_OK = 0x4B4F4B4F  # int value of 'OKOK'
 104SERIAL_SVMAX = 0x01
 105SERIAL_QVMAX = 0x02
 106SERIAL_SVSET = 0x03
 107SERIAL_QVSET = 0x04
 108SERIAL_SAVE = 0x05
 109SERIAL_SCONF = 0x06
 110SERIAL_QCONF = 0x07
 111SERIAL_QVER = 0x08
 112SERIAL_SNAME = 0x09
 113SERIAL_QNAME = 0x0a
 114SERIAL_SF = 0x0b
 115SERIAL_QF = 0x0c
 116SERIAL_SSWMODE = 0x0d
 117SERIAL_QSWMODE = 0x0e
 118SERIAL_SDUTY = 0x0f
 119SERIAL_QDUTY = 0x10
 120SERIAL_SPID = 0x11
 121SERIAL_QPID = 0x12
 122SERIAL_SCAL = 0x13
 123SERIAL_QCAL = 0x14
 124SERIAL_SBTTNCFG = 0x15
 125SERIAL_QBTTNCFG = 0x16
 126SERIAL_SSWSRC = 0x17
 127SERIAL_QSWSRC = 0x18
 128SERIAL_SVMODE = 0x19
 129SERIAL_QVMODE = 0x1a
 130SERIAL_QVNOW = 0x1b
 131SERIAL_QKILL = 0x1c
 132SERIAL_QVNOWRAW = 0x1d
 133SERIAL_SLKUP = 0x1e
 134SERIAL_QLKUP = 0x1f
 135SERIAL_SST = 0x20
 136SERIAL_SHW = 0x21
 137SERIAL_SCALMETH = 0x22
 138SERIAL_QCALMETH = 0x23
 139SERIAL_QMEM = 0x24
 140SERIAL_LST = 0x25
 141SERIAL_CPYST = 0x26
 142SERIAL_XFERST = 0x27
 143SERIAL_QTRACE = 0x28
 144SERIAL_STRRATE = 0x29
 145SERIAL_QTRRATE = 0x2A
 146SERIAL_SWFMPTS = 0x2B
 147SERIAL_SSATLIM = 0x2C
 148SERIAL_QWFMPTS =  0x2D
 149SERIAL_SCURR = 0x2E
 150SERIAL_QCURR = 0x2F
 151SERIAL_QLOAD = 0x30
 152SERIAL_SVSETRAW = 0x31
 153SERIAL_QVSETRAW = 0x32
 154SERIAL_SCURRSEL = 0x33
 155SERIAL_QCURRSEL = 0x34
 156
 157SERIAL_DADC = 0xA0
 158SERIAL_WFMPWM = 0xA1
 159
 160# board configurations
 161CONF_UNCONF = 0  # hvpsx unconfigured device
 162CONF_BIPOLAR = 1  # hvpsx Bipolar board version 1
 163CONF_UNIPOLAR = 2  # hvpsx Unipolar board version 1
 164
 165# setting types
 166SETTINGS_CURRENT = 0
 167SETTINGS_BACKUP = 1
 168SETTINGS_FACTORY = 2
 169
 170# calibration methods
 171CALMETH_POLYNOMIAL = 0
 172CALMETH_LOOKUP = 1
 173
 174# DCDC converter
 175DCDC_POS = 1  # positive DC/DC converter
 176DCDC_NEG = 2  # negative DC/DC converter
 177DCDC_BOTH = 3  # Both DC/DC converters
 178
 179VNOW_POS = 0x00  # Voltage reading from positive DC/DC
 180VNOW_NEG = 0x01  # Voltage reading from negative DC/DC
 181VNOW_OUT = 0x02  # Voltage reading at the hvps-x output
 182
 183# gains of the different PIDs
 184PID_KPP = 0x0  # P for positive voltage DCDC converter
 185PID_KIP = 0x1
 186PID_KDP = 0x2
 187PID_KPN = 0x3  # N for negative voltage DCDC converter
 188PID_KIN = 0x4
 189PID_KDN = 0x5
 190PID_KPO = 0x6  # O for output waveform PID
 191PID_KIO = 0x7
 192PID_KDO = 0x8
 193
 194# Calibration constants
 195CAL_C0P = 0x00  # P for positive voltage measurement
 196CAL_C1P = 0x01
 197CAL_C2P = 0x02
 198CAL_C0N = 0x03  # N for negative voltage measurement
 199CAL_C1N = 0x04
 200CAL_C2N = 0x05
 201CAL_C0O = 0x06  # O for output waveform measurement
 202CAL_C1O = 0x07
 203CAL_C2O = 0x08
 204
 205#Current selection jumper (J2)
 206J2_5V = 1   # jumper J2 in position 5V (normal current)
 207J2_3V3 = 0  # jumper J2 in position 3V3 (normal current)
 208
 209memorymap = {
 210    'Fw-1.0': [(1, 'hw_major', 'B'), (1, 'hw_minor', 'B'), (1, 'fw_major', 'B'), (1, 'fw_minor', 'B'),
 211               (1, 'setting_type', 'B'), (1, 'conf', 'B'), (1, 'cal_meth_p', 'B'), (1, 'cal_meth_o', 'B'),
 212               (1, 'cal_meth_n', 'B'), (1, 'bttncfg', 'B'), (1, 'SwMode', 'B'), (1, 'SwSrc', 'B'), (1, 'VMode', 'B'),
 213               (1, 'Padding', 'B'), (1, 'pid', 'H'), (1, 'Vmax', 'H'), (1, 'Vsetp_raw', 'H'), (1, 'Vsetn_raw', 'H'),
 214               (1, 'Duty', 'H'), (12, 'Name', 's'), (21, 'lookup_ADC_p', 'h'), (21, 'lookup_ADC_n', 'h'),
 215               (21, 'lookup_ADC_o', 'h'), (21, 'lookup_Vout', 'h'), (3, 'cp', 'f'), (3, 'cn', 'f'), (3, 'co', 'f'),
 216               (1, 'Freq', 'f'), (1, 'kpp', 'f'), (1, 'kip', 'f'), (1, 'kdp', 'f')],
 217    'Fw-1.1': [(1, 'hw_major', 'B'), (1, 'hw_minor', 'B'), (1, 'fw_major', 'B'), (1, 'fw_minor', 'B'),
 218               (1, 'setting_type', 'B'), (1, 'conf', 'B'), (1, 'cal_meth_p', 'B'), (1, 'cal_meth_o', 'B'),
 219               (1, 'cal_meth_n', 'B'), (1, 'bttncfg', 'B'), (1, 'SwMode', 'B'), (1, 'SwSrc', 'B'), (1, 'VMode', 'B'),
 220               (1, 'Padding', 'B'), (1, 'pid', 'H'), (1, 'Vmax', 'H'), (1, 'Vsetp_raw', 'H'), (1, 'Vsetn_raw', 'H'),
 221               (1, 'Duty', 'H'), (12, 'Name', 's'), (21, 'lookup_ADC_p', 'h'), (21, 'lookup_ADC_n', 'h'),
 222               (21, 'lookup_ADC_o', 'h'), (21, 'lookup_Vout', 'h'), (3, 'cp', 'f'), (3, 'cn', 'f'), (3, 'co', 'f'),
 223               (1, 'Freq', 'f'), (1, 'kpp', 'f'), (1, 'kip', 'f'), (1, 'kdp', 'f')],
 224    'Fw-1.2': [(1, 'hw_major', 'B'), (1, 'hw_minor', 'B'), (1, 'fw_major', 'B'), (1, 'fw_minor', 'B'),
 225               (1, 'setting_type', 'B'), (1, 'conf', 'B'), (1, 'cal_meth_p', 'B'), (1, 'cal_meth_o', 'B'),
 226               (1, 'cal_meth_n', 'B'), (1, 'bttncfg', 'B'), (1, 'SwMode', 'B'), (1, 'SwSrc', 'B'), (1, 'VMode', 'B'),
 227               (1, 'Padding0', 'B'), (1, 'pid', 'H'), (1, 'Vmax', 'H'), (1, 'Vsetp_raw', 'H'), (1, 'Vsetn_raw', 'H'),
 228               (1, 'Duty', 'H'), (12, 'Name', 's'), (21, 'lookup_ADC_p', 'h'), (21, 'lookup_ADC_n', 'h'),
 229               (21, 'lookup_ADC_o', 'h'), (21, 'lookup_Vout', 'h'), (3, 'cp', 'f'), (3, 'cn', 'f'), (3, 'co', 'f'),
 230               (1, 'Freq', 'f'), (3, 'pid_p', 'f'), (3, 'pid_o', 'f'), (2, 'current', 'f'),
 231               (1, 'current_selection', 'B'), (1, 'wfm_pt_div', 'B'), (500, 'wfm_pts', 'H'), (1, 'wfm_n_pts', 'H')]
 232}
 233memory_string = {
 234    'Fw-1.0': '<14B5H12s84h13f',
 235    'Fw-1.1': '<14B5H12s84h13f',
 236    'Fw-1.2': '<14B5H12s84h18f2B501H'
 237}
 238
 239# Waveform parameters
 240
 241WFM_TP = 0.5e-3 # WFM pid update period
 242WFM_MAX_PTS = 500 # maximum number of pts.
 243
 244class HVPS:
 245    """ Class to control a petapicovoltron hvps-x
 246
 247    The class implements the low-level functions to interface the hvps-x firmware
 248    for easy communication with the hvps-x."""
 249
 250    # ====Communication functions and class constructor====
 251    def __init__(self, port, init=True):
 252        """ Initialises a HVPS object: dev = HVPS('/path/to/serial/port').
 253
 254        :param port: COM port to which the HVPS is connected.
 255        :param init: if True (default) initialises the hvps-x. Setting to false can be useful if the intended use is
 256        to check whether the device connected to the com port is a hvps-x"""
 257        self.name = ''
 258        self.vmax = 0
 259        self.err = 0
 260        self.ser = serial.Serial()  # the serial connection to the HVPS
 261        self.lookup_adc_p = []  # lookup tables for linear interpolation
 262        self.lookup_adc_o = []
 263        self.lookup_v_out = []
 264        self.cp = []  # calibration coefficients.
 265        self.co = []
 266        self.calmeth_p = -1
 267        self.calmeth_n = -1
 268        self.calmeth_o = -1
 269
 270        self.firmware = 0
 271        self.hardware = 0
 272        self.conf = 0  # configuration of the hvps-x
 273        self.is_hvpsx = 0
 274
 275        try:
 276            self.ser = serial.Serial(port, 115200, timeout=20)
 277        except serial.SerialException:
 278            self.err = self.err | ERR_PORT
 279            if DEBUG:
 280                print("Cannot connect to serial port. Probably busy.")
 281        else:
 282            self.ser.reset_input_buffer()
 283            if DEBUG:
 284                text = "connecting to " + port
 285                print(text)
 286                print("Serial port open")
 287            z = self._send_receive_4bytes(SERIAL_QCONF)
 288            if z == 0:  # no response: the device connected doesn't talk using this protocol
 289                self.err = self.err | ERR_COM
 290                if DEBUG:
 291                    print("Communication error: the connected device doesn't reply to commands")
 292            z_hvps = z & 0xFFFF
 293            z_conf = (z & 0xFFFF0000) >> 16
 294            if z_hvps == HVPSX_PID:
 295                self.is_hvpsx = 1
 296                if z_conf != CONF_UNCONF:
 297                    self.conf = z_conf
 298                else:
 299                    self.err = self.err | ERR_CONF  # the hvps-x is not configured
 300                    if DEBUG:
 301                        print("The hvps is not configured")
 302                z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s')
 303                z = z[0].split(b'\x00', 1)[0]
 304                self.name = z
 305            else:
 306                self.err = self.err | ERR_TYPE  # The connected device replied with Err! but is not a hvps-x
 307                if DEBUG:
 308                    print("Type error: the device replies to commands, but is not a hvps-x.")
 309            if init and not self.err:
 310                self.check_version_compatibility()
 311                self.q_vmax()
 312
 313    def close(self, zero=True):  # closes connection with the HVPS
 314        """Closes the connection to the HVPS. Sets voltage to 0 if board was connected
 315
 316        Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the
 317        communication. If False, the hvps-x is left in its current state"""
 318
 319        if self.ser.is_open:
 320            if not (self.err & ERR_COM):  # if connected board is not running the firmware, do not send command
 321                if zero:
 322                    self.s_sw_mode(SWMODE_OFF)  # for hvps-x, this will disable both optocouplers.
 323                    self.s_vset(0, DCDC_BOTH)  # set the voltage to 0 as a safety measure
 324            self.ser.close()
 325
 326        if DEBUG:
 327            print("Serial port closed")
 328
 329    def _read_4bytes_uint(self):
 330        ans = self.ser.read(4)
 331        z = int.from_bytes(ans, byteorder='little')
 332        return z
 333
 334    def _read_4bytes_int16(self):  # Read 4 bytes with a 16-bit signed int in bytes 0 and 1 (and ignore bytes 2,3)
 335        ans1 = self.ser.read(2)
 336        self.ser.read(2)  # discard the last 2 bytes
 337        z = int.from_bytes(ans1, byteorder='little', signed=True)
 338        return z
 339
 340    def _read_4bytes_float(self):
 341        ans = self.ser.read(4)
 342        z = struct.unpack('<f', ans)  # little endian: string starts with the low index of the array, which
 343        # represents the low bits of the float
 344        return z[0]
 345
 346    def _send_receive_4bytes(self, cmd, param1=0, param2=0, typ='uint'):
 347        param2 = round(param2)
 348        if param2 < 0:
 349            param2 = param2 & 0xFFFF  # represent is as two's complement
 350        cmd = cmd + (param1 << 8) + (param2 << 16)
 351        cmd_b = cmd.to_bytes(4, 'little')
 352        self.ser.write(cmd_b)
 353        if typ == 'uint':
 354            z = self._read_4bytes_uint()
 355        elif typ == 'int16':
 356            z = self._read_4bytes_int16()
 357        else:
 358            z = self._read_4bytes_float()
 359        return z
 360
 361    def _send_4bytes_receive_nbytes(self, cmd, param1=0, param2=0, fstring='<2B'):
 362        n = struct.calcsize(fstring)
 363        param2 = round(param2)
 364        if param2 < 0:
 365            param2 = param2 & 0xFFFF  # represent it as two's complement
 366        cmd = cmd + (param1 << 8) + (param2 << 16)
 367        cmd_b = cmd.to_bytes(4, 'little')
 368        self.ser.write(cmd_b)
 369        x = self.ser.read(n)
 370        z = struct.unpack(fstring, x)
 371        return z  # returns unpacked tuples
 372
 373    def _send_nbytes_receive_4bytes(self, cmd, packet, typ='uint'):
 374        packet = bytes([cmd]) + packet
 375        self.ser.write(packet)
 376        if typ == 'uint':
 377            z = self._read_4bytes_uint()
 378        elif typ == 'int16':
 379            z = self._read_4bytes_int16()
 380        else:
 381            z = self._read_4bytes_float()
 382        return z
 383
 384    def _download_voltage_calibration_data(self):
 385        # only download if required.
 386        if self.calmeth_p == -1:  # calibration method not yet fetched from HVPS
 387            self.q_calibration_method(which=VNOW_POS)
 388        if self.calmeth_n == -1:  # calibration method not yet fetched from HVPS
 389            self.q_calibration_method(which=VNOW_NEG)
 390        if self.calmeth_o == -1:  # calibration method not yet fetched from HVPS
 391            self.q_calibration_method(which=VNOW_OUT)
 392
 393        if (self.calmeth_o == CALMETH_LOOKUP or self.calmeth_p == CALMETH_LOOKUP) and not self.lookup_v_out:
 394            self.lookup_v_out = self.q_lookup(0)
 395        if self.calmeth_p == CALMETH_LOOKUP and not self.lookup_adc_p:
 396            self.lookup_adc_p = self.q_lookup(1)
 397        if self.calmeth_o == CALMETH_LOOKUP and not self.lookup_adc_o:
 398            self.lookup_adc_o = self.q_lookup(2)
 399
 400        if self.calmeth_p == CALMETH_POLYNOMIAL and not self.cp:  # if we don't know what are the calibration values
 401            self.cp.append(self.q_cal(CAL_C0P))
 402            self.cp.append(self.q_cal(CAL_C1P))
 403            self.cp.append(self.q_cal(CAL_C2P))
 404
 405        if self.calmeth_o == CALMETH_POLYNOMIAL and not self.co:  # if we don't know what are the calibration values
 406            self.co.append(self.q_cal(CAL_C0O))
 407            self.co.append(self.q_cal(CAL_C1O))
 408            self.co.append(self.q_cal(CAL_C2O))
 409
 410    def _convert_raw_to_voltage(self, vraw, which=VNOW_POS):
 411        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
 412            which = VNOW_POS
 413        # Download calibration values the first time the function is used
 414        self._download_voltage_calibration_data()
 415        if which == VNOW_POS:
 416            if self.calmeth_p == CALMETH_LOOKUP:
 417                vcal = np.interp(vraw, self.lookup_adc_p, self.lookup_v_out, left=None, right=None, period=None)
 418            else:
 419                vcal = self.cp[0] + self.cp[1] * vraw + self.cp[2] * np.square(vraw)
 420        elif which == VNOW_NEG:
 421           vcal = 0
 422        else:
 423            if self.calmeth_o == CALMETH_LOOKUP:
 424                vcal = np.interp(vraw, self.lookup_adc_o, self.lookup_v_out, left=None, right=None, period=None)
 425            else:
 426                vcal = self.co[0] + self.co[1] * vraw + self.co[2] * np.square(vraw)
 427        return vcal
 428
 429    def _convert_voltage_to_raw(self, v, which):
 430        # this function is not needed and can be removed.
 431        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
 432            which = VNOW_POS
 433        # Download calibration values the first time the function is used
 434        self._download_voltage_calibration_data()
 435
 436        if which == VNOW_POS:
 437            if self.calmeth_p == CALMETH_LOOKUP:
 438                vraw = np.interp(v, self.lookup_v_out, self.lookup_adc_p, left=None, right=None, period=None)
 439            elif self.cp[2] == 0:   # if polynomial but linear
 440                vraw = (v - self.cp[0]) / self.cp[1]
 441            else:   # polynomial quadratic
 442                vraw = (-self.cp[1] + np.sqrt(np.square(self.cp[1]) - 4 * self.cp[2] * (self.cp[0]-v)))/(2 * self.cp[2])
 443
 444        elif which == VNOW_NEG:
 445            vraw = 0     # bipolar option not implemented yet
 446        else:
 447            if self.calmeth_o == CALMETH_LOOKUP:
 448                vraw = np.interp(v, self.lookup_v_out, self.lookup_adc_o, left=None, right=None, period=None)
 449            elif self.co[2] == 0:   # if polynomial but linear
 450                vraw = (v - self.co[0]) / self.co[1]
 451            else:   # polynomial quadratic
 452                vraw = (-self.co[1] + np.sqrt(np.square(self.co[1]) - 4 * self.co[2] * (self.co[0]-v)))/(2 * self.co[2])
 453        return vraw
 454
 455    def is_bipolar(self):  # return true if connected device is hvpsx and bipolar unit
 456        """ Returns true if the hvps-x configuration is bipolar."""
 457        if self.conf & 1:
 458            return True
 459        else:
 460            return False
 461
 462    # ====Commands related to voltage and frequency====
 463    def s_vset(self, x, polarity=DCDC_POS):  # sets the output voltage
 464        """Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or
 465        when the HVPS is powered off. Using the
 466        save() command enables to save this parameter in memory\n
 467        Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is
 468        regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the
 469        output to the set point; see s_vmode() command.
 470        :param x: voltage set point in volt (int)
 471        :param polarity: which DC/DC converter to address (DCDC_POS (default), DCDC_NEG (bipolar config), DCDC_BOTH).
 472        This parameter can be ignored given that only unipolar configuration is currently supported.
 473        :return: voltage set point accepted by hvps-x
 474        """
 475        x = constrain(abs(x), 0, self.vmax)
 476        x = int(x)
 477        if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH:
 478            polarity = DCDC_POS
 479        z = self._send_receive_4bytes(SERIAL_SVSET, param1=polarity, param2=x)
 480        if x == 0:
 481            z = 0   # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in
 482            # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a
 483            # few 10s of volts when back calculated in volts
 484        if DEBUG:
 485            y = "s_vset(" + str(x) + ") -> " + str(z)
 486            print(y)
 487        return z
 488
 489    def q_vset(self, polarity=DCDC_POS):  # queries the voltage setpoint
 490        """Queries the voltage set point. The returned value is in volts.
 491        :param polarity: DCDC converter to be queried (DCDC_POS (default) or DCDC_NEG). DCDC_NEG will return 0 for
 492        unipolar configs. This parameter can be ignored given that only unipolar configuration is currently supported.
 493        :return: Voltage set point in volts
 494        """
 495        if polarity != DCDC_POS and polarity != DCDC_NEG:
 496            polarity = DCDC_POS
 497        z = self._send_receive_4bytes(SERIAL_QVSET, param1=polarity)
 498        if DEBUG:
 499            y = "q_vset -> " + str(z)
 500            print(y)
 501
 502        return z
 503
 504    def s_vset_raw(self, x, polarity=DCDC_POS):  # sets the output voltage
 505        """Sets the raw output voltage of the HVPS (0-4095). The new parameter remains valid until a new call to this
 506        command, or when the HVPS is powered off. Using the
 507        save() command enables to save this parameter in memory\n
 508        Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is
 509        regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the
 510        output to the set point; see s_vmode() command.
 511        :param x: raw voltage set point (0-4095)
 512        :param polarity: which DC/DC converter to address (DCDC_POS (default), DCDC_NEG (bipolar config), DCDC_BOTH).
 513        This parameter can be ignored given that only unipolar configuration is currently supported.
 514        :return: voltage set point accepted by hvps-x
 515        """
 516        x = constrain(x, 0, 4095)
 517        x = int(x)
 518        if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH:
 519            polarity = DCDC_POS
 520        z = self._send_receive_4bytes(SERIAL_SVSETRAW, param1=polarity, param2=x)
 521        if x == 0:
 522            z = 0   # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in
 523            # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a
 524            # few 10s of volts when back calculated in volts
 525        if DEBUG:
 526            y = "s_vset_raw(" + str(x) + ") -> " + str(z)
 527            print(y)
 528        return z
 529
 530    def q_vset_raw(self, polarity=DCDC_POS):  # queries the voltage setpoint
 531        """Queries the raw voltage set point. The returned value is between 0 and 4095.
 532
 533        :param polarity: DCDC converter to be queried (DCDC_POS (default) or DCDC_NEG). DCDC_NEG will return 0 for
 534        unipolar configs. This parameter can be ignored given that only unipolar configuration is currently supported.
 535        :return: Voltage raw set point (0-4095))
 536        """
 537        if polarity != DCDC_POS and polarity != DCDC_NEG:
 538            polarity = DCDC_POS
 539        z = self._send_receive_4bytes(SERIAL_QVSETRAW, param1=polarity)
 540        if DEBUG:
 541            y = "q_vset_raw -> " + str(z)
 542            print(y)
 543        return z
 544
 545    def q_vnow(self, which=VNOW_POS):  # queries the voltage output
 546        """Queries the feedback voltage of the HVPS.
 547        :param which: which output voltage to read {VNOW_POS (default), VNOW_NEG, VNOW_OUT}. Voltage at the output of the
 548        positive DCDC converter, negative DCDC converter, or output of the HVPS
 549        :return: voltage value in volts
 550        """
 551
 552        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
 553            which = VNOW_POS
 554        z = self._send_4bytes_receive_nbytes(SERIAL_QVNOW, param1=which, fstring='<2h')
 555        z_out = z[1]
 556        z_main = z[0]
 557        if DEBUG:
 558            y = "q_vnow -> " + str(z_main) + " / " + str(z_out)
 559            print(y)
 560        return z_main
 561
 562    def q_vnow_raw(self, which=VNOW_POS):  # queries a raw value of the voltage output
 563        """Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids
 564        running slow floating point calculations on the
 565        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
 566        execute than q_vnow(). Useful for streaming voltage values
 567        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
 568        positive DCDC converter, negative DCDC converter, or output of the HVPS.
 569        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
 570        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
 571        a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a
 572        way to do this automatically"""
 573
 574        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
 575            which = VNOW_POS
 576        z = self._send_receive_4bytes(SERIAL_QVNOWRAW, param1=which, typ='uint')
 577        z_out = z & 0xFFFF
 578        z_main = (z & 0xFFFF0000) >> 16
 579        if DEBUG:
 580            y = "q_vnow_raw -> " + str(z_main) + " , " + str(z_out)
 581            print(y)
 582        return z_main, z_out
 583
 584    def q_vnow_fast(self, which=VNOW_POS):  # queries the voltage output
 585        """Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage
 586        This avoids running slow floating point calculations on the
 587        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
 588        execute than q_vnow(). Useful for streaming voltage values.   This method is similar to q_vnow(), except that
 589        the conversion from a raw value to a calibrated value is done
 590        on the host rather than on the microcontroller. It can take up to 300us to convert a value on the MUC
 591        (it depends on the method used (linear, quadratic, lookup table)
 592        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
 593        positive DCDC converter, negative DCDC converter, or output of the HVPS.
 594        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
 595        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
 596        a 12 bit value that can be converted to voltage using the unit's calibration values The returned values
 597        are calibrated voltages in Volt"""
 598
 599        z_main, z_out = self.q_vnow_raw(which=which)
 600        v_main = self._convert_raw_to_voltage(z_main, which=which)
 601        v_out = self._convert_raw_to_voltage(z_out, which=VNOW_OUT)
 602
 603        return v_main, v_out
 604
 605    def s_f(self, x):  # sets the frequency
 606        """Sets the frequency of the signal when the HVPS is in switching or waveform mode (SWMODE_SW / SWMODE_SW).\n
 607        The value returned is the new frequency, taking quantification into account.
 608        :param x: frequency in Hz between 0.001 and 1000. In waveform mode, 0.16 Hz <= f <=100 Hz
 609        :return: frequency accepted by hvps-x in Hz
 610        """
 611        x = constrain(x, 0.001, 1000.0)
 612
 613        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
 614        # Ex: 10.0: 0x41 0x20 0x00 0x00.
 615        # 0 10000010 01000000000000000000000
 616        # S=0 Positive
 617        # Exp=130-127 (offset)=2^3=8
 618        # Fraction=1+2^-2=1.25
 619        # number=+1.25*8=10
 620        f_byte = struct.pack('<f', x)
 621        z = self._send_nbytes_receive_4bytes(SERIAL_SF, f_byte, typ='float')
 622
 623        if DEBUG:
 624            y = "s_f(" + str(x) + ") -> " + str(z)
 625            print(y)
 626        return z
 627
 628    def q_f(self):  # queries the frequency
 629        """Queries the frequency. The returned value is in Hz."""
 630        z = self._send_receive_4bytes(SERIAL_QF, param1=0, param2=0, typ='float')
 631
 632        if DEBUG:
 633            y = "q_f -> " + str(z)
 634            print(y)
 635
 636        return z
 637
 638    def s_current(self, xnorm, xhigh):  # sets the current limit setting
 639        """Sets the current output in both current settings (normal and high). This value is used to calculate the PID
 640         coefficients for the waveform. Values can be obtained by measuring the charging rate when charging a capacitor
 641         of known value. This is a configuration parameter that must be set according to the capabilities of the hvps-x.
 642         It is not a user-settable output current.
 643        :param xnorm: current in A between 50E-6 and 500E-6 in normal output current config.
 644        :param xhigh: current in A between 50E-6 and 500E-6 in high output current config.
 645        :return: SERIAL_OK or SERIAL_ERROR
 646        """
 647        xnorm = constrain(xnorm, 50E-6, 500E-6)
 648        xhigh= constrain(xhigh, 50E-6, 500E-6)
 649        f_byte = struct.pack('<2f', xnorm, xhigh)
 650        z = self._send_nbytes_receive_4bytes(SERIAL_SCURR, f_byte, typ='uint')
 651
 652        if DEBUG:
 653            y = "s_current({},{}) -> {}".format(xnorm, xhigh, z)
 654            print(y)
 655        return z
 656
 657    def q_current(self):  # queries the current limit setting
 658        """Queries the current limit setting. The returned value is in A.
 659
 660        :return: A tuple of two current values corresponding to the output current settings in normal and high current
 661        modes."""
 662
 663        z = self._send_4bytes_receive_nbytes(SERIAL_QCURR, param1=0, param2=0, fstring='<2f')
 664
 665        if DEBUG:
 666            y = "q_curr() -> " + str(z)
 667            print(y)
 668        return z
 669
 670    def s_current_selection(self, x):
 671        """Sets the position of the current selection jumper
 672        :param x: Position of the output current selection jumper. Either J2_3V3 (normal) or J2_5V depending on the
 673        jumper position. 3V3 and 5V are written on the silkscreen and respectively correspond to normal and high current
 674        modes. After changing the position of the jumper, this command should be run to reflect the change. The new
 675        setting will be kept until the unit is power-cycled. To make the selection permanent, use the save() command
 676        after the s_current_selection(x) command.
 677        :return: Accepted current selection jumper position (J2_3V3 or J2_5V)
 678        """
 679        x = int(np.abs(x))
 680        if x > J2_5V:
 681            x = J2_5V
 682        z = self._send_receive_4bytes(SERIAL_SCURRSEL, param1=x, param2=0, typ='uint')
 683
 684        if DEBUG:
 685            print("s_current_selection({}) --> {}".format(x, z))
 686        return z
 687
 688    def q_current_selection(self):
 689        """Queries the position of the current selection jumper
 690        :return: Current selection (J2_3V3 or J2_5V)
 691        """
 692        z = self._send_receive_4bytes(SERIAL_QCURRSEL, param1=0, param2=0, typ='uint')
 693
 694        if DEBUG:
 695            print("q_current_selection() --> {}".format(z))
 696        return z
 697
 698    def s_duty(self, x):  # sets the duty cycle of the switching signal
 699        """Sets the duty cycle of the switching signal
 700        :param x: the duty cycle (float in the range 0-1)
 701        :return: the current duty cycle (float between 0 and 1)
 702        """
 703        duty = int(x * 1000)  # hvps-x is coding duty cycle on a 0-1000 scale
 704
 705        z = self._send_receive_4bytes(SERIAL_SDUTY, param1=0, param2=duty, typ='uint')
 706        z = z / 1000
 707        if DEBUG:
 708            y = "s_duty(" + str(x) + ") -> " + str(z)
 709            print(y)
 710        return z
 711
 712    def q_duty(self):  # queries the duty cycle of the switching signal
 713        """queries the duty cycle of the switching signal
 714        :return: the current duty cycle (float between 0 and 1)
 715        """
 716
 717        z = self._send_receive_4bytes(SERIAL_QDUTY, param1=0, param2=0, typ='uint')
 718        z = float(z) / 1000.0
 719        if DEBUG:
 720            y = "q_duty() -> " + str(z)
 721            print(y)
 722        return z
 723
 724    # ===Commands to change the voltage control and switching behaviour (mode and source)====
 725    def s_sw_mode(self, x):  # sets the switching mode
 726        """Sets the switching mode of the hvps-x. \n
 727        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
 728        This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV
 729        DC/DC converter.
 730        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. This connects J4 to ground and
 731        SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency and with
 732        the specified duty cycle.
 733        SWMODE_WFM: The optocouplers are controlled with a PWM signal to reproduce the user-defined waveform in memory
 734        The program tab "Waveform" can be used to set the parameters of the waveform.
 735        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
 736        save() command enables to save this parameter in memory
 737        :param x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW, SWMODE_WFM
 738        :return: Switching mode set by the HVPS
 739        """
 740
 741        if x > SWMODE_LOW:
 742            x = SWMODE_OFF
 743
 744        z = self._send_receive_4bytes(SERIAL_SSWMODE, param1=x, param2=0, typ='uint')
 745        if DEBUG:
 746            y = "s_sw_mode(" + str(x) + ") -> " + str(z)
 747            print(y)
 748        return z
 749
 750    def q_sw_mode(self):  # queries the switching mode
 751        """queries the switching mode of the hvps-x. \n
 752        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
 753        This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV
 754        DC/DC converter.
 755        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. This connects J4 to ground and
 756        SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency and with
 757        the specified duty cycle.
 758        SWMODE_WFM: The optocouplers are controlled with a PWM signal to reproduce the user-defined waveform in memory
 759        The program tab "Waveform" can be used to set the parameters of the waveform.
 760        :return: Switching mode set by the HVPS: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW, SWMODE_WFM
 761        """
 762        z = self._send_receive_4bytes(SERIAL_QSWMODE, param1=0, param2=0, typ='uint')
 763        if DEBUG:
 764            y = "q_sw_mode -> " + str(z)
 765            print(y)
 766        return z
 767
 768    def s_sw_src(self, x):  # sets the switching source
 769        """Sets the source of the switching signal.
 770
 771        Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard
 772        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
 773        Using the save() command enables to save this parameter in memory
 774        :param x: SWSRC_TMR, or SWSRC_BTTN
 775        :return: SWSRC_TMR, or SWSRC_BTTN
 776        """
 777        if x > SWSRC_BTTN:
 778            x = SWSRC_TMR
 779        z = self._send_receive_4bytes(SERIAL_SSWSRC, param1=x, param2=0, typ='uint')
 780        if DEBUG:
 781            y = "s_sw_src(" + str(x) + ") -> " + str(z)
 782            print(y)
 783        return z
 784
 785    def q_sw_src(self):  # queries the switching source
 786        """queries the source of the switching signal.
 787
 788        queries the source of the switching signal. Output values are: SWSRC_TMR for onboard
 789        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
 790        Using the save() command enables to save this parameter in memory
 791        :return: SWSRC_TMR, or SWSRC_BTTN
 792        """
 793
 794        z = self._send_receive_4bytes(SERIAL_QSWSRC, param1=0, param2=0, typ='uint')
 795
 796        if DEBUG:
 797            y = "q_sw_src -> " + str(z)
 798            print(y)
 799        return z
 800
 801    def s_bttn_cfg(self, x):  # sets the configuration of the push button
 802        """Defines the behaviour of the push button
 803
 804        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
 805        save() command enables to save this parameter in memory
 806        :param x: 2-bit value (0x0 to 0x3). bit 0 - Defines the behaviour of the push button, when the switching source
 807        of the HVPS is set to the push button
 808        (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour
 809        (i.e. the high voltage is turned on as long as the button is pressed),
 810        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
 811        to turn it off).\n
 812        bit 1 - State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
 813        :return: the 2-bit button config value
 814        """
 815        if x > 3:
 816            x = 3
 817        z = self._send_receive_4bytes(SERIAL_SBTTNCFG, param1=x, param2=0, typ='uint')
 818        if DEBUG:
 819            y = "s_bttn_cfg(" + str(x) + ") -> " + str(z)
 820            print(y)
 821
 822        return z
 823
 824    def q_bttn_cfg(self):  # queries the latch mode of the push button
 825        """Queries the behaviour of the push button
 826
 827        :return: the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching
 828        source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above).
 829        Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed),
 830        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
 831        to turn it off).\n
 832        bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
 833        """
 834        z = self._send_receive_4bytes(SERIAL_QBTTNCFG, param1=0, param2=0, typ='uint')
 835        if DEBUG:
 836            y = "q_bttn_cfg -> " + str(z)
 837            print(y)
 838
 839        return z
 840
 841    def q_kill(self):  # queries the state of the kill button. Kill=1 means HV is disabled
 842        """Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at
 843        the hvps-x output, irrespective of any software setting.
 844        :return: 1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise.
 845        """
 846        z = self._send_receive_4bytes(SERIAL_QKILL, param1=0, param2=0, typ='uint')
 847
 848        if DEBUG:
 849            y = "q_kill -> " + str(z)
 850            print(y)
 851        return z
 852
 853    def s_v_mode(self, x):  # sets the voltage control mode
 854        """Sets the voltage control mode
 855
 856        Sets the voltage control mode (i.e. how is the value of the output voltage controlled):\n
 857        VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).\n
 858        VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).\n
 859        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
 860        save() command enables to save this parameter in memory\n
 861        VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However,
 862        the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output
 863        voltage remains within the allowed range
 864        :param x: VMODE_R, VMODE_O
 865        :return: VMODE_R, VMODE_O
 866        """
 867
 868        if x > VMODE_O:
 869            x = VMODE_R
 870
 871        z = self._send_receive_4bytes(SERIAL_SVMODE, param1=x, param2=0, typ='uint')
 872        if DEBUG:
 873            y = "s_v_mode(" + str(x) + ") -> " + str(z)
 874            print(y)
 875        return z
 876
 877    def q_v_mode(self):  # queries the switching source
 878        """Queries the voltage control mode
 879
 880        :return: VMODE_R internal voltage regulator, VMODE_O internal
 881        open loop control (on-board regulator disconnected).
 882        """
 883
 884        z = self._send_receive_4bytes(SERIAL_QVMODE, param1=0, param2=0, typ='uint')
 885
 886        if DEBUG:
 887            y = "q_v_mode -> " + str(z)
 888            print(y)
 889        return z
 890
 891    # ====Functions to set configuration parameters====
 892    def s_pid(self, x, pid=PID_KPP):
 893        """Sets the gains of the PIDs
 894        Use save() command to save the new values to memory
 895        :param x: the value (float) of the gain.
 896        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,O},
 897        for positive voltage PID, output voltage PID (used for the waveform mode).
 898        :return: the gain value
 899        """
 900        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
 901        # Ex: 10.0: 0x41 0x20 0x00 0x00.
 902        # 0 10000010 01000000000000000000000
 903        # S=0 Positive
 904        # Exp=130-127 (offset)=2^3=8
 905        # Fraction=1+2^-2=1.25
 906        # number=+1.25*8=10
 907        pid_byte = struct.pack('<Bf', pid, x)
 908        z = self._send_nbytes_receive_4bytes(SERIAL_SPID, pid_byte, typ='float')
 909
 910        if DEBUG:
 911            y = "s_pid(" + str(x) + "," + str(pid) + ") -> " + str(z)
 912            print(y)
 913        return z
 914
 915    def q_pid(self, pid=PID_KPP):  # queries the frequency
 916        """returns the gains of the PIDs
 917
 918        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,O},
 919        for positive voltage PID, output voltage PID (used for the waveform mode).
 920        :return: the gain value of the chosen parameter
 921        """
 922
 923        z = self._send_receive_4bytes(SERIAL_QPID, param1=pid, param2=0, typ='float')
 924
 925        if DEBUG:
 926            y = "q_pid(" + str(pid) + ") -> " + str(z)
 927            print(y)
 928
 929        return z
 930
 931    def s_cal(self, x, cal=CAL_C1P):
 932        """Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values)
 933        Use save() to commit the setting to memory
 934        :param x:  the value of the calibration coefficient (float)
 935        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,O}, for positive
 936        voltage measurement, output voltage measurement
 937        :return: the calibration value
 938        """
 939
 940        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
 941        # Ex: 10.0: 0x41 0x20 0x00 0x00.
 942        # 0 10000010 01000000000000000000000
 943        # S=0 Positive
 944        # Exp=130-127 (offset)=2^3=8
 945        # Fraction=1+2^-2=1.25
 946        # number=+1.25*8=10
 947        cal_byte = struct.pack('<Bf', cal, x)
 948        z = self._send_nbytes_receive_4bytes(SERIAL_SCAL, cal_byte, typ='float')
 949
 950        if DEBUG:
 951            y = "s_cal(" + str(x) + "," + str(cal) + ") -> " + str(z)
 952            print(y)
 953        return z
 954
 955    def q_cal(self, cal=CAL_C1P):  # queries the frequency
 956        """queries the calibration constants of the analogue input signals (conversion to calibrated voltage values)
 957
 958        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,O}, for positive
 959        voltage measurement, output voltage measurement
 960        :return: the calibration value
 961        """
 962
 963        z = self._send_receive_4bytes(SERIAL_QCAL, param1=cal, param2=0, typ='float')
 964
 965        if DEBUG:
 966            y = "q_cal(" + str(cal) + ") -> " + str(z)
 967            print(y)
 968        return z
 969
 970    def s_calibration_method(self, which=VNOW_POS, calmeth=CALMETH_POLYNOMIAL):
 971        if not (calmeth == CALMETH_POLYNOMIAL or calmeth == CALMETH_LOOKUP):
 972            calmeth = CALMETH_POLYNOMIAL
 973        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
 974            which = VNOW_POS
 975        z = self._send_receive_4bytes(SERIAL_SCALMETH, param1=which, param2=calmeth, typ='uint')
 976
 977        if DEBUG:
 978            y = "s_calibration_method({0}, {1}) -> {2}".format(which, calmeth, z)
 979            print(y)
 980        if which == VNOW_POS:
 981            self.calmeth_p = z
 982        elif which == VNOW_OUT:
 983            self.calmeth_o = z
 984        else:
 985            self.calmeth_n = z
 986
 987    def q_calibration_method(self, which=VNOW_POS):
 988        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
 989            which = VNOW_POS
 990        z = self._send_receive_4bytes(SERIAL_QCALMETH, param1=which, param2=0, typ='uint')
 991
 992        if DEBUG:
 993            y = "q_calibration_method({0}) -> {1}".format(which, z)
 994            print(y)
 995        if which == VNOW_POS:
 996            self.calmeth_p = z
 997        elif which == VNOW_NEG:
 998            self.calmeth_n = z
 999        else:
1000            self.calmeth_o = z
1001
1002    def s_lookup(self, x, n, table=LOOKUP_ADC_P):
1003        if n > 20:
1004            n = 20
1005        n = n << 2  # shift left 2 bits
1006        if table == LOOKUP_ADC_P:
1007            n = n | 0b01
1008        elif table == LOOKUP_ADC_O:
1009            n = n | 0b10
1010        x = int(x)
1011        z = self._send_receive_4bytes(SERIAL_SLKUP, param1=n, param2=x, typ='int16')
1012        if DEBUG:
1013            y = "s_lookup({0},{1},{2}) - > {3}".format(x, n, table, z)
1014            print(y)
1015
1016    def q_lookup(self, which):  # which=0: vout, which=1: ADC_p, which=2: ADC_o
1017        format_string = f'<21h'
1018        z = self._send_4bytes_receive_nbytes(SERIAL_QLKUP, param1=which, param2=0, fstring=format_string)
1019        lookup_list = list(z)
1020        if DEBUG:
1021            y = "q_lookup({0}) - > {1}".format(which, lookup_list)
1022            print(y)
1023        return lookup_list
1024
1025    def s_saturation_limit(self, limit):    # limit: 0-> reset to default 2% above Vmax. Otherwise:1-4095
1026        if limit < 0:
1027            limit = 0
1028        elif limit > 4095:
1029            limit = 4095
1030        z = self._send_receive_4bytes(SERIAL_SLKUP, param1=0, param2=limit, typ='int16')
1031        if DEBUG:
1032            y = "s_saturation_limit({0}) - > {1}".format(limit, z)
1033            print(y)
1034        return z
1035
1036    # ====User Waveform Functions====
1037    def s_wfm_pts(self, n=1, m=500, pts=None):
1038        """Sets the data points of the waveform
1039        Use save() to commit the setting to memory
1040        :param n:  The loop counter. The frequency of the signal is f = 1 / (n * m * 0.5E-3) 0 < n < 256. If you don't
1041        want to bother with n, set n = 1, and use s_f() after transferring the points to get the desired wfm frequency.
1042        :param m: The number of points in the waveform 0 <m <= 500
1043        :param pts: a list of integer voltage points. The list must be 500 elements long. If m<500, then the end of the list
1044        must be padded with zeros.
1045        :return: SERIAL_OK or SERIAL_ERROR
1046        """
1047        # parameters are 1) n (byte B) the counter divider, 2) the number of points m, an unsigned short, and 3) m unsigned short pts
1048        if pts is None:
1049            z = SERIAL_ERROR
1050            if DEBUG:
1051                print("s_wfm_pts(): ERROR: no points given")
1052        elif len(pts) == 500 and all(isinstance(item, int) for item in pts):
1053            n=int(n)
1054            if n > 255:
1055                n=255
1056            m=int(m)
1057            format_string = f'<B{WFM_MAX_PTS+1}H'
1058            buffer = struct.pack(format_string, n, m, *pts) # array pts always has WFM_MAX_PTS points, but with zeros as
1059            # padding at the end. m indicated the effective number of data points. This is to always have the same command
1060            # length
1061            z = self._send_nbytes_receive_4bytes(SERIAL_SWFMPTS, buffer)
1062            if DEBUG:
1063                y = "s_wfm_pts({0}, {1}, {2}) - > {3}".format(n, m, buffer, z)
1064                print(y)
1065            else:
1066                z = SERIAL_ERROR
1067                if DEBUG:
1068                    print("s_wfm_pts(): ERROR: number of points not equal to 500, or not all integer values")
1069        return z
1070
1071    def q_wfm_pts(self):
1072        """queries the data points of the waveform
1073        :return : n, z with n: the number of points in the waveform and z: the list of waveform points in Volts
1074        """
1075        format_string = f'<H500H' # 1 16-bit unsigned value for the number of points and 500 unsigned 16-bit values. Not all points are effective points (some may be padding zeroes) hence why n is required.
1076        z = self._send_4bytes_receive_nbytes(SERIAL_QWFMPTS, param1=0, param2=0, fstring=format_string)
1077        z = list(z)
1078        n = z.pop(0)    # the first element is the number of points
1079        del z[n:]       #remove all elements of the list after the number of points to keep
1080        if DEBUG:
1081            y = "q_wfm_pts() - > {}/{}".format(n, z)
1082            print(y)
1083        return n, z
1084
1085
1086    # Miscellaneous functions
1087    def save(self):  # save current HVPS parameters into the memory
1088        """save active HVPS parameters into the memory
1089
1090        This command saves the active parameters as \'current\' settings. Current setting are the settings that are
1091        loaded when power is applied to the hvps-x
1092
1093        :return: SERIAL_OK or SERIAL_ERROR
1094        """
1095
1096        z = self._send_receive_4bytes(SERIAL_SAVE)
1097
1098        if DEBUG:
1099            y = "save -> " + str(z)
1100            print(y)
1101        return z
1102
1103    def save_memory_to_file(self, settings=SETTINGS_CURRENT):
1104        """Dumps the content of the memory into a JSON file.
1105
1106        Dumps the content of the memory into a file. This is useful to keep a backup of the parameters on file.
1107        Files will be created in the interface folder and have the following format: Name_settings_type_date_time.json\n
1108        json files with settings can be transferred back to the hvps-x with the transfer_settings() method of the HVPS
1109        class, or the higher-level function transfer_file_to_memory()
1110        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
1111        :return: the name of the created file (it follows a standard naming scheme with the date, the name of the
1112        hvps-x and the setting type.
1113        """
1114
1115        memory = self.q_mem(settings)
1116        now = datetime.now()  # current date and time
1117        date_time = now.strftime("%Y_%m_%d")
1118        if settings == SETTINGS_CURRENT:
1119            st_string = '_cur_st_'
1120        else:
1121            st_string = '_bk_st_'
1122        file_name = self.name.decode("utf-8") + st_string + date_time + '.json'
1123        with open(file_name, "w") as write_file:
1124            json.dump(memory, write_file, indent=4)
1125
1126        return file_name
1127
1128    def s_settings(self,
1129                   settings=SETTINGS_CURRENT):  # sets the active setting to a particular type (useful before saving)
1130        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_FACTORY or settings == SETTINGS_BACKUP):
1131            settings = SETTINGS_CURRENT
1132        x = self._send_receive_4bytes(SERIAL_SST, param1=settings)
1133        if DEBUG:
1134            print('s_settings({0}) -> {1}'.format(settings, x))
1135        return x
1136
1137    def load_settings(self, settings=SETTINGS_CURRENT):  # Load a setting set from memory to the active settings
1138        """This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x
1139
1140        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_BACKUP
1141        :return:
1142        """
1143
1144        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_BACKUP):
1145            settings = SETTINGS_CURRENT
1146        x = self._send_receive_4bytes(SERIAL_LST, param1=settings)
1147        if DEBUG:
1148            print('load_settings({0}) -> {1}'.format(settings, x))
1149
1150        self._initialise_hvpsx()  # need to reread all parameters
1151
1152        return x
1153
1154    def transfer_settings(self, dict_settings):
1155        """ transfer a setting dictionary from the computer to the hvps-x memory
1156        :param dict_settings: a dictionary containing the settings values.
1157        :return: SERIAL_OK or SERIAL_ERROR
1158        The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together
1159        these two functions make it possible to backup the settings (this includes calibration and PID settings in a
1160        file, and gives the opportunity to restore the settings"""
1161
1162        error = False
1163        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1164        siz = struct.calcsize(memory_string[fw_string])
1165        buffer = ctypes.create_string_buffer(siz)
1166        memmap = memorymap[fw_string]
1167        counter = 0
1168        for x in memmap:
1169            n = x[0]
1170            key = x[1]
1171            data_format = x[2]
1172            if data_format == 'B' or data_format == 's':
1173                bytelength = 1
1174            elif data_format == 'h' or data_format == 'H':
1175                bytelength = 2
1176            elif data_format == 'f':
1177                bytelength = 4
1178            else:
1179                bytelength = 1
1180            data = dict_settings[key]
1181            if key == 'Name':
1182                data = bytes(data, 'utf-8')
1183                if len(data) > 12:
1184                    error = True
1185                    if DEBUG:
1186                        print("Error: name should not exceed 12 characters")
1187                else:
1188                    format_str = '<{0}{1}'.format(n, data_format)
1189                    struct.pack_into(format_str, buffer, counter, data)
1190            else:
1191                format_str = '<{0}'.format(data_format)
1192                if n == 1:
1193                    struct.pack_into(format_str, buffer, counter, data)
1194                else:
1195                    for i in range(n):
1196                        try:
1197                            struct.pack_into(format_str, buffer, counter + i * bytelength, data[i])
1198                        except IndexError:
1199                            error = True
1200                            if DEBUG:
1201                                print('setting dictionary does not fit the expected format')
1202
1203            counter = counter + n * bytelength
1204        data_fw = 'Fw-{0}.{1}'.format(dict_settings['fw_major'], dict_settings['fw_minor'])
1205        if data_fw != fw_string:
1206            error = True
1207            if DEBUG:
1208                print('Error: JSON file firmware version does not match firmware currently on hvps-x. Exiting')
1209            exit(0)
1210        if not error:
1211            buffer = b'xxx' + buffer    # Adds 3 random bytes. CMD + 3 random bytes means that when mapping the transfer
1212            # buffer to a structure, it will on an aligned memory address
1213            x = self._send_nbytes_receive_4bytes(SERIAL_XFERST, buffer)
1214        else:
1215            x = SERIAL_ERROR
1216
1217        if DEBUG:
1218            print("transfer_settings(...) -> {0}".format(x))
1219        return x
1220
1221    def copy_settings(self, src, dst):
1222        """Copies one set of settings to another location:\n
1223        Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings
1224        (for example if some temporary settings were saved as current settings, for example to experiment with new PID
1225        gain values)\
1226        Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new
1227        calibration as a set of back-up settings
1228        :param src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS)
1229        :param dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by
1230        the source
1231        :return: SERIAL_OK or SERIAL_ERROR
1232        """
1233        if (src == SETTINGS_CURRENT or src == SETTINGS_BACKUP) and (dst == SETTINGS_CURRENT or dst == SETTINGS_BACKUP):
1234            x = self._send_receive_4bytes(SERIAL_CPYST, param1=src, param2=dst)
1235        else:
1236            x = SERIAL_ERROR
1237
1238        if DEBUG:
1239            print('copy_settings({0},{1}) -> {2}'.format(src, dst, x))
1240
1241        return x
1242
1243    def q_mem(self, settings=SETTINGS_CURRENT):  # queries the content of the memory
1244        """
1245        :param settings: which of the two sets of setting sets to query. Either SETTINGS_CURRENT or SETTINGS_Backup
1246        :return: A dictionary with the content of the memory. This is similar to the function save_memory_to_file()
1247        except that it doesn't save the content to a file"""
1248        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1249        memmap = memorymap[fw_string]
1250        dict_mem = {}
1251
1252        z = self._send_4bytes_receive_nbytes(SERIAL_QMEM, settings, param2=0, fstring=memory_string[fw_string])
1253        bytecount = 0
1254        for i in range(len(memmap)):
1255            length = memmap[i][0]
1256            field = memmap[i][1]
1257            if field == 'Name':
1258                length = 1
1259            y = z[bytecount:bytecount + length]
1260            # if field != 'Padding':
1261            if length == 1:
1262                dict_mem[field] = y[0]
1263            else:
1264                dict_mem[field] = y
1265            bytecount = bytecount + length
1266
1267        dict_mem['Name'] = dict_mem['Name'].decode('UTF-8')
1268        dict_mem['Name'] = dict_mem['Name'].split("\0")[0]
1269
1270        if DEBUG:
1271            print(dict_mem)
1272        return dict_mem
1273
1274    def q_ver(self):  # queries the firmware version
1275        """returns the current version of the firmware / hardware running on the board."""
1276        z = self._send_receive_4bytes(SERIAL_QVER)
1277        Firm_minor = z & 0xFF
1278        Firm_major = (z >> 8) & 0xFF
1279        Hard_minor = (z >> 16) & 0xFF
1280        Hard_major = (z >> 24)
1281        self.firmware = Firm_major + Firm_minor / 10
1282        self.hardware = Hard_major + Hard_minor / 10
1283        if DEBUG:
1284            y = "q_ver -> {0} / {1}".format(self.firmware, self.hardware)
1285            print(y)
1286        return self.firmware, self.hardware
1287
1288    # Configuration functions
1289    def q_conf(self):  # queries the configuration
1290        """returns the configuration of the board.
1291        :return: the configuration of the board in the high 2 bytes (CONF_UNCONF, CONF_UNIPOLAR, or CONF_BIPOLAR)
1292        and the PID (product ID) in the low 2 bytes. This is 0x0632 for a hvps-x unit.
1293        """
1294
1295        z = self._send_receive_4bytes(SERIAL_QCONF)
1296        z_hvps = z & 0xFFFF
1297        z_conf = (z & 0xFFFF0000) >> 16
1298        if DEBUG:
1299            y = "q_conf -> " + hex(z_conf) + " / " + hex(z_hvps)
1300            print(y)
1301        self.conf = z_conf
1302        return z
1303
1304    def s_conf(self, bipolar):
1305        """Sets the configuration of the board
1306        :param bipolar: boolean. Is board bipolar (True), or unipolar (false)
1307        :return: bipolar: boolean
1308        """
1309        if bipolar:
1310            param = CONF_BIPOLAR
1311        else:
1312            param = CONF_UNIPOLAR
1313        z = self._send_receive_4bytes(SERIAL_SCONF, param1=param)
1314        if DEBUG:
1315            y = "s_conf -> " + hex(z)
1316            print(y)
1317            if z == SERIAL_ERROR:
1318                print("Error: this configuration is not recognised")
1319        self.conf = z
1320        return z
1321
1322    def s_vmax(self, x):  # sets the maximum voltage rating of the board
1323        """ sets the voltage rating of the hvps-x. Must match the EMCO DC/DC converter rating.
1324        :param x: Voltage rating of hvps-x in Volt
1325        :return: Voltage rating of hvps-x in Volt
1326        """
1327
1328        x = constrain(x, 0, 6000)
1329        z = self._send_receive_4bytes(SERIAL_SVMAX, param2=x)
1330        if DEBUG:
1331            y = "s_vmax(" + str(x) + ") -> " + str(z)
1332            print(y)
1333        self.vmax = z
1334        return z
1335
1336    def q_vmax(self):  # queries the voltage rating of the board
1337        """ Queries the maximal voltage of the board. The returned value is in volts.
1338        :return: board maximal voltage (V)
1339        """
1340        z = self._send_receive_4bytes(SERIAL_QVMAX)
1341        if DEBUG:
1342            y = "q_vmax -> " + str(z)
1343            print(y)
1344        self.vmax = z
1345        return z
1346
1347    def s_name(self, x):  # set the name of the HVPS
1348        """ Sets the name of the HVPS.
1349        :param x: Name of the hvps-x. 11 characters maximum
1350        :return: name accepted by hvps-x
1351        """
1352        ll = len(x)
1353        if ll <= 12:
1354            x = bytearray(x, 'utf-8')
1355            for i in range(12 - ll):  # pad the string with 0s
1356                x = x + b'\0'
1357            self._send_nbytes_receive_4bytes(SERIAL_SNAME, x, typ='uint')
1358            z = self.q_name()
1359        else:
1360            z = 'too long'
1361        if DEBUG:
1362            y = "s_name(" + str(x) + ") -> " + str(z)
1363            print(y)
1364        return z
1365
1366    def q_name(self):  # queries the name of the board
1367        """queries the name of the board
1368        :return: Name of the board
1369        """
1370
1371        # x = self._send_receive_4bytes(SERIAL_QNAME, param1=0)
1372        # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=4) << 32)
1373        # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=8) << 64)
1374        # x = x.to_bytes(12, 'little')
1375
1376        z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s')
1377        z = z[0].split(b'\x00', 1)[0]   # take first (only) element of the tuple and remove the 0s at the end of string
1378
1379        self.name = z
1380        if DEBUG:
1381            y = "q_name -> " + str(z)
1382            print(y)
1383        return z
1384
1385    def set_hardware_version(self, hw_major, hw_minor):
1386        param = (hw_major << 8) + hw_minor
1387        self._send_receive_4bytes(SERIAL_SHW, param2=param)
1388
1389    def q_load_parameters(self):
1390        z = self._send_4bytes_receive_nbytes(SERIAL_QLOAD, param1=0, param2=0, fstring='<4H')
1391        if np.isin(0, np.array(z)):
1392            r = 0
1393            c = 0
1394        else:
1395            dv = self._convert_raw_to_voltage(z[0], VNOW_OUT)
1396            pwm_dv = z[1]
1397            ssv = self._convert_raw_to_voltage(z[2], VNOW_OUT)
1398            pwm_ss = z[3]
1399            current_selection = self.q_current_selection()
1400            imax = self.q_current()[current_selection]
1401            i_step = imax * pwm_dv / 4095
1402            i_ss = imax * pwm_ss / 4095
1403            dv_dt = dv / 5E-3  # dv is measured over 20ms
1404            c = i_step / dv_dt * 1E9 # C in nF
1405            r = ssv / i_ss / 1E6   # R in MOhm
1406        if DEBUG:
1407            print('q_load_parameters() --> {} nF, {} MOhm'.format(c, r))
1408        return r, c
1409
1410    def check_version_compatibility(self):
1411        self.q_ver()
1412        lib_string = 'Lib-' + LIB_VER
1413        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1414        hw_string = 'Hw-{0:.1f}'.format(self.hardware)
1415
1416        list_compatible_fw = compatibility_dict[lib_string]
1417        if fw_string in list_compatible_fw:
1418            list_compatible_hw = compatibility_dict[fw_string]
1419            if hw_string not in list_compatible_hw:
1420                self.err |= ERR_FIRM
1421        else:
1422            self.err |= ERR_FIRM
1423
1424    def diagnostic_ADC(self, which_vnow=VNOW_POS):
1425        x = self._send_4bytes_receive_nbytes(SERIAL_DADC, which_vnow, 0, '<32H')
1426        return x
1427
1428    def q_trace(self, which_vnow=VNOW_POS):
1429        x = self._send_4bytes_receive_nbytes(SERIAL_QTRACE, which_vnow, 0, '<100H')
1430        x = np.array(x)
1431        if DEBUG:
1432            y = "trace({0}) -> {1}".format(which_vnow, x)
1433            print(y)
1434        return x
1435
1436    def s_trace_rate(self, rate=5):
1437        """Sets the rate at which trace points are stored in memory
1438        :param rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is
1439        calculating the voltages; currently evey 5ms.
1440        :return: the rate accepted by the hvps-x"""
1441
1442        rate = int(rate)
1443        if rate < 2:
1444            rate = 2
1445        x = self._send_receive_4bytes(SERIAL_STRRATE, param1=0, param2=rate, typ='uint')
1446        x = x / 10      # the function returns the number of ticks of the counter, which counts at 10kHz.
1447        if DEBUG:
1448            y = "s_trace_rate({0}) -> {1}".format(rate, x)
1449            print(y)
1450        return x
1451
1452    def q_trace_rate(self):
1453        """queries the rate at which trace points are stored in memory
1454        :param
1455        :return the rate at which trace points are stored in memory (in ms)"""
1456
1457        x = self._send_receive_4bytes(SERIAL_QTRRATE, param1=0, param2=0, typ='uint')
1458        x = x / 10  # the function returns the number of ticks of the counter, which counts at 10kHz.
1459        if DEBUG:
1460            y = "q_trace_rate() -> {0}".format(x)
1461            print(y)
1462        return x
1463
1464    def s_waveform_pwm(self, value):
1465
1466        value = constrain(value, -4095, 4095)
1467        value = int(value)
1468        x = self._send_receive_4bytes(SERIAL_WFMPWM, param1=0, param2=value, typ='uint')
1469        if DEBUG:
1470            y = "s_waveform_pwm({0}) -> {1}".format(value, x)
1471            print(y)
1472        return x
1473
1474
1475def list_hvps(list_all=False):
1476    """lists the hvps-x connected to PC in a dictionary.The key is the name of the HVPS, and the parameter is
1477    the associated serial port
1478    :param list_all: if True list all connected HVPS.
1479    if list_all is false, it will only list device that are configured. Using True enable to list unconfigured
1480    devices and give the opportunity to configure them
1481    :return: dictionary of connected hvps-x"""
1482
1483    hvpsx_ports = [  # creates a list with all STM32 VCP devices connected
1484        p.device
1485        for p in serial.tools.list_ports.comports()
1486        if (p.pid == 0x5740 and p.vid == 0x483)
1487    ]
1488    dict_hvps = {}
1489    for port in hvpsx_ports:
1490        dev = HVPS(port, init=False)  # Attempts to connect to HVPS but without initialisation
1491        if dev.is_hvpsx:
1492            if dev.conf != CONF_UNCONF or list_all:
1493                dict_hvps[dev.name.decode()] = port  # add an entry in the dictionary with the name of the
1494            # HVPS and the port
1495        dev.close(zero=False)
1496
1497    return dict_hvps
1498
1499
1500def connect_to_hvps(unconf=False):
1501    """Scan for connected hvps-x and connects to it if a single hvps-x is found. User can choose to which hvps-x to
1502    connect if more than one is detected.
1503    :param unconf: True/False. If true will connect to an unconfigured hvps-x. If False (default) only connects to a
1504    configured device
1505    :return: a HVPS object (or None if could not connect to a valid hvps-x
1506    If more than 1 hvps-x is connected to the computed, a list of names is displayed and the user must choose which one
1507    they want to connect to"""
1508    cont = False  # flag to decide whether to continue the program or close the COM port
1509    dev = None
1510
1511    ports = list_hvps(list_all=unconf)
1512    keys = list(ports.keys())
1513    if len(ports) == 0:
1514        print("No HVPS connected to the device. Terminating program")
1515        exit()
1516    elif len(ports) == 1:
1517        dev = HVPS(ports[keys[0]])  # connects to the only available HVPS
1518    else:
1519        print("List of connected HVPS:")
1520        for i in range(len(ports)):
1521            print(str(i) + ": " + keys[i])
1522        print("Enter the number of the board you want to connect to")
1523        connect_to = 0
1524        try:
1525            connect_to = int(input())
1526        except ValueError:
1527            print("Invalid entry. Terminating program")
1528            exit()
1529        if connect_to >= len(ports):
1530            print("Invalid entry. Terminating program")
1531            exit()
1532        else:
1533            dev = HVPS(ports[keys[connect_to]])
1534    error = dev.err
1535    if error & ERR_PORT:
1536        print("Cannot open COM port. Probably busy with another process. Terminating program")
1537    if error & ERR_COM:
1538        print("Cannot communicate with hvps-x. Has firmware been flashed?. Terminating program")
1539    if error & ERR_TYPE:
1540        print("Device connected this port is not recognised. Terminating program")
1541    if error & ERR_CONF:
1542        print("This hvps-x is not configured. Please configure unit before using it. "
1543              "Terminating program.")
1544        cont = True
1545    elif error & ERR_FIRM:
1546        print("Warning: your hvps-x library is not optimal for the firmware or the firmware is not optimal for your "
1547              "hardware. Refer to the website for a compatibility table")
1548        cont = True
1549    if error == 0:
1550        cont = True
1551    if cont:
1552        return dev
1553    else:
1554        return None
1555
1556
1557def configure(dev):
1558    """Script to use to for the initial configuration (or re-configuration) of an hvps-x. Follow prompt on the console.
1559    :param dev: an HVPS object
1560    :return: nothing
1561    """
1562    if dev != -1:
1563        config = configparser.RawConfigParser()
1564        config.read('config.ini')
1565
1566        if dev.conf != CONF_UNCONF:
1567            answer = input("It appears this hvps-x is already configured. (C)ontinue with configuration or (A)bort?: ")
1568            if answer == 'a' or answer == 'A':
1569                print("exiting configuration")
1570                dev.close()
1571                exit(-1)
1572        dev.s_conf(bipolar=False)   # This is now a constant as there is no bipolar configuration
1573        answer = input("Enter the version of your hvps-x PCB (hardware version), as printed on the PCB "
1574                       "(format: X.Y; e.g. 1.2): ")
1575        hw_list = answer.split('.')
1576        hw = int(hw_list[0]) + int(hw_list[1]) / 10
1577        if len(hw_list) != 2:
1578            print("Error, the format of the hardware string must be X.Y, e.g. 1.2. Exiting")
1579            exit(0)
1580        dev.set_hardware_version(int(hw_list[0]), int(hw_list[1]))
1581        print("The current name of the hvps-x is " + dev.q_name().decode("utf-8"))
1582        answer = input("Do you want to set/change the name of this hvps-x? Y/N?: ")
1583        if answer == 'y' or answer == 'Y':
1584            name = input("Enter hvps-x name (12 char max): ")
1585            if len(name) > 12:
1586                print("Name too long. Ignoring name change")
1587            else:
1588                dev.s_name(name)
1589        else:
1590            print("Leaving name unchanged")
1591        print("\nThe current maximal voltage rating of this HVPS is " + str(dev.q_vmax()) + " V")
1592        answer = input("Do you want to set the maximal voltage of the HVPS? Y/N: ")
1593        if answer == 'y' or answer == 'Y':
1594            answer = input("Enter the maximal voltage of the hvps-x in Volt. It must match the voltage rating of the "
1595                           "Emco DC/DC converter: ")
1596            print("Setting Vmax to " + answer + "V")
1597            dev.s_vmax(int(answer))
1598        else:
1599            print("Leaving Vmax unchanged")
1600        vmax = dev.q_vmax()
1601        conf_section = 'hvps-x-' + str(vmax)
1602        if config.has_section(conf_section):
1603            print("Default values for this voltage found in configuration file\n")
1604            answer = input("Do you want to replace the voltage calibration values stored in the hvps-x by the one from the config file "
1605                           "Y/N (choose Y if configuring a new hvps-x): ")
1606            if answer == 'Y' or answer == 'y':
1607                c0p = config.getfloat(conf_section, 'C0P')
1608                c1p = config.getfloat(conf_section, 'C1P')
1609                c2p = config.getfloat(conf_section, 'C2P')
1610                c0o = config.getfloat(conf_section, 'C0O')
1611                c1o = config.getfloat(conf_section, 'C1O')
1612                c2o = config.getfloat(conf_section, 'C2O')
1613
1614                dev.s_cal(c0p, CAL_C0P)
1615                dev.s_cal(c1p, CAL_C1P)
1616                dev.s_cal(c2p, CAL_C2P)
1617                dev.s_cal(c0o, CAL_C0O)
1618                dev.s_cal(c1o, CAL_C1O)
1619                dev.s_cal(c2o, CAL_C2O)
1620                print("Voltage calibration values set...\n")
1621            answer = input(
1622                "Reset PID values to their default values? Y/N (choose Y when configuring a board for the first "
1623                "time): ")
1624            if answer == 'Y' or answer == 'y':
1625                kpp = config.getfloat(conf_section, 'KPP')
1626                kip = config.getfloat(conf_section, 'KIP')
1627                kdp = config.getfloat(conf_section, 'KDP')
1628
1629                dev.s_pid(kpp, PID_KPP)
1630                dev.s_pid(kip, PID_KIP)
1631                dev.s_pid(kdp, PID_KDP)
1632                print("PID values set...\n")
1633
1634            if hw >= 1.4:
1635                answer = input("Do you want to set the output current values? Y/N (if No, then default values will "
1636                               "be used): ")
1637                if answer == 'Y' or answer == 'y':
1638                    inorm = input("Normal current mode: Enter the output current in uA (e.g. 120): ")
1639                    ihigh = input("High current mode: Enter the output current in uA (e.g. 120): ")
1640                    inorm = 1e-6 * float(inorm)
1641                    ihigh = 1e-6 * float(ihigh)
1642                else:
1643                    inorm = config.getfloat(conf_section, 'INORM')
1644                    ihigh = config.getfloat(conf_section, 'IHIGH')
1645                dev.s_current(inorm, ihigh)
1646                print("Current values set...\n")
1647                curr_sel = input("Position of the output current selection jumper:\n1: 3V3 (Norm)\n2: 5V (High)?\n"
1648                                 "Choose 1/2: ")
1649                if curr_sel == '2':
1650                    curr_sel = 1
1651                else:
1652                    curr_sel = 0
1653                dev.s_current_selection(curr_sel)
1654                print("Current selection jumper position set...\n")
1655
1656            print("hvps-x configured. It is recommended to perform a calibration of the voltage readout circuit!")
1657            print("Saving information to hvps-x memory. Backup-settings and default settings")
1658            dev.s_settings(SETTINGS_BACKUP)
1659            dev.save()
1660            dev.s_settings(SETTINGS_CURRENT)
1661            dev.save()
1662        else:
1663            print("Cannot find a section in the config file for a {0} V hvps-x. You need to enter values for voltage "
1664                  "calibration and PID manually ".format(vmax))
1665            dev.save()  # saves previous parameters anyway
1666
1667
1668def check_functionality(dev):
1669    """This script can be used after configuration to test the hvps-x functionality. Follow prompt on the console
1670
1671    :param dev: an HVPS object
1672    :return: nothing
1673    """
1674
1675    print("Important:\n"
1676          "This function is experimental and the threshold values are based on a limited number of assembled boards."
1677          "In particular:\n"
1678          "- If the test fails with a value very close to the threshold value, then your board is most likely fine.\n"
1679          "- This version of the script must be used with the bill of material revision 1. If you assembled your"
1680          "board with the original bill of material, this script will not return the correct output. You can use the"
1681          "script included with the library v1.0. Otherwise the website describe how the high-voltage functionality "
1682          "can be manually tested.\n"
1683          "- If unsure, contact Peta-pico-Voltron with test results (be sure to specify hardware version of PCB).\n\n")
1684    if not dev.q_kill():
1685        input("Place HV safety switch (S1) on position 0 and press any key.")
1686        if not dev.q_kill():
1687            input("Switch S1 appears not to be working. Check Switch S1 functionality and restart the test")
1688            dev.close()
1689            exit()
1690    print("During the course of this test, a moderate (~20% of full scale) voltage will be applied to the output of\n "
1691          "the hvps-x. Make sure that it nothing is connected to the output and that you are not touching the\n "
1692          "instrument\n")
1693    print("Voltage set point 0V. Output off")
1694    dev.s_vset(0, DCDC_BOTH)
1695    dev.s_sw_mode(SWMODE_OFF)
1696    dev.s_v_mode(VMODE_O)
1697    dev.s_sw_src(SWSRC_TMR)
1698
1699    print("---Testing HV enable switch (S1) ---")
1700    input("Place HV enable switch (S1) on position 1 and press any key")
1701    if dev.q_kill():
1702        print("S1 switch still reads off state. Check functionality of switch S1.\n Test FAILED. Exiting script...")
1703        dev.close()
1704        exit()
1705    else:
1706        print("***PASS\n")
1707
1708    print("---Testing Voltage monitoring at set point 0 ---")
1709    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1710    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1711    if vnow_p < 25:
1712        print("This is within the tolerance range V < 25. Continuing...")
1713
1714    else:
1715        print("This is outside of the tolerance range V < 25. Something appears to be wrong with the positive DCDC "
1716              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1717        dev.close()
1718        exit()
1719
1720    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1721    if vnow_o < 25:
1722        print("This is within the tolerance range V < 25. Continuing...")
1723    else:
1724        print("This is outside of the tolerance range V < 25. Something appears to be wrong with the output "
1725              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1726        dev.close()
1727        exit()
1728    print("***PASS\n")
1729
1730    print("---Testing Voltage monitoring at set point 20% FS Output off ---")
1731    print("Applying 20% PWM value")
1732    dev.s_vset(int(0.2 * dev.vmax), DCDC_POS)
1733    sleep(0.2)
1734    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1735    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1736    if vnow_p > 900:
1737        print("This is within the tolerance range V > 900. Continuing...")
1738    else:
1739        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1740              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1741        dev.close()
1742        exit()
1743
1744    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1745    if vnow_o < 100:
1746        print("This is within the tolerance range V < 100. Continuing...")
1747    else:
1748        print("This is outside of the tolerance range V < 100. Something appears to be wrong with the output "
1749              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1750        dev.close()
1751        exit()
1752    print("***PASS\n")
1753
1754    print("---Testing Voltage monitoring at set point 20% FS Output LOW ---")
1755    print("Output LOW")
1756    dev.s_sw_mode(SWMODE_LOW)
1757    sleep(0.2)
1758    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1759    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1760    if vnow_p > 900:
1761        print("This is within the tolerance range V > 900. Continuing...")
1762    else:
1763        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1764              "voltage monitoring circuit.\n Test Failed. Exiting script...")
1765        dev.close()
1766        exit()
1767
1768    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1769    if vnow_o < 25:
1770        print("This is within the tolerance range V < 25. Continuing...")
1771    else:
1772        print("This is outside of the tolerance range V < 25. Something appears to be wrong with the output "
1773              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1774        dev.close()
1775        exit()
1776    print("***PASS\n")
1777
1778    print("---Testing Voltage monitoring at set point 20% FS Output HIGH ---")
1779    print("Output HIGH")
1780    dev.s_sw_mode(SWMODE_HIGH)
1781    sleep(0.2)
1782    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1783    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1784    if vnow_p > 900:
1785        print("This is within the tolerance range V > 900. Continuing...")
1786    else:
1787        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1788              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1789        dev.close()
1790        exit()
1791
1792    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1793    if vnow_o > 900:
1794        print("This is within the tolerance range V > 900. Continuing...")
1795    else:
1796        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the output "
1797              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1798        dev.close()
1799        exit()
1800    print("***PASS\n")
1801    print("***hvps-x passes all tests***")
1802
1803    dev.s_vset(0, DCDC_BOTH)
1804    dev.close()
1805
1806
1807def main():
1808    dev = connect_to_hvps(unconf=True)
1809    if dev:
1810        print("Name: {0}".format(dev.q_name()))
1811        print("Vmax: {0} V".format(dev.vmax))
1812        print("\n Your choices:")
1813        print("[1] Initial configuration (to be performed after assembling the low-voltage components)")
1814        print("[2] Basic functionality test (to be performed after assembling the high-voltage components)")
1815        print("[q] quit\n")
1816        answer = input("Your choice: ")
1817        if answer == '1':
1818            configure(dev)
1819        elif answer == '2':
1820            check_functionality(dev)
1821        dev.close(zero=False)
1822
1823def constrain(val, min_val, max_val):
1824    """A simple implementation to constrain a value between two boundaries"""
1825    return min(max_val, max(min_val, val))
1826
1827if __name__ == "__main__":  # if the library is executed as the main program
1828    main()
DEBUG = False
LIB_VER = '1.2'
LIB_NAME = 'Release the kraken'
HVPSX_PID = 1586
compatibility_dict = {'Lib-1.0': ['Fw-1.0'], 'Lib-1.1': ['Fw-1.1'], 'Lib-1.2': ['Fw-1.2'], 'Fw-1.0': ['Hw-1.1', 'Hw-1.2', 'Hw-1.3'], 'Fw-1.1': ['Hw-1.1', 'Hw-1.2', 'Hw-1.3', 'Hw-1.4'], 'Fw-1.2': ['Hw-1.1', 'Hw-1.2', 'Hw-1.3', 'Hw-1.4']}
SWMODE_OFF = 0
SWMODE_DC = 1
SWMODE_SW = 2
SWMODE_WFRM = 3
SWMODE_HIGH = 4
SWMODE_LOW = 5
SWSRC_TMR = 0
SWSRC_EXT = 1
SWSRC_BTTN = 2
VMODE_R = 0
VMODE_EXT = 1
VMODE_O = 2
STMODE_OFF = 0
STMODE_FIXED = 1
STMODE_SWEEP = 2
LOOKUP_VOUT = 0
LOOKUP_ADC_P = 1
LOOKUP_ADC_O = 2
ERR_FIRM = 1
ERR_TYPE = 2
ERR_JACK = 4
ERR_COM = 8
ERR_CONF = 16
ERR_PORT = 32
ERR_CMD = 64
SERIAL_ERROR = 561148485
SERIAL_OK = 1263487823
SERIAL_SVMAX = 1
SERIAL_QVMAX = 2
SERIAL_SVSET = 3
SERIAL_QVSET = 4
SERIAL_SAVE = 5
SERIAL_SCONF = 6
SERIAL_QCONF = 7
SERIAL_QVER = 8
SERIAL_SNAME = 9
SERIAL_QNAME = 10
SERIAL_SF = 11
SERIAL_QF = 12
SERIAL_SSWMODE = 13
SERIAL_QSWMODE = 14
SERIAL_SDUTY = 15
SERIAL_QDUTY = 16
SERIAL_SPID = 17
SERIAL_QPID = 18
SERIAL_SCAL = 19
SERIAL_QCAL = 20
SERIAL_SBTTNCFG = 21
SERIAL_QBTTNCFG = 22
SERIAL_SSWSRC = 23
SERIAL_QSWSRC = 24
SERIAL_SVMODE = 25
SERIAL_QVMODE = 26
SERIAL_QVNOW = 27
SERIAL_QKILL = 28
SERIAL_QVNOWRAW = 29
SERIAL_SLKUP = 30
SERIAL_QLKUP = 31
SERIAL_SST = 32
SERIAL_SHW = 33
SERIAL_SCALMETH = 34
SERIAL_QCALMETH = 35
SERIAL_QMEM = 36
SERIAL_LST = 37
SERIAL_CPYST = 38
SERIAL_XFERST = 39
SERIAL_QTRACE = 40
SERIAL_STRRATE = 41
SERIAL_QTRRATE = 42
SERIAL_SWFMPTS = 43
SERIAL_SSATLIM = 44
SERIAL_QWFMPTS = 45
SERIAL_SCURR = 46
SERIAL_QCURR = 47
SERIAL_QLOAD = 48
SERIAL_SVSETRAW = 49
SERIAL_QVSETRAW = 50
SERIAL_SCURRSEL = 51
SERIAL_QCURRSEL = 52
SERIAL_DADC = 160
SERIAL_WFMPWM = 161
CONF_UNCONF = 0
CONF_BIPOLAR = 1
CONF_UNIPOLAR = 2
SETTINGS_CURRENT = 0
SETTINGS_BACKUP = 1
SETTINGS_FACTORY = 2
CALMETH_POLYNOMIAL = 0
CALMETH_LOOKUP = 1
DCDC_POS = 1
DCDC_NEG = 2
DCDC_BOTH = 3
VNOW_POS = 0
VNOW_NEG = 1
VNOW_OUT = 2
PID_KPP = 0
PID_KIP = 1
PID_KDP = 2
PID_KPN = 3
PID_KIN = 4
PID_KDN = 5
PID_KPO = 6
PID_KIO = 7
PID_KDO = 8
CAL_C0P = 0
CAL_C1P = 1
CAL_C2P = 2
CAL_C0N = 3
CAL_C1N = 4
CAL_C2N = 5
CAL_C0O = 6
CAL_C1O = 7
CAL_C2O = 8
J2_5V = 1
J2_3V3 = 0
memorymap = {'Fw-1.0': [(1, 'hw_major', 'B'), (1, 'hw_minor', 'B'), (1, 'fw_major', 'B'), (1, 'fw_minor', 'B'), (1, 'setting_type', 'B'), (1, 'conf', 'B'), (1, 'cal_meth_p', 'B'), (1, 'cal_meth_o', 'B'), (1, 'cal_meth_n', 'B'), (1, 'bttncfg', 'B'), (1, 'SwMode', 'B'), (1, 'SwSrc', 'B'), (1, 'VMode', 'B'), (1, 'Padding', 'B'), (1, 'pid', 'H'), (1, 'Vmax', 'H'), (1, 'Vsetp_raw', 'H'), (1, 'Vsetn_raw', 'H'), (1, 'Duty', 'H'), (12, 'Name', 's'), (21, 'lookup_ADC_p', 'h'), (21, 'lookup_ADC_n', 'h'), (21, 'lookup_ADC_o', 'h'), (21, 'lookup_Vout', 'h'), (3, 'cp', 'f'), (3, 'cn', 'f'), (3, 'co', 'f'), (1, 'Freq', 'f'), (1, 'kpp', 'f'), (1, 'kip', 'f'), (1, 'kdp', 'f')], 'Fw-1.1': [(1, 'hw_major', 'B'), (1, 'hw_minor', 'B'), (1, 'fw_major', 'B'), (1, 'fw_minor', 'B'), (1, 'setting_type', 'B'), (1, 'conf', 'B'), (1, 'cal_meth_p', 'B'), (1, 'cal_meth_o', 'B'), (1, 'cal_meth_n', 'B'), (1, 'bttncfg', 'B'), (1, 'SwMode', 'B'), (1, 'SwSrc', 'B'), (1, 'VMode', 'B'), (1, 'Padding', 'B'), (1, 'pid', 'H'), (1, 'Vmax', 'H'), (1, 'Vsetp_raw', 'H'), (1, 'Vsetn_raw', 'H'), (1, 'Duty', 'H'), (12, 'Name', 's'), (21, 'lookup_ADC_p', 'h'), (21, 'lookup_ADC_n', 'h'), (21, 'lookup_ADC_o', 'h'), (21, 'lookup_Vout', 'h'), (3, 'cp', 'f'), (3, 'cn', 'f'), (3, 'co', 'f'), (1, 'Freq', 'f'), (1, 'kpp', 'f'), (1, 'kip', 'f'), (1, 'kdp', 'f')], 'Fw-1.2': [(1, 'hw_major', 'B'), (1, 'hw_minor', 'B'), (1, 'fw_major', 'B'), (1, 'fw_minor', 'B'), (1, 'setting_type', 'B'), (1, 'conf', 'B'), (1, 'cal_meth_p', 'B'), (1, 'cal_meth_o', 'B'), (1, 'cal_meth_n', 'B'), (1, 'bttncfg', 'B'), (1, 'SwMode', 'B'), (1, 'SwSrc', 'B'), (1, 'VMode', 'B'), (1, 'Padding0', 'B'), (1, 'pid', 'H'), (1, 'Vmax', 'H'), (1, 'Vsetp_raw', 'H'), (1, 'Vsetn_raw', 'H'), (1, 'Duty', 'H'), (12, 'Name', 's'), (21, 'lookup_ADC_p', 'h'), (21, 'lookup_ADC_n', 'h'), (21, 'lookup_ADC_o', 'h'), (21, 'lookup_Vout', 'h'), (3, 'cp', 'f'), (3, 'cn', 'f'), (3, 'co', 'f'), (1, 'Freq', 'f'), (3, 'pid_p', 'f'), (3, 'pid_o', 'f'), (2, 'current', 'f'), (1, 'current_selection', 'B'), (1, 'wfm_pt_div', 'B'), (500, 'wfm_pts', 'H'), (1, 'wfm_n_pts', 'H')]}
memory_string = {'Fw-1.0': '<14B5H12s84h13f', 'Fw-1.1': '<14B5H12s84h13f', 'Fw-1.2': '<14B5H12s84h18f2B501H'}
WFM_TP = 0.0005
WFM_MAX_PTS = 500
class HVPS:
 245class HVPS:
 246    """ Class to control a petapicovoltron hvps-x
 247
 248    The class implements the low-level functions to interface the hvps-x firmware
 249    for easy communication with the hvps-x."""
 250
 251    # ====Communication functions and class constructor====
 252    def __init__(self, port, init=True):
 253        """ Initialises a HVPS object: dev = HVPS('/path/to/serial/port').
 254
 255        :param port: COM port to which the HVPS is connected.
 256        :param init: if True (default) initialises the hvps-x. Setting to false can be useful if the intended use is
 257        to check whether the device connected to the com port is a hvps-x"""
 258        self.name = ''
 259        self.vmax = 0
 260        self.err = 0
 261        self.ser = serial.Serial()  # the serial connection to the HVPS
 262        self.lookup_adc_p = []  # lookup tables for linear interpolation
 263        self.lookup_adc_o = []
 264        self.lookup_v_out = []
 265        self.cp = []  # calibration coefficients.
 266        self.co = []
 267        self.calmeth_p = -1
 268        self.calmeth_n = -1
 269        self.calmeth_o = -1
 270
 271        self.firmware = 0
 272        self.hardware = 0
 273        self.conf = 0  # configuration of the hvps-x
 274        self.is_hvpsx = 0
 275
 276        try:
 277            self.ser = serial.Serial(port, 115200, timeout=20)
 278        except serial.SerialException:
 279            self.err = self.err | ERR_PORT
 280            if DEBUG:
 281                print("Cannot connect to serial port. Probably busy.")
 282        else:
 283            self.ser.reset_input_buffer()
 284            if DEBUG:
 285                text = "connecting to " + port
 286                print(text)
 287                print("Serial port open")
 288            z = self._send_receive_4bytes(SERIAL_QCONF)
 289            if z == 0:  # no response: the device connected doesn't talk using this protocol
 290                self.err = self.err | ERR_COM
 291                if DEBUG:
 292                    print("Communication error: the connected device doesn't reply to commands")
 293            z_hvps = z & 0xFFFF
 294            z_conf = (z & 0xFFFF0000) >> 16
 295            if z_hvps == HVPSX_PID:
 296                self.is_hvpsx = 1
 297                if z_conf != CONF_UNCONF:
 298                    self.conf = z_conf
 299                else:
 300                    self.err = self.err | ERR_CONF  # the hvps-x is not configured
 301                    if DEBUG:
 302                        print("The hvps is not configured")
 303                z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s')
 304                z = z[0].split(b'\x00', 1)[0]
 305                self.name = z
 306            else:
 307                self.err = self.err | ERR_TYPE  # The connected device replied with Err! but is not a hvps-x
 308                if DEBUG:
 309                    print("Type error: the device replies to commands, but is not a hvps-x.")
 310            if init and not self.err:
 311                self.check_version_compatibility()
 312                self.q_vmax()
 313
 314    def close(self, zero=True):  # closes connection with the HVPS
 315        """Closes the connection to the HVPS. Sets voltage to 0 if board was connected
 316
 317        Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the
 318        communication. If False, the hvps-x is left in its current state"""
 319
 320        if self.ser.is_open:
 321            if not (self.err & ERR_COM):  # if connected board is not running the firmware, do not send command
 322                if zero:
 323                    self.s_sw_mode(SWMODE_OFF)  # for hvps-x, this will disable both optocouplers.
 324                    self.s_vset(0, DCDC_BOTH)  # set the voltage to 0 as a safety measure
 325            self.ser.close()
 326
 327        if DEBUG:
 328            print("Serial port closed")
 329
 330    def _read_4bytes_uint(self):
 331        ans = self.ser.read(4)
 332        z = int.from_bytes(ans, byteorder='little')
 333        return z
 334
 335    def _read_4bytes_int16(self):  # Read 4 bytes with a 16-bit signed int in bytes 0 and 1 (and ignore bytes 2,3)
 336        ans1 = self.ser.read(2)
 337        self.ser.read(2)  # discard the last 2 bytes
 338        z = int.from_bytes(ans1, byteorder='little', signed=True)
 339        return z
 340
 341    def _read_4bytes_float(self):
 342        ans = self.ser.read(4)
 343        z = struct.unpack('<f', ans)  # little endian: string starts with the low index of the array, which
 344        # represents the low bits of the float
 345        return z[0]
 346
 347    def _send_receive_4bytes(self, cmd, param1=0, param2=0, typ='uint'):
 348        param2 = round(param2)
 349        if param2 < 0:
 350            param2 = param2 & 0xFFFF  # represent is as two's complement
 351        cmd = cmd + (param1 << 8) + (param2 << 16)
 352        cmd_b = cmd.to_bytes(4, 'little')
 353        self.ser.write(cmd_b)
 354        if typ == 'uint':
 355            z = self._read_4bytes_uint()
 356        elif typ == 'int16':
 357            z = self._read_4bytes_int16()
 358        else:
 359            z = self._read_4bytes_float()
 360        return z
 361
 362    def _send_4bytes_receive_nbytes(self, cmd, param1=0, param2=0, fstring='<2B'):
 363        n = struct.calcsize(fstring)
 364        param2 = round(param2)
 365        if param2 < 0:
 366            param2 = param2 & 0xFFFF  # represent it as two's complement
 367        cmd = cmd + (param1 << 8) + (param2 << 16)
 368        cmd_b = cmd.to_bytes(4, 'little')
 369        self.ser.write(cmd_b)
 370        x = self.ser.read(n)
 371        z = struct.unpack(fstring, x)
 372        return z  # returns unpacked tuples
 373
 374    def _send_nbytes_receive_4bytes(self, cmd, packet, typ='uint'):
 375        packet = bytes([cmd]) + packet
 376        self.ser.write(packet)
 377        if typ == 'uint':
 378            z = self._read_4bytes_uint()
 379        elif typ == 'int16':
 380            z = self._read_4bytes_int16()
 381        else:
 382            z = self._read_4bytes_float()
 383        return z
 384
 385    def _download_voltage_calibration_data(self):
 386        # only download if required.
 387        if self.calmeth_p == -1:  # calibration method not yet fetched from HVPS
 388            self.q_calibration_method(which=VNOW_POS)
 389        if self.calmeth_n == -1:  # calibration method not yet fetched from HVPS
 390            self.q_calibration_method(which=VNOW_NEG)
 391        if self.calmeth_o == -1:  # calibration method not yet fetched from HVPS
 392            self.q_calibration_method(which=VNOW_OUT)
 393
 394        if (self.calmeth_o == CALMETH_LOOKUP or self.calmeth_p == CALMETH_LOOKUP) and not self.lookup_v_out:
 395            self.lookup_v_out = self.q_lookup(0)
 396        if self.calmeth_p == CALMETH_LOOKUP and not self.lookup_adc_p:
 397            self.lookup_adc_p = self.q_lookup(1)
 398        if self.calmeth_o == CALMETH_LOOKUP and not self.lookup_adc_o:
 399            self.lookup_adc_o = self.q_lookup(2)
 400
 401        if self.calmeth_p == CALMETH_POLYNOMIAL and not self.cp:  # if we don't know what are the calibration values
 402            self.cp.append(self.q_cal(CAL_C0P))
 403            self.cp.append(self.q_cal(CAL_C1P))
 404            self.cp.append(self.q_cal(CAL_C2P))
 405
 406        if self.calmeth_o == CALMETH_POLYNOMIAL and not self.co:  # if we don't know what are the calibration values
 407            self.co.append(self.q_cal(CAL_C0O))
 408            self.co.append(self.q_cal(CAL_C1O))
 409            self.co.append(self.q_cal(CAL_C2O))
 410
 411    def _convert_raw_to_voltage(self, vraw, which=VNOW_POS):
 412        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
 413            which = VNOW_POS
 414        # Download calibration values the first time the function is used
 415        self._download_voltage_calibration_data()
 416        if which == VNOW_POS:
 417            if self.calmeth_p == CALMETH_LOOKUP:
 418                vcal = np.interp(vraw, self.lookup_adc_p, self.lookup_v_out, left=None, right=None, period=None)
 419            else:
 420                vcal = self.cp[0] + self.cp[1] * vraw + self.cp[2] * np.square(vraw)
 421        elif which == VNOW_NEG:
 422           vcal = 0
 423        else:
 424            if self.calmeth_o == CALMETH_LOOKUP:
 425                vcal = np.interp(vraw, self.lookup_adc_o, self.lookup_v_out, left=None, right=None, period=None)
 426            else:
 427                vcal = self.co[0] + self.co[1] * vraw + self.co[2] * np.square(vraw)
 428        return vcal
 429
 430    def _convert_voltage_to_raw(self, v, which):
 431        # this function is not needed and can be removed.
 432        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
 433            which = VNOW_POS
 434        # Download calibration values the first time the function is used
 435        self._download_voltage_calibration_data()
 436
 437        if which == VNOW_POS:
 438            if self.calmeth_p == CALMETH_LOOKUP:
 439                vraw = np.interp(v, self.lookup_v_out, self.lookup_adc_p, left=None, right=None, period=None)
 440            elif self.cp[2] == 0:   # if polynomial but linear
 441                vraw = (v - self.cp[0]) / self.cp[1]
 442            else:   # polynomial quadratic
 443                vraw = (-self.cp[1] + np.sqrt(np.square(self.cp[1]) - 4 * self.cp[2] * (self.cp[0]-v)))/(2 * self.cp[2])
 444
 445        elif which == VNOW_NEG:
 446            vraw = 0     # bipolar option not implemented yet
 447        else:
 448            if self.calmeth_o == CALMETH_LOOKUP:
 449                vraw = np.interp(v, self.lookup_v_out, self.lookup_adc_o, left=None, right=None, period=None)
 450            elif self.co[2] == 0:   # if polynomial but linear
 451                vraw = (v - self.co[0]) / self.co[1]
 452            else:   # polynomial quadratic
 453                vraw = (-self.co[1] + np.sqrt(np.square(self.co[1]) - 4 * self.co[2] * (self.co[0]-v)))/(2 * self.co[2])
 454        return vraw
 455
 456    def is_bipolar(self):  # return true if connected device is hvpsx and bipolar unit
 457        """ Returns true if the hvps-x configuration is bipolar."""
 458        if self.conf & 1:
 459            return True
 460        else:
 461            return False
 462
 463    # ====Commands related to voltage and frequency====
 464    def s_vset(self, x, polarity=DCDC_POS):  # sets the output voltage
 465        """Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or
 466        when the HVPS is powered off. Using the
 467        save() command enables to save this parameter in memory\n
 468        Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is
 469        regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the
 470        output to the set point; see s_vmode() command.
 471        :param x: voltage set point in volt (int)
 472        :param polarity: which DC/DC converter to address (DCDC_POS (default), DCDC_NEG (bipolar config), DCDC_BOTH).
 473        This parameter can be ignored given that only unipolar configuration is currently supported.
 474        :return: voltage set point accepted by hvps-x
 475        """
 476        x = constrain(abs(x), 0, self.vmax)
 477        x = int(x)
 478        if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH:
 479            polarity = DCDC_POS
 480        z = self._send_receive_4bytes(SERIAL_SVSET, param1=polarity, param2=x)
 481        if x == 0:
 482            z = 0   # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in
 483            # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a
 484            # few 10s of volts when back calculated in volts
 485        if DEBUG:
 486            y = "s_vset(" + str(x) + ") -> " + str(z)
 487            print(y)
 488        return z
 489
 490    def q_vset(self, polarity=DCDC_POS):  # queries the voltage setpoint
 491        """Queries the voltage set point. The returned value is in volts.
 492        :param polarity: DCDC converter to be queried (DCDC_POS (default) or DCDC_NEG). DCDC_NEG will return 0 for
 493        unipolar configs. This parameter can be ignored given that only unipolar configuration is currently supported.
 494        :return: Voltage set point in volts
 495        """
 496        if polarity != DCDC_POS and polarity != DCDC_NEG:
 497            polarity = DCDC_POS
 498        z = self._send_receive_4bytes(SERIAL_QVSET, param1=polarity)
 499        if DEBUG:
 500            y = "q_vset -> " + str(z)
 501            print(y)
 502
 503        return z
 504
 505    def s_vset_raw(self, x, polarity=DCDC_POS):  # sets the output voltage
 506        """Sets the raw output voltage of the HVPS (0-4095). The new parameter remains valid until a new call to this
 507        command, or when the HVPS is powered off. Using the
 508        save() command enables to save this parameter in memory\n
 509        Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is
 510        regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the
 511        output to the set point; see s_vmode() command.
 512        :param x: raw voltage set point (0-4095)
 513        :param polarity: which DC/DC converter to address (DCDC_POS (default), DCDC_NEG (bipolar config), DCDC_BOTH).
 514        This parameter can be ignored given that only unipolar configuration is currently supported.
 515        :return: voltage set point accepted by hvps-x
 516        """
 517        x = constrain(x, 0, 4095)
 518        x = int(x)
 519        if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH:
 520            polarity = DCDC_POS
 521        z = self._send_receive_4bytes(SERIAL_SVSETRAW, param1=polarity, param2=x)
 522        if x == 0:
 523            z = 0   # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in
 524            # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a
 525            # few 10s of volts when back calculated in volts
 526        if DEBUG:
 527            y = "s_vset_raw(" + str(x) + ") -> " + str(z)
 528            print(y)
 529        return z
 530
 531    def q_vset_raw(self, polarity=DCDC_POS):  # queries the voltage setpoint
 532        """Queries the raw voltage set point. The returned value is between 0 and 4095.
 533
 534        :param polarity: DCDC converter to be queried (DCDC_POS (default) or DCDC_NEG). DCDC_NEG will return 0 for
 535        unipolar configs. This parameter can be ignored given that only unipolar configuration is currently supported.
 536        :return: Voltage raw set point (0-4095))
 537        """
 538        if polarity != DCDC_POS and polarity != DCDC_NEG:
 539            polarity = DCDC_POS
 540        z = self._send_receive_4bytes(SERIAL_QVSETRAW, param1=polarity)
 541        if DEBUG:
 542            y = "q_vset_raw -> " + str(z)
 543            print(y)
 544        return z
 545
 546    def q_vnow(self, which=VNOW_POS):  # queries the voltage output
 547        """Queries the feedback voltage of the HVPS.
 548        :param which: which output voltage to read {VNOW_POS (default), VNOW_NEG, VNOW_OUT}. Voltage at the output of the
 549        positive DCDC converter, negative DCDC converter, or output of the HVPS
 550        :return: voltage value in volts
 551        """
 552
 553        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
 554            which = VNOW_POS
 555        z = self._send_4bytes_receive_nbytes(SERIAL_QVNOW, param1=which, fstring='<2h')
 556        z_out = z[1]
 557        z_main = z[0]
 558        if DEBUG:
 559            y = "q_vnow -> " + str(z_main) + " / " + str(z_out)
 560            print(y)
 561        return z_main
 562
 563    def q_vnow_raw(self, which=VNOW_POS):  # queries a raw value of the voltage output
 564        """Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids
 565        running slow floating point calculations on the
 566        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
 567        execute than q_vnow(). Useful for streaming voltage values
 568        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
 569        positive DCDC converter, negative DCDC converter, or output of the HVPS.
 570        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
 571        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
 572        a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a
 573        way to do this automatically"""
 574
 575        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
 576            which = VNOW_POS
 577        z = self._send_receive_4bytes(SERIAL_QVNOWRAW, param1=which, typ='uint')
 578        z_out = z & 0xFFFF
 579        z_main = (z & 0xFFFF0000) >> 16
 580        if DEBUG:
 581            y = "q_vnow_raw -> " + str(z_main) + " , " + str(z_out)
 582            print(y)
 583        return z_main, z_out
 584
 585    def q_vnow_fast(self, which=VNOW_POS):  # queries the voltage output
 586        """Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage
 587        This avoids running slow floating point calculations on the
 588        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
 589        execute than q_vnow(). Useful for streaming voltage values.   This method is similar to q_vnow(), except that
 590        the conversion from a raw value to a calibrated value is done
 591        on the host rather than on the microcontroller. It can take up to 300us to convert a value on the MUC
 592        (it depends on the method used (linear, quadratic, lookup table)
 593        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
 594        positive DCDC converter, negative DCDC converter, or output of the HVPS.
 595        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
 596        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
 597        a 12 bit value that can be converted to voltage using the unit's calibration values The returned values
 598        are calibrated voltages in Volt"""
 599
 600        z_main, z_out = self.q_vnow_raw(which=which)
 601        v_main = self._convert_raw_to_voltage(z_main, which=which)
 602        v_out = self._convert_raw_to_voltage(z_out, which=VNOW_OUT)
 603
 604        return v_main, v_out
 605
 606    def s_f(self, x):  # sets the frequency
 607        """Sets the frequency of the signal when the HVPS is in switching or waveform mode (SWMODE_SW / SWMODE_SW).\n
 608        The value returned is the new frequency, taking quantification into account.
 609        :param x: frequency in Hz between 0.001 and 1000. In waveform mode, 0.16 Hz <= f <=100 Hz
 610        :return: frequency accepted by hvps-x in Hz
 611        """
 612        x = constrain(x, 0.001, 1000.0)
 613
 614        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
 615        # Ex: 10.0: 0x41 0x20 0x00 0x00.
 616        # 0 10000010 01000000000000000000000
 617        # S=0 Positive
 618        # Exp=130-127 (offset)=2^3=8
 619        # Fraction=1+2^-2=1.25
 620        # number=+1.25*8=10
 621        f_byte = struct.pack('<f', x)
 622        z = self._send_nbytes_receive_4bytes(SERIAL_SF, f_byte, typ='float')
 623
 624        if DEBUG:
 625            y = "s_f(" + str(x) + ") -> " + str(z)
 626            print(y)
 627        return z
 628
 629    def q_f(self):  # queries the frequency
 630        """Queries the frequency. The returned value is in Hz."""
 631        z = self._send_receive_4bytes(SERIAL_QF, param1=0, param2=0, typ='float')
 632
 633        if DEBUG:
 634            y = "q_f -> " + str(z)
 635            print(y)
 636
 637        return z
 638
 639    def s_current(self, xnorm, xhigh):  # sets the current limit setting
 640        """Sets the current output in both current settings (normal and high). This value is used to calculate the PID
 641         coefficients for the waveform. Values can be obtained by measuring the charging rate when charging a capacitor
 642         of known value. This is a configuration parameter that must be set according to the capabilities of the hvps-x.
 643         It is not a user-settable output current.
 644        :param xnorm: current in A between 50E-6 and 500E-6 in normal output current config.
 645        :param xhigh: current in A between 50E-6 and 500E-6 in high output current config.
 646        :return: SERIAL_OK or SERIAL_ERROR
 647        """
 648        xnorm = constrain(xnorm, 50E-6, 500E-6)
 649        xhigh= constrain(xhigh, 50E-6, 500E-6)
 650        f_byte = struct.pack('<2f', xnorm, xhigh)
 651        z = self._send_nbytes_receive_4bytes(SERIAL_SCURR, f_byte, typ='uint')
 652
 653        if DEBUG:
 654            y = "s_current({},{}) -> {}".format(xnorm, xhigh, z)
 655            print(y)
 656        return z
 657
 658    def q_current(self):  # queries the current limit setting
 659        """Queries the current limit setting. The returned value is in A.
 660
 661        :return: A tuple of two current values corresponding to the output current settings in normal and high current
 662        modes."""
 663
 664        z = self._send_4bytes_receive_nbytes(SERIAL_QCURR, param1=0, param2=0, fstring='<2f')
 665
 666        if DEBUG:
 667            y = "q_curr() -> " + str(z)
 668            print(y)
 669        return z
 670
 671    def s_current_selection(self, x):
 672        """Sets the position of the current selection jumper
 673        :param x: Position of the output current selection jumper. Either J2_3V3 (normal) or J2_5V depending on the
 674        jumper position. 3V3 and 5V are written on the silkscreen and respectively correspond to normal and high current
 675        modes. After changing the position of the jumper, this command should be run to reflect the change. The new
 676        setting will be kept until the unit is power-cycled. To make the selection permanent, use the save() command
 677        after the s_current_selection(x) command.
 678        :return: Accepted current selection jumper position (J2_3V3 or J2_5V)
 679        """
 680        x = int(np.abs(x))
 681        if x > J2_5V:
 682            x = J2_5V
 683        z = self._send_receive_4bytes(SERIAL_SCURRSEL, param1=x, param2=0, typ='uint')
 684
 685        if DEBUG:
 686            print("s_current_selection({}) --> {}".format(x, z))
 687        return z
 688
 689    def q_current_selection(self):
 690        """Queries the position of the current selection jumper
 691        :return: Current selection (J2_3V3 or J2_5V)
 692        """
 693        z = self._send_receive_4bytes(SERIAL_QCURRSEL, param1=0, param2=0, typ='uint')
 694
 695        if DEBUG:
 696            print("q_current_selection() --> {}".format(z))
 697        return z
 698
 699    def s_duty(self, x):  # sets the duty cycle of the switching signal
 700        """Sets the duty cycle of the switching signal
 701        :param x: the duty cycle (float in the range 0-1)
 702        :return: the current duty cycle (float between 0 and 1)
 703        """
 704        duty = int(x * 1000)  # hvps-x is coding duty cycle on a 0-1000 scale
 705
 706        z = self._send_receive_4bytes(SERIAL_SDUTY, param1=0, param2=duty, typ='uint')
 707        z = z / 1000
 708        if DEBUG:
 709            y = "s_duty(" + str(x) + ") -> " + str(z)
 710            print(y)
 711        return z
 712
 713    def q_duty(self):  # queries the duty cycle of the switching signal
 714        """queries the duty cycle of the switching signal
 715        :return: the current duty cycle (float between 0 and 1)
 716        """
 717
 718        z = self._send_receive_4bytes(SERIAL_QDUTY, param1=0, param2=0, typ='uint')
 719        z = float(z) / 1000.0
 720        if DEBUG:
 721            y = "q_duty() -> " + str(z)
 722            print(y)
 723        return z
 724
 725    # ===Commands to change the voltage control and switching behaviour (mode and source)====
 726    def s_sw_mode(self, x):  # sets the switching mode
 727        """Sets the switching mode of the hvps-x. \n
 728        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
 729        This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV
 730        DC/DC converter.
 731        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. This connects J4 to ground and
 732        SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency and with
 733        the specified duty cycle.
 734        SWMODE_WFM: The optocouplers are controlled with a PWM signal to reproduce the user-defined waveform in memory
 735        The program tab "Waveform" can be used to set the parameters of the waveform.
 736        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
 737        save() command enables to save this parameter in memory
 738        :param x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW, SWMODE_WFM
 739        :return: Switching mode set by the HVPS
 740        """
 741
 742        if x > SWMODE_LOW:
 743            x = SWMODE_OFF
 744
 745        z = self._send_receive_4bytes(SERIAL_SSWMODE, param1=x, param2=0, typ='uint')
 746        if DEBUG:
 747            y = "s_sw_mode(" + str(x) + ") -> " + str(z)
 748            print(y)
 749        return z
 750
 751    def q_sw_mode(self):  # queries the switching mode
 752        """queries the switching mode of the hvps-x. \n
 753        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
 754        This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV
 755        DC/DC converter.
 756        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. This connects J4 to ground and
 757        SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency and with
 758        the specified duty cycle.
 759        SWMODE_WFM: The optocouplers are controlled with a PWM signal to reproduce the user-defined waveform in memory
 760        The program tab "Waveform" can be used to set the parameters of the waveform.
 761        :return: Switching mode set by the HVPS: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW, SWMODE_WFM
 762        """
 763        z = self._send_receive_4bytes(SERIAL_QSWMODE, param1=0, param2=0, typ='uint')
 764        if DEBUG:
 765            y = "q_sw_mode -> " + str(z)
 766            print(y)
 767        return z
 768
 769    def s_sw_src(self, x):  # sets the switching source
 770        """Sets the source of the switching signal.
 771
 772        Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard
 773        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
 774        Using the save() command enables to save this parameter in memory
 775        :param x: SWSRC_TMR, or SWSRC_BTTN
 776        :return: SWSRC_TMR, or SWSRC_BTTN
 777        """
 778        if x > SWSRC_BTTN:
 779            x = SWSRC_TMR
 780        z = self._send_receive_4bytes(SERIAL_SSWSRC, param1=x, param2=0, typ='uint')
 781        if DEBUG:
 782            y = "s_sw_src(" + str(x) + ") -> " + str(z)
 783            print(y)
 784        return z
 785
 786    def q_sw_src(self):  # queries the switching source
 787        """queries the source of the switching signal.
 788
 789        queries the source of the switching signal. Output values are: SWSRC_TMR for onboard
 790        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
 791        Using the save() command enables to save this parameter in memory
 792        :return: SWSRC_TMR, or SWSRC_BTTN
 793        """
 794
 795        z = self._send_receive_4bytes(SERIAL_QSWSRC, param1=0, param2=0, typ='uint')
 796
 797        if DEBUG:
 798            y = "q_sw_src -> " + str(z)
 799            print(y)
 800        return z
 801
 802    def s_bttn_cfg(self, x):  # sets the configuration of the push button
 803        """Defines the behaviour of the push button
 804
 805        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
 806        save() command enables to save this parameter in memory
 807        :param x: 2-bit value (0x0 to 0x3). bit 0 - Defines the behaviour of the push button, when the switching source
 808        of the HVPS is set to the push button
 809        (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour
 810        (i.e. the high voltage is turned on as long as the button is pressed),
 811        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
 812        to turn it off).\n
 813        bit 1 - State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
 814        :return: the 2-bit button config value
 815        """
 816        if x > 3:
 817            x = 3
 818        z = self._send_receive_4bytes(SERIAL_SBTTNCFG, param1=x, param2=0, typ='uint')
 819        if DEBUG:
 820            y = "s_bttn_cfg(" + str(x) + ") -> " + str(z)
 821            print(y)
 822
 823        return z
 824
 825    def q_bttn_cfg(self):  # queries the latch mode of the push button
 826        """Queries the behaviour of the push button
 827
 828        :return: the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching
 829        source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above).
 830        Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed),
 831        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
 832        to turn it off).\n
 833        bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
 834        """
 835        z = self._send_receive_4bytes(SERIAL_QBTTNCFG, param1=0, param2=0, typ='uint')
 836        if DEBUG:
 837            y = "q_bttn_cfg -> " + str(z)
 838            print(y)
 839
 840        return z
 841
 842    def q_kill(self):  # queries the state of the kill button. Kill=1 means HV is disabled
 843        """Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at
 844        the hvps-x output, irrespective of any software setting.
 845        :return: 1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise.
 846        """
 847        z = self._send_receive_4bytes(SERIAL_QKILL, param1=0, param2=0, typ='uint')
 848
 849        if DEBUG:
 850            y = "q_kill -> " + str(z)
 851            print(y)
 852        return z
 853
 854    def s_v_mode(self, x):  # sets the voltage control mode
 855        """Sets the voltage control mode
 856
 857        Sets the voltage control mode (i.e. how is the value of the output voltage controlled):\n
 858        VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).\n
 859        VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).\n
 860        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
 861        save() command enables to save this parameter in memory\n
 862        VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However,
 863        the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output
 864        voltage remains within the allowed range
 865        :param x: VMODE_R, VMODE_O
 866        :return: VMODE_R, VMODE_O
 867        """
 868
 869        if x > VMODE_O:
 870            x = VMODE_R
 871
 872        z = self._send_receive_4bytes(SERIAL_SVMODE, param1=x, param2=0, typ='uint')
 873        if DEBUG:
 874            y = "s_v_mode(" + str(x) + ") -> " + str(z)
 875            print(y)
 876        return z
 877
 878    def q_v_mode(self):  # queries the switching source
 879        """Queries the voltage control mode
 880
 881        :return: VMODE_R internal voltage regulator, VMODE_O internal
 882        open loop control (on-board regulator disconnected).
 883        """
 884
 885        z = self._send_receive_4bytes(SERIAL_QVMODE, param1=0, param2=0, typ='uint')
 886
 887        if DEBUG:
 888            y = "q_v_mode -> " + str(z)
 889            print(y)
 890        return z
 891
 892    # ====Functions to set configuration parameters====
 893    def s_pid(self, x, pid=PID_KPP):
 894        """Sets the gains of the PIDs
 895        Use save() command to save the new values to memory
 896        :param x: the value (float) of the gain.
 897        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,O},
 898        for positive voltage PID, output voltage PID (used for the waveform mode).
 899        :return: the gain value
 900        """
 901        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
 902        # Ex: 10.0: 0x41 0x20 0x00 0x00.
 903        # 0 10000010 01000000000000000000000
 904        # S=0 Positive
 905        # Exp=130-127 (offset)=2^3=8
 906        # Fraction=1+2^-2=1.25
 907        # number=+1.25*8=10
 908        pid_byte = struct.pack('<Bf', pid, x)
 909        z = self._send_nbytes_receive_4bytes(SERIAL_SPID, pid_byte, typ='float')
 910
 911        if DEBUG:
 912            y = "s_pid(" + str(x) + "," + str(pid) + ") -> " + str(z)
 913            print(y)
 914        return z
 915
 916    def q_pid(self, pid=PID_KPP):  # queries the frequency
 917        """returns the gains of the PIDs
 918
 919        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,O},
 920        for positive voltage PID, output voltage PID (used for the waveform mode).
 921        :return: the gain value of the chosen parameter
 922        """
 923
 924        z = self._send_receive_4bytes(SERIAL_QPID, param1=pid, param2=0, typ='float')
 925
 926        if DEBUG:
 927            y = "q_pid(" + str(pid) + ") -> " + str(z)
 928            print(y)
 929
 930        return z
 931
 932    def s_cal(self, x, cal=CAL_C1P):
 933        """Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values)
 934        Use save() to commit the setting to memory
 935        :param x:  the value of the calibration coefficient (float)
 936        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,O}, for positive
 937        voltage measurement, output voltage measurement
 938        :return: the calibration value
 939        """
 940
 941        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
 942        # Ex: 10.0: 0x41 0x20 0x00 0x00.
 943        # 0 10000010 01000000000000000000000
 944        # S=0 Positive
 945        # Exp=130-127 (offset)=2^3=8
 946        # Fraction=1+2^-2=1.25
 947        # number=+1.25*8=10
 948        cal_byte = struct.pack('<Bf', cal, x)
 949        z = self._send_nbytes_receive_4bytes(SERIAL_SCAL, cal_byte, typ='float')
 950
 951        if DEBUG:
 952            y = "s_cal(" + str(x) + "," + str(cal) + ") -> " + str(z)
 953            print(y)
 954        return z
 955
 956    def q_cal(self, cal=CAL_C1P):  # queries the frequency
 957        """queries the calibration constants of the analogue input signals (conversion to calibrated voltage values)
 958
 959        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,O}, for positive
 960        voltage measurement, output voltage measurement
 961        :return: the calibration value
 962        """
 963
 964        z = self._send_receive_4bytes(SERIAL_QCAL, param1=cal, param2=0, typ='float')
 965
 966        if DEBUG:
 967            y = "q_cal(" + str(cal) + ") -> " + str(z)
 968            print(y)
 969        return z
 970
 971    def s_calibration_method(self, which=VNOW_POS, calmeth=CALMETH_POLYNOMIAL):
 972        if not (calmeth == CALMETH_POLYNOMIAL or calmeth == CALMETH_LOOKUP):
 973            calmeth = CALMETH_POLYNOMIAL
 974        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
 975            which = VNOW_POS
 976        z = self._send_receive_4bytes(SERIAL_SCALMETH, param1=which, param2=calmeth, typ='uint')
 977
 978        if DEBUG:
 979            y = "s_calibration_method({0}, {1}) -> {2}".format(which, calmeth, z)
 980            print(y)
 981        if which == VNOW_POS:
 982            self.calmeth_p = z
 983        elif which == VNOW_OUT:
 984            self.calmeth_o = z
 985        else:
 986            self.calmeth_n = z
 987
 988    def q_calibration_method(self, which=VNOW_POS):
 989        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
 990            which = VNOW_POS
 991        z = self._send_receive_4bytes(SERIAL_QCALMETH, param1=which, param2=0, typ='uint')
 992
 993        if DEBUG:
 994            y = "q_calibration_method({0}) -> {1}".format(which, z)
 995            print(y)
 996        if which == VNOW_POS:
 997            self.calmeth_p = z
 998        elif which == VNOW_NEG:
 999            self.calmeth_n = z
1000        else:
1001            self.calmeth_o = z
1002
1003    def s_lookup(self, x, n, table=LOOKUP_ADC_P):
1004        if n > 20:
1005            n = 20
1006        n = n << 2  # shift left 2 bits
1007        if table == LOOKUP_ADC_P:
1008            n = n | 0b01
1009        elif table == LOOKUP_ADC_O:
1010            n = n | 0b10
1011        x = int(x)
1012        z = self._send_receive_4bytes(SERIAL_SLKUP, param1=n, param2=x, typ='int16')
1013        if DEBUG:
1014            y = "s_lookup({0},{1},{2}) - > {3}".format(x, n, table, z)
1015            print(y)
1016
1017    def q_lookup(self, which):  # which=0: vout, which=1: ADC_p, which=2: ADC_o
1018        format_string = f'<21h'
1019        z = self._send_4bytes_receive_nbytes(SERIAL_QLKUP, param1=which, param2=0, fstring=format_string)
1020        lookup_list = list(z)
1021        if DEBUG:
1022            y = "q_lookup({0}) - > {1}".format(which, lookup_list)
1023            print(y)
1024        return lookup_list
1025
1026    def s_saturation_limit(self, limit):    # limit: 0-> reset to default 2% above Vmax. Otherwise:1-4095
1027        if limit < 0:
1028            limit = 0
1029        elif limit > 4095:
1030            limit = 4095
1031        z = self._send_receive_4bytes(SERIAL_SLKUP, param1=0, param2=limit, typ='int16')
1032        if DEBUG:
1033            y = "s_saturation_limit({0}) - > {1}".format(limit, z)
1034            print(y)
1035        return z
1036
1037    # ====User Waveform Functions====
1038    def s_wfm_pts(self, n=1, m=500, pts=None):
1039        """Sets the data points of the waveform
1040        Use save() to commit the setting to memory
1041        :param n:  The loop counter. The frequency of the signal is f = 1 / (n * m * 0.5E-3) 0 < n < 256. If you don't
1042        want to bother with n, set n = 1, and use s_f() after transferring the points to get the desired wfm frequency.
1043        :param m: The number of points in the waveform 0 <m <= 500
1044        :param pts: a list of integer voltage points. The list must be 500 elements long. If m<500, then the end of the list
1045        must be padded with zeros.
1046        :return: SERIAL_OK or SERIAL_ERROR
1047        """
1048        # parameters are 1) n (byte B) the counter divider, 2) the number of points m, an unsigned short, and 3) m unsigned short pts
1049        if pts is None:
1050            z = SERIAL_ERROR
1051            if DEBUG:
1052                print("s_wfm_pts(): ERROR: no points given")
1053        elif len(pts) == 500 and all(isinstance(item, int) for item in pts):
1054            n=int(n)
1055            if n > 255:
1056                n=255
1057            m=int(m)
1058            format_string = f'<B{WFM_MAX_PTS+1}H'
1059            buffer = struct.pack(format_string, n, m, *pts) # array pts always has WFM_MAX_PTS points, but with zeros as
1060            # padding at the end. m indicated the effective number of data points. This is to always have the same command
1061            # length
1062            z = self._send_nbytes_receive_4bytes(SERIAL_SWFMPTS, buffer)
1063            if DEBUG:
1064                y = "s_wfm_pts({0}, {1}, {2}) - > {3}".format(n, m, buffer, z)
1065                print(y)
1066            else:
1067                z = SERIAL_ERROR
1068                if DEBUG:
1069                    print("s_wfm_pts(): ERROR: number of points not equal to 500, or not all integer values")
1070        return z
1071
1072    def q_wfm_pts(self):
1073        """queries the data points of the waveform
1074        :return : n, z with n: the number of points in the waveform and z: the list of waveform points in Volts
1075        """
1076        format_string = f'<H500H' # 1 16-bit unsigned value for the number of points and 500 unsigned 16-bit values. Not all points are effective points (some may be padding zeroes) hence why n is required.
1077        z = self._send_4bytes_receive_nbytes(SERIAL_QWFMPTS, param1=0, param2=0, fstring=format_string)
1078        z = list(z)
1079        n = z.pop(0)    # the first element is the number of points
1080        del z[n:]       #remove all elements of the list after the number of points to keep
1081        if DEBUG:
1082            y = "q_wfm_pts() - > {}/{}".format(n, z)
1083            print(y)
1084        return n, z
1085
1086
1087    # Miscellaneous functions
1088    def save(self):  # save current HVPS parameters into the memory
1089        """save active HVPS parameters into the memory
1090
1091        This command saves the active parameters as \'current\' settings. Current setting are the settings that are
1092        loaded when power is applied to the hvps-x
1093
1094        :return: SERIAL_OK or SERIAL_ERROR
1095        """
1096
1097        z = self._send_receive_4bytes(SERIAL_SAVE)
1098
1099        if DEBUG:
1100            y = "save -> " + str(z)
1101            print(y)
1102        return z
1103
1104    def save_memory_to_file(self, settings=SETTINGS_CURRENT):
1105        """Dumps the content of the memory into a JSON file.
1106
1107        Dumps the content of the memory into a file. This is useful to keep a backup of the parameters on file.
1108        Files will be created in the interface folder and have the following format: Name_settings_type_date_time.json\n
1109        json files with settings can be transferred back to the hvps-x with the transfer_settings() method of the HVPS
1110        class, or the higher-level function transfer_file_to_memory()
1111        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
1112        :return: the name of the created file (it follows a standard naming scheme with the date, the name of the
1113        hvps-x and the setting type.
1114        """
1115
1116        memory = self.q_mem(settings)
1117        now = datetime.now()  # current date and time
1118        date_time = now.strftime("%Y_%m_%d")
1119        if settings == SETTINGS_CURRENT:
1120            st_string = '_cur_st_'
1121        else:
1122            st_string = '_bk_st_'
1123        file_name = self.name.decode("utf-8") + st_string + date_time + '.json'
1124        with open(file_name, "w") as write_file:
1125            json.dump(memory, write_file, indent=4)
1126
1127        return file_name
1128
1129    def s_settings(self,
1130                   settings=SETTINGS_CURRENT):  # sets the active setting to a particular type (useful before saving)
1131        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_FACTORY or settings == SETTINGS_BACKUP):
1132            settings = SETTINGS_CURRENT
1133        x = self._send_receive_4bytes(SERIAL_SST, param1=settings)
1134        if DEBUG:
1135            print('s_settings({0}) -> {1}'.format(settings, x))
1136        return x
1137
1138    def load_settings(self, settings=SETTINGS_CURRENT):  # Load a setting set from memory to the active settings
1139        """This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x
1140
1141        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_BACKUP
1142        :return:
1143        """
1144
1145        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_BACKUP):
1146            settings = SETTINGS_CURRENT
1147        x = self._send_receive_4bytes(SERIAL_LST, param1=settings)
1148        if DEBUG:
1149            print('load_settings({0}) -> {1}'.format(settings, x))
1150
1151        self._initialise_hvpsx()  # need to reread all parameters
1152
1153        return x
1154
1155    def transfer_settings(self, dict_settings):
1156        """ transfer a setting dictionary from the computer to the hvps-x memory
1157        :param dict_settings: a dictionary containing the settings values.
1158        :return: SERIAL_OK or SERIAL_ERROR
1159        The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together
1160        these two functions make it possible to backup the settings (this includes calibration and PID settings in a
1161        file, and gives the opportunity to restore the settings"""
1162
1163        error = False
1164        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1165        siz = struct.calcsize(memory_string[fw_string])
1166        buffer = ctypes.create_string_buffer(siz)
1167        memmap = memorymap[fw_string]
1168        counter = 0
1169        for x in memmap:
1170            n = x[0]
1171            key = x[1]
1172            data_format = x[2]
1173            if data_format == 'B' or data_format == 's':
1174                bytelength = 1
1175            elif data_format == 'h' or data_format == 'H':
1176                bytelength = 2
1177            elif data_format == 'f':
1178                bytelength = 4
1179            else:
1180                bytelength = 1
1181            data = dict_settings[key]
1182            if key == 'Name':
1183                data = bytes(data, 'utf-8')
1184                if len(data) > 12:
1185                    error = True
1186                    if DEBUG:
1187                        print("Error: name should not exceed 12 characters")
1188                else:
1189                    format_str = '<{0}{1}'.format(n, data_format)
1190                    struct.pack_into(format_str, buffer, counter, data)
1191            else:
1192                format_str = '<{0}'.format(data_format)
1193                if n == 1:
1194                    struct.pack_into(format_str, buffer, counter, data)
1195                else:
1196                    for i in range(n):
1197                        try:
1198                            struct.pack_into(format_str, buffer, counter + i * bytelength, data[i])
1199                        except IndexError:
1200                            error = True
1201                            if DEBUG:
1202                                print('setting dictionary does not fit the expected format')
1203
1204            counter = counter + n * bytelength
1205        data_fw = 'Fw-{0}.{1}'.format(dict_settings['fw_major'], dict_settings['fw_minor'])
1206        if data_fw != fw_string:
1207            error = True
1208            if DEBUG:
1209                print('Error: JSON file firmware version does not match firmware currently on hvps-x. Exiting')
1210            exit(0)
1211        if not error:
1212            buffer = b'xxx' + buffer    # Adds 3 random bytes. CMD + 3 random bytes means that when mapping the transfer
1213            # buffer to a structure, it will on an aligned memory address
1214            x = self._send_nbytes_receive_4bytes(SERIAL_XFERST, buffer)
1215        else:
1216            x = SERIAL_ERROR
1217
1218        if DEBUG:
1219            print("transfer_settings(...) -> {0}".format(x))
1220        return x
1221
1222    def copy_settings(self, src, dst):
1223        """Copies one set of settings to another location:\n
1224        Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings
1225        (for example if some temporary settings were saved as current settings, for example to experiment with new PID
1226        gain values)\
1227        Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new
1228        calibration as a set of back-up settings
1229        :param src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS)
1230        :param dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by
1231        the source
1232        :return: SERIAL_OK or SERIAL_ERROR
1233        """
1234        if (src == SETTINGS_CURRENT or src == SETTINGS_BACKUP) and (dst == SETTINGS_CURRENT or dst == SETTINGS_BACKUP):
1235            x = self._send_receive_4bytes(SERIAL_CPYST, param1=src, param2=dst)
1236        else:
1237            x = SERIAL_ERROR
1238
1239        if DEBUG:
1240            print('copy_settings({0},{1}) -> {2}'.format(src, dst, x))
1241
1242        return x
1243
1244    def q_mem(self, settings=SETTINGS_CURRENT):  # queries the content of the memory
1245        """
1246        :param settings: which of the two sets of setting sets to query. Either SETTINGS_CURRENT or SETTINGS_Backup
1247        :return: A dictionary with the content of the memory. This is similar to the function save_memory_to_file()
1248        except that it doesn't save the content to a file"""
1249        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1250        memmap = memorymap[fw_string]
1251        dict_mem = {}
1252
1253        z = self._send_4bytes_receive_nbytes(SERIAL_QMEM, settings, param2=0, fstring=memory_string[fw_string])
1254        bytecount = 0
1255        for i in range(len(memmap)):
1256            length = memmap[i][0]
1257            field = memmap[i][1]
1258            if field == 'Name':
1259                length = 1
1260            y = z[bytecount:bytecount + length]
1261            # if field != 'Padding':
1262            if length == 1:
1263                dict_mem[field] = y[0]
1264            else:
1265                dict_mem[field] = y
1266            bytecount = bytecount + length
1267
1268        dict_mem['Name'] = dict_mem['Name'].decode('UTF-8')
1269        dict_mem['Name'] = dict_mem['Name'].split("\0")[0]
1270
1271        if DEBUG:
1272            print(dict_mem)
1273        return dict_mem
1274
1275    def q_ver(self):  # queries the firmware version
1276        """returns the current version of the firmware / hardware running on the board."""
1277        z = self._send_receive_4bytes(SERIAL_QVER)
1278        Firm_minor = z & 0xFF
1279        Firm_major = (z >> 8) & 0xFF
1280        Hard_minor = (z >> 16) & 0xFF
1281        Hard_major = (z >> 24)
1282        self.firmware = Firm_major + Firm_minor / 10
1283        self.hardware = Hard_major + Hard_minor / 10
1284        if DEBUG:
1285            y = "q_ver -> {0} / {1}".format(self.firmware, self.hardware)
1286            print(y)
1287        return self.firmware, self.hardware
1288
1289    # Configuration functions
1290    def q_conf(self):  # queries the configuration
1291        """returns the configuration of the board.
1292        :return: the configuration of the board in the high 2 bytes (CONF_UNCONF, CONF_UNIPOLAR, or CONF_BIPOLAR)
1293        and the PID (product ID) in the low 2 bytes. This is 0x0632 for a hvps-x unit.
1294        """
1295
1296        z = self._send_receive_4bytes(SERIAL_QCONF)
1297        z_hvps = z & 0xFFFF
1298        z_conf = (z & 0xFFFF0000) >> 16
1299        if DEBUG:
1300            y = "q_conf -> " + hex(z_conf) + " / " + hex(z_hvps)
1301            print(y)
1302        self.conf = z_conf
1303        return z
1304
1305    def s_conf(self, bipolar):
1306        """Sets the configuration of the board
1307        :param bipolar: boolean. Is board bipolar (True), or unipolar (false)
1308        :return: bipolar: boolean
1309        """
1310        if bipolar:
1311            param = CONF_BIPOLAR
1312        else:
1313            param = CONF_UNIPOLAR
1314        z = self._send_receive_4bytes(SERIAL_SCONF, param1=param)
1315        if DEBUG:
1316            y = "s_conf -> " + hex(z)
1317            print(y)
1318            if z == SERIAL_ERROR:
1319                print("Error: this configuration is not recognised")
1320        self.conf = z
1321        return z
1322
1323    def s_vmax(self, x):  # sets the maximum voltage rating of the board
1324        """ sets the voltage rating of the hvps-x. Must match the EMCO DC/DC converter rating.
1325        :param x: Voltage rating of hvps-x in Volt
1326        :return: Voltage rating of hvps-x in Volt
1327        """
1328
1329        x = constrain(x, 0, 6000)
1330        z = self._send_receive_4bytes(SERIAL_SVMAX, param2=x)
1331        if DEBUG:
1332            y = "s_vmax(" + str(x) + ") -> " + str(z)
1333            print(y)
1334        self.vmax = z
1335        return z
1336
1337    def q_vmax(self):  # queries the voltage rating of the board
1338        """ Queries the maximal voltage of the board. The returned value is in volts.
1339        :return: board maximal voltage (V)
1340        """
1341        z = self._send_receive_4bytes(SERIAL_QVMAX)
1342        if DEBUG:
1343            y = "q_vmax -> " + str(z)
1344            print(y)
1345        self.vmax = z
1346        return z
1347
1348    def s_name(self, x):  # set the name of the HVPS
1349        """ Sets the name of the HVPS.
1350        :param x: Name of the hvps-x. 11 characters maximum
1351        :return: name accepted by hvps-x
1352        """
1353        ll = len(x)
1354        if ll <= 12:
1355            x = bytearray(x, 'utf-8')
1356            for i in range(12 - ll):  # pad the string with 0s
1357                x = x + b'\0'
1358            self._send_nbytes_receive_4bytes(SERIAL_SNAME, x, typ='uint')
1359            z = self.q_name()
1360        else:
1361            z = 'too long'
1362        if DEBUG:
1363            y = "s_name(" + str(x) + ") -> " + str(z)
1364            print(y)
1365        return z
1366
1367    def q_name(self):  # queries the name of the board
1368        """queries the name of the board
1369        :return: Name of the board
1370        """
1371
1372        # x = self._send_receive_4bytes(SERIAL_QNAME, param1=0)
1373        # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=4) << 32)
1374        # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=8) << 64)
1375        # x = x.to_bytes(12, 'little')
1376
1377        z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s')
1378        z = z[0].split(b'\x00', 1)[0]   # take first (only) element of the tuple and remove the 0s at the end of string
1379
1380        self.name = z
1381        if DEBUG:
1382            y = "q_name -> " + str(z)
1383            print(y)
1384        return z
1385
1386    def set_hardware_version(self, hw_major, hw_minor):
1387        param = (hw_major << 8) + hw_minor
1388        self._send_receive_4bytes(SERIAL_SHW, param2=param)
1389
1390    def q_load_parameters(self):
1391        z = self._send_4bytes_receive_nbytes(SERIAL_QLOAD, param1=0, param2=0, fstring='<4H')
1392        if np.isin(0, np.array(z)):
1393            r = 0
1394            c = 0
1395        else:
1396            dv = self._convert_raw_to_voltage(z[0], VNOW_OUT)
1397            pwm_dv = z[1]
1398            ssv = self._convert_raw_to_voltage(z[2], VNOW_OUT)
1399            pwm_ss = z[3]
1400            current_selection = self.q_current_selection()
1401            imax = self.q_current()[current_selection]
1402            i_step = imax * pwm_dv / 4095
1403            i_ss = imax * pwm_ss / 4095
1404            dv_dt = dv / 5E-3  # dv is measured over 20ms
1405            c = i_step / dv_dt * 1E9 # C in nF
1406            r = ssv / i_ss / 1E6   # R in MOhm
1407        if DEBUG:
1408            print('q_load_parameters() --> {} nF, {} MOhm'.format(c, r))
1409        return r, c
1410
1411    def check_version_compatibility(self):
1412        self.q_ver()
1413        lib_string = 'Lib-' + LIB_VER
1414        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1415        hw_string = 'Hw-{0:.1f}'.format(self.hardware)
1416
1417        list_compatible_fw = compatibility_dict[lib_string]
1418        if fw_string in list_compatible_fw:
1419            list_compatible_hw = compatibility_dict[fw_string]
1420            if hw_string not in list_compatible_hw:
1421                self.err |= ERR_FIRM
1422        else:
1423            self.err |= ERR_FIRM
1424
1425    def diagnostic_ADC(self, which_vnow=VNOW_POS):
1426        x = self._send_4bytes_receive_nbytes(SERIAL_DADC, which_vnow, 0, '<32H')
1427        return x
1428
1429    def q_trace(self, which_vnow=VNOW_POS):
1430        x = self._send_4bytes_receive_nbytes(SERIAL_QTRACE, which_vnow, 0, '<100H')
1431        x = np.array(x)
1432        if DEBUG:
1433            y = "trace({0}) -> {1}".format(which_vnow, x)
1434            print(y)
1435        return x
1436
1437    def s_trace_rate(self, rate=5):
1438        """Sets the rate at which trace points are stored in memory
1439        :param rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is
1440        calculating the voltages; currently evey 5ms.
1441        :return: the rate accepted by the hvps-x"""
1442
1443        rate = int(rate)
1444        if rate < 2:
1445            rate = 2
1446        x = self._send_receive_4bytes(SERIAL_STRRATE, param1=0, param2=rate, typ='uint')
1447        x = x / 10      # the function returns the number of ticks of the counter, which counts at 10kHz.
1448        if DEBUG:
1449            y = "s_trace_rate({0}) -> {1}".format(rate, x)
1450            print(y)
1451        return x
1452
1453    def q_trace_rate(self):
1454        """queries the rate at which trace points are stored in memory
1455        :param
1456        :return the rate at which trace points are stored in memory (in ms)"""
1457
1458        x = self._send_receive_4bytes(SERIAL_QTRRATE, param1=0, param2=0, typ='uint')
1459        x = x / 10  # the function returns the number of ticks of the counter, which counts at 10kHz.
1460        if DEBUG:
1461            y = "q_trace_rate() -> {0}".format(x)
1462            print(y)
1463        return x
1464
1465    def s_waveform_pwm(self, value):
1466
1467        value = constrain(value, -4095, 4095)
1468        value = int(value)
1469        x = self._send_receive_4bytes(SERIAL_WFMPWM, param1=0, param2=value, typ='uint')
1470        if DEBUG:
1471            y = "s_waveform_pwm({0}) -> {1}".format(value, x)
1472            print(y)
1473        return x

Class to control a petapicovoltron hvps-x

The class implements the low-level functions to interface the hvps-x firmware for easy communication with the hvps-x.

HVPS(port, init=True)
252    def __init__(self, port, init=True):
253        """ Initialises a HVPS object: dev = HVPS('/path/to/serial/port').
254
255        :param port: COM port to which the HVPS is connected.
256        :param init: if True (default) initialises the hvps-x. Setting to false can be useful if the intended use is
257        to check whether the device connected to the com port is a hvps-x"""
258        self.name = ''
259        self.vmax = 0
260        self.err = 0
261        self.ser = serial.Serial()  # the serial connection to the HVPS
262        self.lookup_adc_p = []  # lookup tables for linear interpolation
263        self.lookup_adc_o = []
264        self.lookup_v_out = []
265        self.cp = []  # calibration coefficients.
266        self.co = []
267        self.calmeth_p = -1
268        self.calmeth_n = -1
269        self.calmeth_o = -1
270
271        self.firmware = 0
272        self.hardware = 0
273        self.conf = 0  # configuration of the hvps-x
274        self.is_hvpsx = 0
275
276        try:
277            self.ser = serial.Serial(port, 115200, timeout=20)
278        except serial.SerialException:
279            self.err = self.err | ERR_PORT
280            if DEBUG:
281                print("Cannot connect to serial port. Probably busy.")
282        else:
283            self.ser.reset_input_buffer()
284            if DEBUG:
285                text = "connecting to " + port
286                print(text)
287                print("Serial port open")
288            z = self._send_receive_4bytes(SERIAL_QCONF)
289            if z == 0:  # no response: the device connected doesn't talk using this protocol
290                self.err = self.err | ERR_COM
291                if DEBUG:
292                    print("Communication error: the connected device doesn't reply to commands")
293            z_hvps = z & 0xFFFF
294            z_conf = (z & 0xFFFF0000) >> 16
295            if z_hvps == HVPSX_PID:
296                self.is_hvpsx = 1
297                if z_conf != CONF_UNCONF:
298                    self.conf = z_conf
299                else:
300                    self.err = self.err | ERR_CONF  # the hvps-x is not configured
301                    if DEBUG:
302                        print("The hvps is not configured")
303                z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s')
304                z = z[0].split(b'\x00', 1)[0]
305                self.name = z
306            else:
307                self.err = self.err | ERR_TYPE  # The connected device replied with Err! but is not a hvps-x
308                if DEBUG:
309                    print("Type error: the device replies to commands, but is not a hvps-x.")
310            if init and not self.err:
311                self.check_version_compatibility()
312                self.q_vmax()

Initialises a HVPS object: dev = HVPS('/path/to/serial/port').

Parameters
  • port: COM port to which the HVPS is connected.
  • init: if True (default) initialises the hvps-x. Setting to false can be useful if the intended use is to check whether the device connected to the com port is a hvps-x
name
vmax
err
ser
lookup_adc_p
lookup_adc_o
lookup_v_out
cp
co
calmeth_p
calmeth_n
calmeth_o
firmware
hardware
conf
is_hvpsx
def close(self, zero=True):
314    def close(self, zero=True):  # closes connection with the HVPS
315        """Closes the connection to the HVPS. Sets voltage to 0 if board was connected
316
317        Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the
318        communication. If False, the hvps-x is left in its current state"""
319
320        if self.ser.is_open:
321            if not (self.err & ERR_COM):  # if connected board is not running the firmware, do not send command
322                if zero:
323                    self.s_sw_mode(SWMODE_OFF)  # for hvps-x, this will disable both optocouplers.
324                    self.s_vset(0, DCDC_BOTH)  # set the voltage to 0 as a safety measure
325            self.ser.close()
326
327        if DEBUG:
328            print("Serial port closed")

Closes the connection to the HVPS. Sets voltage to 0 if board was connected

Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the communication. If False, the hvps-x is left in its current state

def is_bipolar(self):
456    def is_bipolar(self):  # return true if connected device is hvpsx and bipolar unit
457        """ Returns true if the hvps-x configuration is bipolar."""
458        if self.conf & 1:
459            return True
460        else:
461            return False

Returns true if the hvps-x configuration is bipolar.

def s_vset(self, x, polarity=1):
464    def s_vset(self, x, polarity=DCDC_POS):  # sets the output voltage
465        """Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or
466        when the HVPS is powered off. Using the
467        save() command enables to save this parameter in memory\n
468        Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is
469        regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the
470        output to the set point; see s_vmode() command.
471        :param x: voltage set point in volt (int)
472        :param polarity: which DC/DC converter to address (DCDC_POS (default), DCDC_NEG (bipolar config), DCDC_BOTH).
473        This parameter can be ignored given that only unipolar configuration is currently supported.
474        :return: voltage set point accepted by hvps-x
475        """
476        x = constrain(abs(x), 0, self.vmax)
477        x = int(x)
478        if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH:
479            polarity = DCDC_POS
480        z = self._send_receive_4bytes(SERIAL_SVSET, param1=polarity, param2=x)
481        if x == 0:
482            z = 0   # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in
483            # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a
484            # few 10s of volts when back calculated in volts
485        if DEBUG:
486            y = "s_vset(" + str(x) + ") -> " + str(z)
487            print(y)
488        return z

Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the save() command enables to save this parameter in memory

Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the output to the set point; see s_vmode() command.

Parameters
  • x: voltage set point in volt (int)
  • polarity: which DC/DC converter to address (DCDC_POS (default), DCDC_NEG (bipolar config), DCDC_BOTH). This parameter can be ignored given that only unipolar configuration is currently supported.
Returns

voltage set point accepted by hvps-x

def q_vset(self, polarity=1):
490    def q_vset(self, polarity=DCDC_POS):  # queries the voltage setpoint
491        """Queries the voltage set point. The returned value is in volts.
492        :param polarity: DCDC converter to be queried (DCDC_POS (default) or DCDC_NEG). DCDC_NEG will return 0 for
493        unipolar configs. This parameter can be ignored given that only unipolar configuration is currently supported.
494        :return: Voltage set point in volts
495        """
496        if polarity != DCDC_POS and polarity != DCDC_NEG:
497            polarity = DCDC_POS
498        z = self._send_receive_4bytes(SERIAL_QVSET, param1=polarity)
499        if DEBUG:
500            y = "q_vset -> " + str(z)
501            print(y)
502
503        return z

Queries the voltage set point. The returned value is in volts.

Parameters
  • polarity: DCDC converter to be queried (DCDC_POS (default) or DCDC_NEG). DCDC_NEG will return 0 for unipolar configs. This parameter can be ignored given that only unipolar configuration is currently supported.
Returns

Voltage set point in volts

def s_vset_raw(self, x, polarity=1):
505    def s_vset_raw(self, x, polarity=DCDC_POS):  # sets the output voltage
506        """Sets the raw output voltage of the HVPS (0-4095). The new parameter remains valid until a new call to this
507        command, or when the HVPS is powered off. Using the
508        save() command enables to save this parameter in memory\n
509        Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is
510        regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the
511        output to the set point; see s_vmode() command.
512        :param x: raw voltage set point (0-4095)
513        :param polarity: which DC/DC converter to address (DCDC_POS (default), DCDC_NEG (bipolar config), DCDC_BOTH).
514        This parameter can be ignored given that only unipolar configuration is currently supported.
515        :return: voltage set point accepted by hvps-x
516        """
517        x = constrain(x, 0, 4095)
518        x = int(x)
519        if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH:
520            polarity = DCDC_POS
521        z = self._send_receive_4bytes(SERIAL_SVSETRAW, param1=polarity, param2=x)
522        if x == 0:
523            z = 0   # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in
524            # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a
525            # few 10s of volts when back calculated in volts
526        if DEBUG:
527            y = "s_vset_raw(" + str(x) + ") -> " + str(z)
528            print(y)
529        return z

Sets the raw output voltage of the HVPS (0-4095). The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the save() command enables to save this parameter in memory

Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the output to the set point; see s_vmode() command.

Parameters
  • x: raw voltage set point (0-4095)
  • polarity: which DC/DC converter to address (DCDC_POS (default), DCDC_NEG (bipolar config), DCDC_BOTH). This parameter can be ignored given that only unipolar configuration is currently supported.
Returns

voltage set point accepted by hvps-x

def q_vset_raw(self, polarity=1):
531    def q_vset_raw(self, polarity=DCDC_POS):  # queries the voltage setpoint
532        """Queries the raw voltage set point. The returned value is between 0 and 4095.
533
534        :param polarity: DCDC converter to be queried (DCDC_POS (default) or DCDC_NEG). DCDC_NEG will return 0 for
535        unipolar configs. This parameter can be ignored given that only unipolar configuration is currently supported.
536        :return: Voltage raw set point (0-4095))
537        """
538        if polarity != DCDC_POS and polarity != DCDC_NEG:
539            polarity = DCDC_POS
540        z = self._send_receive_4bytes(SERIAL_QVSETRAW, param1=polarity)
541        if DEBUG:
542            y = "q_vset_raw -> " + str(z)
543            print(y)
544        return z

Queries the raw voltage set point. The returned value is between 0 and 4095.

Parameters
  • polarity: DCDC converter to be queried (DCDC_POS (default) or DCDC_NEG). DCDC_NEG will return 0 for unipolar configs. This parameter can be ignored given that only unipolar configuration is currently supported.
Returns

Voltage raw set point (0-4095))

def q_vnow(self, which=0):
546    def q_vnow(self, which=VNOW_POS):  # queries the voltage output
547        """Queries the feedback voltage of the HVPS.
548        :param which: which output voltage to read {VNOW_POS (default), VNOW_NEG, VNOW_OUT}. Voltage at the output of the
549        positive DCDC converter, negative DCDC converter, or output of the HVPS
550        :return: voltage value in volts
551        """
552
553        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
554            which = VNOW_POS
555        z = self._send_4bytes_receive_nbytes(SERIAL_QVNOW, param1=which, fstring='<2h')
556        z_out = z[1]
557        z_main = z[0]
558        if DEBUG:
559            y = "q_vnow -> " + str(z_main) + " / " + str(z_out)
560            print(y)
561        return z_main

Queries the feedback voltage of the HVPS.

Parameters
  • which: which output voltage to read {VNOW_POS (default), VNOW_NEG, VNOW_OUT}. Voltage at the output of the positive DCDC converter, negative DCDC converter, or output of the HVPS
Returns

voltage value in volts

def q_vnow_raw(self, which=0):
563    def q_vnow_raw(self, which=VNOW_POS):  # queries a raw value of the voltage output
564        """Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids
565        running slow floating point calculations on the
566        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
567        execute than q_vnow(). Useful for streaming voltage values
568        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
569        positive DCDC converter, negative DCDC converter, or output of the HVPS.
570        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
571        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
572        a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a
573        way to do this automatically"""
574
575        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
576            which = VNOW_POS
577        z = self._send_receive_4bytes(SERIAL_QVNOWRAW, param1=which, typ='uint')
578        z_out = z & 0xFFFF
579        z_main = (z & 0xFFFF0000) >> 16
580        if DEBUG:
581            y = "q_vnow_raw -> " + str(z_main) + " , " + str(z_out)
582            print(y)
583        return z_main, z_out

Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids running slow floating point calculations on the microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to execute than q_vnow(). Useful for streaming voltage values

Parameters
  • which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the positive DCDC converter, negative DCDC converter, or output of the HVPS.
Returns

a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a way to do this automatically

def q_vnow_fast(self, which=0):
585    def q_vnow_fast(self, which=VNOW_POS):  # queries the voltage output
586        """Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage
587        This avoids running slow floating point calculations on the
588        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
589        execute than q_vnow(). Useful for streaming voltage values.   This method is similar to q_vnow(), except that
590        the conversion from a raw value to a calibrated value is done
591        on the host rather than on the microcontroller. It can take up to 300us to convert a value on the MUC
592        (it depends on the method used (linear, quadratic, lookup table)
593        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
594        positive DCDC converter, negative DCDC converter, or output of the HVPS.
595        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
596        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
597        a 12 bit value that can be converted to voltage using the unit's calibration values The returned values
598        are calibrated voltages in Volt"""
599
600        z_main, z_out = self.q_vnow_raw(which=which)
601        v_main = self._convert_raw_to_voltage(z_main, which=which)
602        v_out = self._convert_raw_to_voltage(z_out, which=VNOW_OUT)
603
604        return v_main, v_out

Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage This avoids running slow floating point calculations on the microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to execute than q_vnow(). Useful for streaming voltage values. This method is similar to q_vnow(), except that the conversion from a raw value to a calibrated value is done on the host rather than on the microcontroller. It can take up to 300us to convert a value on the MUC (it depends on the method used (linear, quadratic, lookup table)

Parameters
  • which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the positive DCDC converter, negative DCDC converter, or output of the HVPS.
Returns

a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is a 12 bit value that can be converted to voltage using the unit's calibration values The returned values are calibrated voltages in Volt

def s_f(self, x):
606    def s_f(self, x):  # sets the frequency
607        """Sets the frequency of the signal when the HVPS is in switching or waveform mode (SWMODE_SW / SWMODE_SW).\n
608        The value returned is the new frequency, taking quantification into account.
609        :param x: frequency in Hz between 0.001 and 1000. In waveform mode, 0.16 Hz <= f <=100 Hz
610        :return: frequency accepted by hvps-x in Hz
611        """
612        x = constrain(x, 0.001, 1000.0)
613
614        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
615        # Ex: 10.0: 0x41 0x20 0x00 0x00.
616        # 0 10000010 01000000000000000000000
617        # S=0 Positive
618        # Exp=130-127 (offset)=2^3=8
619        # Fraction=1+2^-2=1.25
620        # number=+1.25*8=10
621        f_byte = struct.pack('<f', x)
622        z = self._send_nbytes_receive_4bytes(SERIAL_SF, f_byte, typ='float')
623
624        if DEBUG:
625            y = "s_f(" + str(x) + ") -> " + str(z)
626            print(y)
627        return z

Sets the frequency of the signal when the HVPS is in switching or waveform mode (SWMODE_SW / SWMODE_SW).

The value returned is the new frequency, taking quantification into account.

Parameters
  • x: frequency in Hz between 0.001 and 1000. In waveform mode, 0.16 Hz <= f <=100 Hz
Returns

frequency accepted by hvps-x in Hz

def q_f(self):
629    def q_f(self):  # queries the frequency
630        """Queries the frequency. The returned value is in Hz."""
631        z = self._send_receive_4bytes(SERIAL_QF, param1=0, param2=0, typ='float')
632
633        if DEBUG:
634            y = "q_f -> " + str(z)
635            print(y)
636
637        return z

Queries the frequency. The returned value is in Hz.

def s_current(self, xnorm, xhigh):
639    def s_current(self, xnorm, xhigh):  # sets the current limit setting
640        """Sets the current output in both current settings (normal and high). This value is used to calculate the PID
641         coefficients for the waveform. Values can be obtained by measuring the charging rate when charging a capacitor
642         of known value. This is a configuration parameter that must be set according to the capabilities of the hvps-x.
643         It is not a user-settable output current.
644        :param xnorm: current in A between 50E-6 and 500E-6 in normal output current config.
645        :param xhigh: current in A between 50E-6 and 500E-6 in high output current config.
646        :return: SERIAL_OK or SERIAL_ERROR
647        """
648        xnorm = constrain(xnorm, 50E-6, 500E-6)
649        xhigh= constrain(xhigh, 50E-6, 500E-6)
650        f_byte = struct.pack('<2f', xnorm, xhigh)
651        z = self._send_nbytes_receive_4bytes(SERIAL_SCURR, f_byte, typ='uint')
652
653        if DEBUG:
654            y = "s_current({},{}) -> {}".format(xnorm, xhigh, z)
655            print(y)
656        return z

Sets the current output in both current settings (normal and high). This value is used to calculate the PID coefficients for the waveform. Values can be obtained by measuring the charging rate when charging a capacitor of known value. This is a configuration parameter that must be set according to the capabilities of the hvps-x. It is not a user-settable output current.

Parameters
  • xnorm: current in A between 50E-6 and 500E-6 in normal output current config.
  • xhigh: current in A between 50E-6 and 500E-6 in high output current config.
Returns

SERIAL_OK or SERIAL_ERROR

def q_current(self):
658    def q_current(self):  # queries the current limit setting
659        """Queries the current limit setting. The returned value is in A.
660
661        :return: A tuple of two current values corresponding to the output current settings in normal and high current
662        modes."""
663
664        z = self._send_4bytes_receive_nbytes(SERIAL_QCURR, param1=0, param2=0, fstring='<2f')
665
666        if DEBUG:
667            y = "q_curr() -> " + str(z)
668            print(y)
669        return z

Queries the current limit setting. The returned value is in A.

Returns

A tuple of two current values corresponding to the output current settings in normal and high current modes.

def s_current_selection(self, x):
671    def s_current_selection(self, x):
672        """Sets the position of the current selection jumper
673        :param x: Position of the output current selection jumper. Either J2_3V3 (normal) or J2_5V depending on the
674        jumper position. 3V3 and 5V are written on the silkscreen and respectively correspond to normal and high current
675        modes. After changing the position of the jumper, this command should be run to reflect the change. The new
676        setting will be kept until the unit is power-cycled. To make the selection permanent, use the save() command
677        after the s_current_selection(x) command.
678        :return: Accepted current selection jumper position (J2_3V3 or J2_5V)
679        """
680        x = int(np.abs(x))
681        if x > J2_5V:
682            x = J2_5V
683        z = self._send_receive_4bytes(SERIAL_SCURRSEL, param1=x, param2=0, typ='uint')
684
685        if DEBUG:
686            print("s_current_selection({}) --> {}".format(x, z))
687        return z

Sets the position of the current selection jumper

Parameters
  • x: Position of the output current selection jumper. Either J2_3V3 (normal) or J2_5V depending on the jumper position. 3V3 and 5V are written on the silkscreen and respectively correspond to normal and high current modes. After changing the position of the jumper, this command should be run to reflect the change. The new setting will be kept until the unit is power-cycled. To make the selection permanent, use the save() command after the s_current_selection(x) command.
Returns

Accepted current selection jumper position (J2_3V3 or J2_5V)

def q_current_selection(self):
689    def q_current_selection(self):
690        """Queries the position of the current selection jumper
691        :return: Current selection (J2_3V3 or J2_5V)
692        """
693        z = self._send_receive_4bytes(SERIAL_QCURRSEL, param1=0, param2=0, typ='uint')
694
695        if DEBUG:
696            print("q_current_selection() --> {}".format(z))
697        return z

Queries the position of the current selection jumper

Returns

Current selection (J2_3V3 or J2_5V)

def s_duty(self, x):
699    def s_duty(self, x):  # sets the duty cycle of the switching signal
700        """Sets the duty cycle of the switching signal
701        :param x: the duty cycle (float in the range 0-1)
702        :return: the current duty cycle (float between 0 and 1)
703        """
704        duty = int(x * 1000)  # hvps-x is coding duty cycle on a 0-1000 scale
705
706        z = self._send_receive_4bytes(SERIAL_SDUTY, param1=0, param2=duty, typ='uint')
707        z = z / 1000
708        if DEBUG:
709            y = "s_duty(" + str(x) + ") -> " + str(z)
710            print(y)
711        return z

Sets the duty cycle of the switching signal

Parameters
  • x: the duty cycle (float in the range 0-1)
Returns

the current duty cycle (float between 0 and 1)

def q_duty(self):
713    def q_duty(self):  # queries the duty cycle of the switching signal
714        """queries the duty cycle of the switching signal
715        :return: the current duty cycle (float between 0 and 1)
716        """
717
718        z = self._send_receive_4bytes(SERIAL_QDUTY, param1=0, param2=0, typ='uint')
719        z = float(z) / 1000.0
720        if DEBUG:
721            y = "q_duty() -> " + str(z)
722            print(y)
723        return z

queries the duty cycle of the switching signal

Returns

the current duty cycle (float between 0 and 1)

def s_sw_mode(self, x):
726    def s_sw_mode(self, x):  # sets the switching mode
727        """Sets the switching mode of the hvps-x. \n
728        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
729        This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV
730        DC/DC converter.
731        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. This connects J4 to ground and
732        SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency and with
733        the specified duty cycle.
734        SWMODE_WFM: The optocouplers are controlled with a PWM signal to reproduce the user-defined waveform in memory
735        The program tab "Waveform" can be used to set the parameters of the waveform.
736        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
737        save() command enables to save this parameter in memory
738        :param x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW, SWMODE_WFM
739        :return: Switching mode set by the HVPS
740        """
741
742        if x > SWMODE_LOW:
743            x = SWMODE_OFF
744
745        z = self._send_receive_4bytes(SERIAL_SSWMODE, param1=x, param2=0, typ='uint')
746        if DEBUG:
747            y = "s_sw_mode(" + str(x) + ") -> " + str(z)
748            print(y)
749        return z

Sets the switching mode of the hvps-x.

SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. This connects J4 to ground and SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency and with the specified duty cycle. SWMODE_WFM: The optocouplers are controlled with a PWM signal to reproduce the user-defined waveform in memory The program tab "Waveform" can be used to set the parameters of the waveform. The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the save() command enables to save this parameter in memory

Parameters
  • x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW, SWMODE_WFM
Returns

Switching mode set by the HVPS

def q_sw_mode(self):
751    def q_sw_mode(self):  # queries the switching mode
752        """queries the switching mode of the hvps-x. \n
753        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
754        This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV
755        DC/DC converter.
756        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. This connects J4 to ground and
757        SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency and with
758        the specified duty cycle.
759        SWMODE_WFM: The optocouplers are controlled with a PWM signal to reproduce the user-defined waveform in memory
760        The program tab "Waveform" can be used to set the parameters of the waveform.
761        :return: Switching mode set by the HVPS: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW, SWMODE_WFM
762        """
763        z = self._send_receive_4bytes(SERIAL_QSWMODE, param1=0, param2=0, typ='uint')
764        if DEBUG:
765            y = "q_sw_mode -> " + str(z)
766            print(y)
767        return z

queries the switching mode of the hvps-x.

SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. This connects J4 to ground and SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency and with the specified duty cycle. SWMODE_WFM: The optocouplers are controlled with a PWM signal to reproduce the user-defined waveform in memory The program tab "Waveform" can be used to set the parameters of the waveform.

Returns

Switching mode set by the HVPS: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW, SWMODE_WFM

def s_sw_src(self, x):
769    def s_sw_src(self, x):  # sets the switching source
770        """Sets the source of the switching signal.
771
772        Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard
773        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
774        Using the save() command enables to save this parameter in memory
775        :param x: SWSRC_TMR, or SWSRC_BTTN
776        :return: SWSRC_TMR, or SWSRC_BTTN
777        """
778        if x > SWSRC_BTTN:
779            x = SWSRC_TMR
780        z = self._send_receive_4bytes(SERIAL_SSWSRC, param1=x, param2=0, typ='uint')
781        if DEBUG:
782            y = "s_sw_src(" + str(x) + ") -> " + str(z)
783            print(y)
784        return z

Sets the source of the switching signal.

Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard switching (from internal clock of the board), or SWSRC_BTTN for the push button.

Using the save() command enables to save this parameter in memory

Parameters
  • x: SWSRC_TMR, or SWSRC_BTTN
Returns

SWSRC_TMR, or SWSRC_BTTN

def q_sw_src(self):
786    def q_sw_src(self):  # queries the switching source
787        """queries the source of the switching signal.
788
789        queries the source of the switching signal. Output values are: SWSRC_TMR for onboard
790        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
791        Using the save() command enables to save this parameter in memory
792        :return: SWSRC_TMR, or SWSRC_BTTN
793        """
794
795        z = self._send_receive_4bytes(SERIAL_QSWSRC, param1=0, param2=0, typ='uint')
796
797        if DEBUG:
798            y = "q_sw_src -> " + str(z)
799            print(y)
800        return z

queries the source of the switching signal.

queries the source of the switching signal. Output values are: SWSRC_TMR for onboard switching (from internal clock of the board), or SWSRC_BTTN for the push button.

Using the save() command enables to save this parameter in memory

Returns

SWSRC_TMR, or SWSRC_BTTN

def s_bttn_cfg(self, x):
802    def s_bttn_cfg(self, x):  # sets the configuration of the push button
803        """Defines the behaviour of the push button
804
805        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
806        save() command enables to save this parameter in memory
807        :param x: 2-bit value (0x0 to 0x3). bit 0 - Defines the behaviour of the push button, when the switching source
808        of the HVPS is set to the push button
809        (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour
810        (i.e. the high voltage is turned on as long as the button is pressed),
811        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
812        to turn it off).\n
813        bit 1 - State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
814        :return: the 2-bit button config value
815        """
816        if x > 3:
817            x = 3
818        z = self._send_receive_4bytes(SERIAL_SBTTNCFG, param1=x, param2=0, typ='uint')
819        if DEBUG:
820            y = "s_bttn_cfg(" + str(x) + ") -> " + str(z)
821            print(y)
822
823        return z

Defines the behaviour of the push button

The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the save() command enables to save this parameter in memory

Parameters
  • x: 2-bit value (0x0 to 0x3). bit 0 - Defines the behaviour of the push button, when the switching source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed), and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time to turn it off).

bit 1 - State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW

Returns

the 2-bit button config value

def q_bttn_cfg(self):
825    def q_bttn_cfg(self):  # queries the latch mode of the push button
826        """Queries the behaviour of the push button
827
828        :return: the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching
829        source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above).
830        Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed),
831        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
832        to turn it off).\n
833        bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
834        """
835        z = self._send_receive_4bytes(SERIAL_QBTTNCFG, param1=0, param2=0, typ='uint')
836        if DEBUG:
837            y = "q_bttn_cfg -> " + str(z)
838            print(y)
839
840        return z

Queries the behaviour of the push button

Returns

the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above). Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed), and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time to turn it off).

bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW

def q_kill(self):
842    def q_kill(self):  # queries the state of the kill button. Kill=1 means HV is disabled
843        """Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at
844        the hvps-x output, irrespective of any software setting.
845        :return: 1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise.
846        """
847        z = self._send_receive_4bytes(SERIAL_QKILL, param1=0, param2=0, typ='uint')
848
849        if DEBUG:
850            y = "q_kill -> " + str(z)
851            print(y)
852        return z

Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at the hvps-x output, irrespective of any software setting.

Returns

1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise.

def s_v_mode(self, x):
854    def s_v_mode(self, x):  # sets the voltage control mode
855        """Sets the voltage control mode
856
857        Sets the voltage control mode (i.e. how is the value of the output voltage controlled):\n
858        VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).\n
859        VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).\n
860        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
861        save() command enables to save this parameter in memory\n
862        VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However,
863        the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output
864        voltage remains within the allowed range
865        :param x: VMODE_R, VMODE_O
866        :return: VMODE_R, VMODE_O
867        """
868
869        if x > VMODE_O:
870            x = VMODE_R
871
872        z = self._send_receive_4bytes(SERIAL_SVMODE, param1=x, param2=0, typ='uint')
873        if DEBUG:
874            y = "s_v_mode(" + str(x) + ") -> " + str(z)
875            print(y)
876        return z

Sets the voltage control mode

Sets the voltage control mode (i.e. how is the value of the output voltage controlled):

VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).

VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).

The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the save() command enables to save this parameter in memory

VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However, the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output voltage remains within the allowed range

Parameters
  • x: VMODE_R, VMODE_O
Returns

VMODE_R, VMODE_O

def q_v_mode(self):
878    def q_v_mode(self):  # queries the switching source
879        """Queries the voltage control mode
880
881        :return: VMODE_R internal voltage regulator, VMODE_O internal
882        open loop control (on-board regulator disconnected).
883        """
884
885        z = self._send_receive_4bytes(SERIAL_QVMODE, param1=0, param2=0, typ='uint')
886
887        if DEBUG:
888            y = "q_v_mode -> " + str(z)
889            print(y)
890        return z

Queries the voltage control mode

Returns

VMODE_R internal voltage regulator, VMODE_O internal open loop control (on-board regulator disconnected).

def s_pid(self, x, pid=0):
893    def s_pid(self, x, pid=PID_KPP):
894        """Sets the gains of the PIDs
895        Use save() command to save the new values to memory
896        :param x: the value (float) of the gain.
897        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,O},
898        for positive voltage PID, output voltage PID (used for the waveform mode).
899        :return: the gain value
900        """
901        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
902        # Ex: 10.0: 0x41 0x20 0x00 0x00.
903        # 0 10000010 01000000000000000000000
904        # S=0 Positive
905        # Exp=130-127 (offset)=2^3=8
906        # Fraction=1+2^-2=1.25
907        # number=+1.25*8=10
908        pid_byte = struct.pack('<Bf', pid, x)
909        z = self._send_nbytes_receive_4bytes(SERIAL_SPID, pid_byte, typ='float')
910
911        if DEBUG:
912            y = "s_pid(" + str(x) + "," + str(pid) + ") -> " + str(z)
913            print(y)
914        return z

Sets the gains of the PIDs Use save() command to save the new values to memory

Parameters
  • x: the value (float) of the gain.
  • pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,O}, for positive voltage PID, output voltage PID (used for the waveform mode).
Returns

the gain value

def q_pid(self, pid=0):
916    def q_pid(self, pid=PID_KPP):  # queries the frequency
917        """returns the gains of the PIDs
918
919        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,O},
920        for positive voltage PID, output voltage PID (used for the waveform mode).
921        :return: the gain value of the chosen parameter
922        """
923
924        z = self._send_receive_4bytes(SERIAL_QPID, param1=pid, param2=0, typ='float')
925
926        if DEBUG:
927            y = "q_pid(" + str(pid) + ") -> " + str(z)
928            print(y)
929
930        return z

returns the gains of the PIDs

Parameters
  • pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,O}, for positive voltage PID, output voltage PID (used for the waveform mode).
Returns

the gain value of the chosen parameter

def s_cal(self, x, cal=1):
932    def s_cal(self, x, cal=CAL_C1P):
933        """Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values)
934        Use save() to commit the setting to memory
935        :param x:  the value of the calibration coefficient (float)
936        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,O}, for positive
937        voltage measurement, output voltage measurement
938        :return: the calibration value
939        """
940
941        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
942        # Ex: 10.0: 0x41 0x20 0x00 0x00.
943        # 0 10000010 01000000000000000000000
944        # S=0 Positive
945        # Exp=130-127 (offset)=2^3=8
946        # Fraction=1+2^-2=1.25
947        # number=+1.25*8=10
948        cal_byte = struct.pack('<Bf', cal, x)
949        z = self._send_nbytes_receive_4bytes(SERIAL_SCAL, cal_byte, typ='float')
950
951        if DEBUG:
952            y = "s_cal(" + str(x) + "," + str(cal) + ") -> " + str(z)
953            print(y)
954        return z

Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values) Use save() to commit the setting to memory

Parameters
  • x: the value of the calibration coefficient (float)
  • cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,O}, for positive voltage measurement, output voltage measurement
Returns

the calibration value

def q_cal(self, cal=1):
956    def q_cal(self, cal=CAL_C1P):  # queries the frequency
957        """queries the calibration constants of the analogue input signals (conversion to calibrated voltage values)
958
959        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,O}, for positive
960        voltage measurement, output voltage measurement
961        :return: the calibration value
962        """
963
964        z = self._send_receive_4bytes(SERIAL_QCAL, param1=cal, param2=0, typ='float')
965
966        if DEBUG:
967            y = "q_cal(" + str(cal) + ") -> " + str(z)
968            print(y)
969        return z

queries the calibration constants of the analogue input signals (conversion to calibrated voltage values)

Parameters
  • cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,O}, for positive voltage measurement, output voltage measurement
Returns

the calibration value

def s_calibration_method(self, which=0, calmeth=0):
971    def s_calibration_method(self, which=VNOW_POS, calmeth=CALMETH_POLYNOMIAL):
972        if not (calmeth == CALMETH_POLYNOMIAL or calmeth == CALMETH_LOOKUP):
973            calmeth = CALMETH_POLYNOMIAL
974        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
975            which = VNOW_POS
976        z = self._send_receive_4bytes(SERIAL_SCALMETH, param1=which, param2=calmeth, typ='uint')
977
978        if DEBUG:
979            y = "s_calibration_method({0}, {1}) -> {2}".format(which, calmeth, z)
980            print(y)
981        if which == VNOW_POS:
982            self.calmeth_p = z
983        elif which == VNOW_OUT:
984            self.calmeth_o = z
985        else:
986            self.calmeth_n = z
def q_calibration_method(self, which=0):
 988    def q_calibration_method(self, which=VNOW_POS):
 989        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
 990            which = VNOW_POS
 991        z = self._send_receive_4bytes(SERIAL_QCALMETH, param1=which, param2=0, typ='uint')
 992
 993        if DEBUG:
 994            y = "q_calibration_method({0}) -> {1}".format(which, z)
 995            print(y)
 996        if which == VNOW_POS:
 997            self.calmeth_p = z
 998        elif which == VNOW_NEG:
 999            self.calmeth_n = z
1000        else:
1001            self.calmeth_o = z
def s_lookup(self, x, n, table=1):
1003    def s_lookup(self, x, n, table=LOOKUP_ADC_P):
1004        if n > 20:
1005            n = 20
1006        n = n << 2  # shift left 2 bits
1007        if table == LOOKUP_ADC_P:
1008            n = n | 0b01
1009        elif table == LOOKUP_ADC_O:
1010            n = n | 0b10
1011        x = int(x)
1012        z = self._send_receive_4bytes(SERIAL_SLKUP, param1=n, param2=x, typ='int16')
1013        if DEBUG:
1014            y = "s_lookup({0},{1},{2}) - > {3}".format(x, n, table, z)
1015            print(y)
def q_lookup(self, which):
1017    def q_lookup(self, which):  # which=0: vout, which=1: ADC_p, which=2: ADC_o
1018        format_string = f'<21h'
1019        z = self._send_4bytes_receive_nbytes(SERIAL_QLKUP, param1=which, param2=0, fstring=format_string)
1020        lookup_list = list(z)
1021        if DEBUG:
1022            y = "q_lookup({0}) - > {1}".format(which, lookup_list)
1023            print(y)
1024        return lookup_list
def s_saturation_limit(self, limit):
1026    def s_saturation_limit(self, limit):    # limit: 0-> reset to default 2% above Vmax. Otherwise:1-4095
1027        if limit < 0:
1028            limit = 0
1029        elif limit > 4095:
1030            limit = 4095
1031        z = self._send_receive_4bytes(SERIAL_SLKUP, param1=0, param2=limit, typ='int16')
1032        if DEBUG:
1033            y = "s_saturation_limit({0}) - > {1}".format(limit, z)
1034            print(y)
1035        return z
def s_wfm_pts(self, n=1, m=500, pts=None):
1038    def s_wfm_pts(self, n=1, m=500, pts=None):
1039        """Sets the data points of the waveform
1040        Use save() to commit the setting to memory
1041        :param n:  The loop counter. The frequency of the signal is f = 1 / (n * m * 0.5E-3) 0 < n < 256. If you don't
1042        want to bother with n, set n = 1, and use s_f() after transferring the points to get the desired wfm frequency.
1043        :param m: The number of points in the waveform 0 <m <= 500
1044        :param pts: a list of integer voltage points. The list must be 500 elements long. If m<500, then the end of the list
1045        must be padded with zeros.
1046        :return: SERIAL_OK or SERIAL_ERROR
1047        """
1048        # parameters are 1) n (byte B) the counter divider, 2) the number of points m, an unsigned short, and 3) m unsigned short pts
1049        if pts is None:
1050            z = SERIAL_ERROR
1051            if DEBUG:
1052                print("s_wfm_pts(): ERROR: no points given")
1053        elif len(pts) == 500 and all(isinstance(item, int) for item in pts):
1054            n=int(n)
1055            if n > 255:
1056                n=255
1057            m=int(m)
1058            format_string = f'<B{WFM_MAX_PTS+1}H'
1059            buffer = struct.pack(format_string, n, m, *pts) # array pts always has WFM_MAX_PTS points, but with zeros as
1060            # padding at the end. m indicated the effective number of data points. This is to always have the same command
1061            # length
1062            z = self._send_nbytes_receive_4bytes(SERIAL_SWFMPTS, buffer)
1063            if DEBUG:
1064                y = "s_wfm_pts({0}, {1}, {2}) - > {3}".format(n, m, buffer, z)
1065                print(y)
1066            else:
1067                z = SERIAL_ERROR
1068                if DEBUG:
1069                    print("s_wfm_pts(): ERROR: number of points not equal to 500, or not all integer values")
1070        return z

Sets the data points of the waveform Use save() to commit the setting to memory

Parameters
  • n: The loop counter. The frequency of the signal is f = 1 / (n * m * 0.5E-3) 0 < n < 256. If you don't want to bother with n, set n = 1, and use s_f() after transferring the points to get the desired wfm frequency.
  • m: The number of points in the waveform 0
  • pts: a list of integer voltage points. The list must be 500 elements long. If m<500, then the end of the list must be padded with zeros.
Returns

SERIAL_OK or SERIAL_ERROR

def q_wfm_pts(self):
1072    def q_wfm_pts(self):
1073        """queries the data points of the waveform
1074        :return : n, z with n: the number of points in the waveform and z: the list of waveform points in Volts
1075        """
1076        format_string = f'<H500H' # 1 16-bit unsigned value for the number of points and 500 unsigned 16-bit values. Not all points are effective points (some may be padding zeroes) hence why n is required.
1077        z = self._send_4bytes_receive_nbytes(SERIAL_QWFMPTS, param1=0, param2=0, fstring=format_string)
1078        z = list(z)
1079        n = z.pop(0)    # the first element is the number of points
1080        del z[n:]       #remove all elements of the list after the number of points to keep
1081        if DEBUG:
1082            y = "q_wfm_pts() - > {}/{}".format(n, z)
1083            print(y)
1084        return n, z

queries the data points of the waveform

Returns

the list of waveform points in Volts

def save(self):
1088    def save(self):  # save current HVPS parameters into the memory
1089        """save active HVPS parameters into the memory
1090
1091        This command saves the active parameters as \'current\' settings. Current setting are the settings that are
1092        loaded when power is applied to the hvps-x
1093
1094        :return: SERIAL_OK or SERIAL_ERROR
1095        """
1096
1097        z = self._send_receive_4bytes(SERIAL_SAVE)
1098
1099        if DEBUG:
1100            y = "save -> " + str(z)
1101            print(y)
1102        return z

save active HVPS parameters into the memory

This command saves the active parameters as 'current' settings. Current setting are the settings that are loaded when power is applied to the hvps-x

Returns

SERIAL_OK or SERIAL_ERROR

def save_memory_to_file(self, settings=0):
1104    def save_memory_to_file(self, settings=SETTINGS_CURRENT):
1105        """Dumps the content of the memory into a JSON file.
1106
1107        Dumps the content of the memory into a file. This is useful to keep a backup of the parameters on file.
1108        Files will be created in the interface folder and have the following format: Name_settings_type_date_time.json\n
1109        json files with settings can be transferred back to the hvps-x with the transfer_settings() method of the HVPS
1110        class, or the higher-level function transfer_file_to_memory()
1111        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
1112        :return: the name of the created file (it follows a standard naming scheme with the date, the name of the
1113        hvps-x and the setting type.
1114        """
1115
1116        memory = self.q_mem(settings)
1117        now = datetime.now()  # current date and time
1118        date_time = now.strftime("%Y_%m_%d")
1119        if settings == SETTINGS_CURRENT:
1120            st_string = '_cur_st_'
1121        else:
1122            st_string = '_bk_st_'
1123        file_name = self.name.decode("utf-8") + st_string + date_time + '.json'
1124        with open(file_name, "w") as write_file:
1125            json.dump(memory, write_file, indent=4)
1126
1127        return file_name

Dumps the content of the memory into a JSON file.

Dumps the content of the memory into a file. This is useful to keep a backup of the parameters on file. Files will be created in the interface folder and have the following format: Name_settings_type_date_time.json

json files with settings can be transferred back to the hvps-x with the transfer_settings() method of the HVPS class, or the higher-level function transfer_file_to_memory()

Parameters
  • settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
Returns

the name of the created file (it follows a standard naming scheme with the date, the name of the hvps-x and the setting type.

def s_settings(self, settings=0):
1129    def s_settings(self,
1130                   settings=SETTINGS_CURRENT):  # sets the active setting to a particular type (useful before saving)
1131        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_FACTORY or settings == SETTINGS_BACKUP):
1132            settings = SETTINGS_CURRENT
1133        x = self._send_receive_4bytes(SERIAL_SST, param1=settings)
1134        if DEBUG:
1135            print('s_settings({0}) -> {1}'.format(settings, x))
1136        return x
def load_settings(self, settings=0):
1138    def load_settings(self, settings=SETTINGS_CURRENT):  # Load a setting set from memory to the active settings
1139        """This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x
1140
1141        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_BACKUP
1142        :return:
1143        """
1144
1145        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_BACKUP):
1146            settings = SETTINGS_CURRENT
1147        x = self._send_receive_4bytes(SERIAL_LST, param1=settings)
1148        if DEBUG:
1149            print('load_settings({0}) -> {1}'.format(settings, x))
1150
1151        self._initialise_hvpsx()  # need to reread all parameters
1152
1153        return x

This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x

Parameters
  • settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_BACKUP
Returns
def transfer_settings(self, dict_settings):
1155    def transfer_settings(self, dict_settings):
1156        """ transfer a setting dictionary from the computer to the hvps-x memory
1157        :param dict_settings: a dictionary containing the settings values.
1158        :return: SERIAL_OK or SERIAL_ERROR
1159        The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together
1160        these two functions make it possible to backup the settings (this includes calibration and PID settings in a
1161        file, and gives the opportunity to restore the settings"""
1162
1163        error = False
1164        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1165        siz = struct.calcsize(memory_string[fw_string])
1166        buffer = ctypes.create_string_buffer(siz)
1167        memmap = memorymap[fw_string]
1168        counter = 0
1169        for x in memmap:
1170            n = x[0]
1171            key = x[1]
1172            data_format = x[2]
1173            if data_format == 'B' or data_format == 's':
1174                bytelength = 1
1175            elif data_format == 'h' or data_format == 'H':
1176                bytelength = 2
1177            elif data_format == 'f':
1178                bytelength = 4
1179            else:
1180                bytelength = 1
1181            data = dict_settings[key]
1182            if key == 'Name':
1183                data = bytes(data, 'utf-8')
1184                if len(data) > 12:
1185                    error = True
1186                    if DEBUG:
1187                        print("Error: name should not exceed 12 characters")
1188                else:
1189                    format_str = '<{0}{1}'.format(n, data_format)
1190                    struct.pack_into(format_str, buffer, counter, data)
1191            else:
1192                format_str = '<{0}'.format(data_format)
1193                if n == 1:
1194                    struct.pack_into(format_str, buffer, counter, data)
1195                else:
1196                    for i in range(n):
1197                        try:
1198                            struct.pack_into(format_str, buffer, counter + i * bytelength, data[i])
1199                        except IndexError:
1200                            error = True
1201                            if DEBUG:
1202                                print('setting dictionary does not fit the expected format')
1203
1204            counter = counter + n * bytelength
1205        data_fw = 'Fw-{0}.{1}'.format(dict_settings['fw_major'], dict_settings['fw_minor'])
1206        if data_fw != fw_string:
1207            error = True
1208            if DEBUG:
1209                print('Error: JSON file firmware version does not match firmware currently on hvps-x. Exiting')
1210            exit(0)
1211        if not error:
1212            buffer = b'xxx' + buffer    # Adds 3 random bytes. CMD + 3 random bytes means that when mapping the transfer
1213            # buffer to a structure, it will on an aligned memory address
1214            x = self._send_nbytes_receive_4bytes(SERIAL_XFERST, buffer)
1215        else:
1216            x = SERIAL_ERROR
1217
1218        if DEBUG:
1219            print("transfer_settings(...) -> {0}".format(x))
1220        return x

transfer a setting dictionary from the computer to the hvps-x memory

Parameters
  • dict_settings: a dictionary containing the settings values.
Returns

SERIAL_OK or SERIAL_ERROR The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together these two functions make it possible to backup the settings (this includes calibration and PID settings in a file, and gives the opportunity to restore the settings

def copy_settings(self, src, dst):
1222    def copy_settings(self, src, dst):
1223        """Copies one set of settings to another location:\n
1224        Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings
1225        (for example if some temporary settings were saved as current settings, for example to experiment with new PID
1226        gain values)\
1227        Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new
1228        calibration as a set of back-up settings
1229        :param src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS)
1230        :param dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by
1231        the source
1232        :return: SERIAL_OK or SERIAL_ERROR
1233        """
1234        if (src == SETTINGS_CURRENT or src == SETTINGS_BACKUP) and (dst == SETTINGS_CURRENT or dst == SETTINGS_BACKUP):
1235            x = self._send_receive_4bytes(SERIAL_CPYST, param1=src, param2=dst)
1236        else:
1237            x = SERIAL_ERROR
1238
1239        if DEBUG:
1240            print('copy_settings({0},{1}) -> {2}'.format(src, dst, x))
1241
1242        return x

Copies one set of settings to another location:

Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings (for example if some temporary settings were saved as current settings, for example to experiment with new PID gain values) Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new calibration as a set of back-up settings

Parameters
  • src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS)
  • dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by the source
Returns

SERIAL_OK or SERIAL_ERROR

def q_mem(self, settings=0):
1244    def q_mem(self, settings=SETTINGS_CURRENT):  # queries the content of the memory
1245        """
1246        :param settings: which of the two sets of setting sets to query. Either SETTINGS_CURRENT or SETTINGS_Backup
1247        :return: A dictionary with the content of the memory. This is similar to the function save_memory_to_file()
1248        except that it doesn't save the content to a file"""
1249        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1250        memmap = memorymap[fw_string]
1251        dict_mem = {}
1252
1253        z = self._send_4bytes_receive_nbytes(SERIAL_QMEM, settings, param2=0, fstring=memory_string[fw_string])
1254        bytecount = 0
1255        for i in range(len(memmap)):
1256            length = memmap[i][0]
1257            field = memmap[i][1]
1258            if field == 'Name':
1259                length = 1
1260            y = z[bytecount:bytecount + length]
1261            # if field != 'Padding':
1262            if length == 1:
1263                dict_mem[field] = y[0]
1264            else:
1265                dict_mem[field] = y
1266            bytecount = bytecount + length
1267
1268        dict_mem['Name'] = dict_mem['Name'].decode('UTF-8')
1269        dict_mem['Name'] = dict_mem['Name'].split("\0")[0]
1270
1271        if DEBUG:
1272            print(dict_mem)
1273        return dict_mem
Parameters
  • settings: which of the two sets of setting sets to query. Either SETTINGS_CURRENT or SETTINGS_Backup
Returns

A dictionary with the content of the memory. This is similar to the function save_memory_to_file() except that it doesn't save the content to a file

def q_ver(self):
1275    def q_ver(self):  # queries the firmware version
1276        """returns the current version of the firmware / hardware running on the board."""
1277        z = self._send_receive_4bytes(SERIAL_QVER)
1278        Firm_minor = z & 0xFF
1279        Firm_major = (z >> 8) & 0xFF
1280        Hard_minor = (z >> 16) & 0xFF
1281        Hard_major = (z >> 24)
1282        self.firmware = Firm_major + Firm_minor / 10
1283        self.hardware = Hard_major + Hard_minor / 10
1284        if DEBUG:
1285            y = "q_ver -> {0} / {1}".format(self.firmware, self.hardware)
1286            print(y)
1287        return self.firmware, self.hardware

returns the current version of the firmware / hardware running on the board.

def q_conf(self):
1290    def q_conf(self):  # queries the configuration
1291        """returns the configuration of the board.
1292        :return: the configuration of the board in the high 2 bytes (CONF_UNCONF, CONF_UNIPOLAR, or CONF_BIPOLAR)
1293        and the PID (product ID) in the low 2 bytes. This is 0x0632 for a hvps-x unit.
1294        """
1295
1296        z = self._send_receive_4bytes(SERIAL_QCONF)
1297        z_hvps = z & 0xFFFF
1298        z_conf = (z & 0xFFFF0000) >> 16
1299        if DEBUG:
1300            y = "q_conf -> " + hex(z_conf) + " / " + hex(z_hvps)
1301            print(y)
1302        self.conf = z_conf
1303        return z

returns the configuration of the board.

Returns

the configuration of the board in the high 2 bytes (CONF_UNCONF, CONF_UNIPOLAR, or CONF_BIPOLAR) and the PID (product ID) in the low 2 bytes. This is 0x0632 for a hvps-x unit.

def s_conf(self, bipolar):
1305    def s_conf(self, bipolar):
1306        """Sets the configuration of the board
1307        :param bipolar: boolean. Is board bipolar (True), or unipolar (false)
1308        :return: bipolar: boolean
1309        """
1310        if bipolar:
1311            param = CONF_BIPOLAR
1312        else:
1313            param = CONF_UNIPOLAR
1314        z = self._send_receive_4bytes(SERIAL_SCONF, param1=param)
1315        if DEBUG:
1316            y = "s_conf -> " + hex(z)
1317            print(y)
1318            if z == SERIAL_ERROR:
1319                print("Error: this configuration is not recognised")
1320        self.conf = z
1321        return z

Sets the configuration of the board

Parameters
  • bipolar: boolean. Is board bipolar (True), or unipolar (false)
Returns

bipolar: boolean

def s_vmax(self, x):
1323    def s_vmax(self, x):  # sets the maximum voltage rating of the board
1324        """ sets the voltage rating of the hvps-x. Must match the EMCO DC/DC converter rating.
1325        :param x: Voltage rating of hvps-x in Volt
1326        :return: Voltage rating of hvps-x in Volt
1327        """
1328
1329        x = constrain(x, 0, 6000)
1330        z = self._send_receive_4bytes(SERIAL_SVMAX, param2=x)
1331        if DEBUG:
1332            y = "s_vmax(" + str(x) + ") -> " + str(z)
1333            print(y)
1334        self.vmax = z
1335        return z

sets the voltage rating of the hvps-x. Must match the EMCO DC/DC converter rating.

Parameters
  • x: Voltage rating of hvps-x in Volt
Returns

Voltage rating of hvps-x in Volt

def q_vmax(self):
1337    def q_vmax(self):  # queries the voltage rating of the board
1338        """ Queries the maximal voltage of the board. The returned value is in volts.
1339        :return: board maximal voltage (V)
1340        """
1341        z = self._send_receive_4bytes(SERIAL_QVMAX)
1342        if DEBUG:
1343            y = "q_vmax -> " + str(z)
1344            print(y)
1345        self.vmax = z
1346        return z

Queries the maximal voltage of the board. The returned value is in volts.

Returns

board maximal voltage (V)

def s_name(self, x):
1348    def s_name(self, x):  # set the name of the HVPS
1349        """ Sets the name of the HVPS.
1350        :param x: Name of the hvps-x. 11 characters maximum
1351        :return: name accepted by hvps-x
1352        """
1353        ll = len(x)
1354        if ll <= 12:
1355            x = bytearray(x, 'utf-8')
1356            for i in range(12 - ll):  # pad the string with 0s
1357                x = x + b'\0'
1358            self._send_nbytes_receive_4bytes(SERIAL_SNAME, x, typ='uint')
1359            z = self.q_name()
1360        else:
1361            z = 'too long'
1362        if DEBUG:
1363            y = "s_name(" + str(x) + ") -> " + str(z)
1364            print(y)
1365        return z

Sets the name of the HVPS.

Parameters
  • x: Name of the hvps-x. 11 characters maximum
Returns

name accepted by hvps-x

def q_name(self):
1367    def q_name(self):  # queries the name of the board
1368        """queries the name of the board
1369        :return: Name of the board
1370        """
1371
1372        # x = self._send_receive_4bytes(SERIAL_QNAME, param1=0)
1373        # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=4) << 32)
1374        # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=8) << 64)
1375        # x = x.to_bytes(12, 'little')
1376
1377        z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s')
1378        z = z[0].split(b'\x00', 1)[0]   # take first (only) element of the tuple and remove the 0s at the end of string
1379
1380        self.name = z
1381        if DEBUG:
1382            y = "q_name -> " + str(z)
1383            print(y)
1384        return z

queries the name of the board

Returns

Name of the board

def set_hardware_version(self, hw_major, hw_minor):
1386    def set_hardware_version(self, hw_major, hw_minor):
1387        param = (hw_major << 8) + hw_minor
1388        self._send_receive_4bytes(SERIAL_SHW, param2=param)
def q_load_parameters(self):
1390    def q_load_parameters(self):
1391        z = self._send_4bytes_receive_nbytes(SERIAL_QLOAD, param1=0, param2=0, fstring='<4H')
1392        if np.isin(0, np.array(z)):
1393            r = 0
1394            c = 0
1395        else:
1396            dv = self._convert_raw_to_voltage(z[0], VNOW_OUT)
1397            pwm_dv = z[1]
1398            ssv = self._convert_raw_to_voltage(z[2], VNOW_OUT)
1399            pwm_ss = z[3]
1400            current_selection = self.q_current_selection()
1401            imax = self.q_current()[current_selection]
1402            i_step = imax * pwm_dv / 4095
1403            i_ss = imax * pwm_ss / 4095
1404            dv_dt = dv / 5E-3  # dv is measured over 20ms
1405            c = i_step / dv_dt * 1E9 # C in nF
1406            r = ssv / i_ss / 1E6   # R in MOhm
1407        if DEBUG:
1408            print('q_load_parameters() --> {} nF, {} MOhm'.format(c, r))
1409        return r, c
def check_version_compatibility(self):
1411    def check_version_compatibility(self):
1412        self.q_ver()
1413        lib_string = 'Lib-' + LIB_VER
1414        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1415        hw_string = 'Hw-{0:.1f}'.format(self.hardware)
1416
1417        list_compatible_fw = compatibility_dict[lib_string]
1418        if fw_string in list_compatible_fw:
1419            list_compatible_hw = compatibility_dict[fw_string]
1420            if hw_string not in list_compatible_hw:
1421                self.err |= ERR_FIRM
1422        else:
1423            self.err |= ERR_FIRM
def diagnostic_ADC(self, which_vnow=0):
1425    def diagnostic_ADC(self, which_vnow=VNOW_POS):
1426        x = self._send_4bytes_receive_nbytes(SERIAL_DADC, which_vnow, 0, '<32H')
1427        return x
def q_trace(self, which_vnow=0):
1429    def q_trace(self, which_vnow=VNOW_POS):
1430        x = self._send_4bytes_receive_nbytes(SERIAL_QTRACE, which_vnow, 0, '<100H')
1431        x = np.array(x)
1432        if DEBUG:
1433            y = "trace({0}) -> {1}".format(which_vnow, x)
1434            print(y)
1435        return x
def s_trace_rate(self, rate=5):
1437    def s_trace_rate(self, rate=5):
1438        """Sets the rate at which trace points are stored in memory
1439        :param rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is
1440        calculating the voltages; currently evey 5ms.
1441        :return: the rate accepted by the hvps-x"""
1442
1443        rate = int(rate)
1444        if rate < 2:
1445            rate = 2
1446        x = self._send_receive_4bytes(SERIAL_STRRATE, param1=0, param2=rate, typ='uint')
1447        x = x / 10      # the function returns the number of ticks of the counter, which counts at 10kHz.
1448        if DEBUG:
1449            y = "s_trace_rate({0}) -> {1}".format(rate, x)
1450            print(y)
1451        return x

Sets the rate at which trace points are stored in memory

Parameters
  • rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is calculating the voltages; currently evey 5ms.
Returns

the rate accepted by the hvps-x

def q_trace_rate(self):
1453    def q_trace_rate(self):
1454        """queries the rate at which trace points are stored in memory
1455        :param
1456        :return the rate at which trace points are stored in memory (in ms)"""
1457
1458        x = self._send_receive_4bytes(SERIAL_QTRRATE, param1=0, param2=0, typ='uint')
1459        x = x / 10  # the function returns the number of ticks of the counter, which counts at 10kHz.
1460        if DEBUG:
1461            y = "q_trace_rate() -> {0}".format(x)
1462            print(y)
1463        return x

queries the rate at which trace points are stored in memory :param :return the rate at which trace points are stored in memory (in ms)

def s_waveform_pwm(self, value):
1465    def s_waveform_pwm(self, value):
1466
1467        value = constrain(value, -4095, 4095)
1468        value = int(value)
1469        x = self._send_receive_4bytes(SERIAL_WFMPWM, param1=0, param2=value, typ='uint')
1470        if DEBUG:
1471            y = "s_waveform_pwm({0}) -> {1}".format(value, x)
1472            print(y)
1473        return x
def list_hvps(list_all=False):
1476def list_hvps(list_all=False):
1477    """lists the hvps-x connected to PC in a dictionary.The key is the name of the HVPS, and the parameter is
1478    the associated serial port
1479    :param list_all: if True list all connected HVPS.
1480    if list_all is false, it will only list device that are configured. Using True enable to list unconfigured
1481    devices and give the opportunity to configure them
1482    :return: dictionary of connected hvps-x"""
1483
1484    hvpsx_ports = [  # creates a list with all STM32 VCP devices connected
1485        p.device
1486        for p in serial.tools.list_ports.comports()
1487        if (p.pid == 0x5740 and p.vid == 0x483)
1488    ]
1489    dict_hvps = {}
1490    for port in hvpsx_ports:
1491        dev = HVPS(port, init=False)  # Attempts to connect to HVPS but without initialisation
1492        if dev.is_hvpsx:
1493            if dev.conf != CONF_UNCONF or list_all:
1494                dict_hvps[dev.name.decode()] = port  # add an entry in the dictionary with the name of the
1495            # HVPS and the port
1496        dev.close(zero=False)
1497
1498    return dict_hvps

lists the hvps-x connected to PC in a dictionary.The key is the name of the HVPS, and the parameter is the associated serial port

Parameters
  • list_all: if True list all connected HVPS. if list_all is false, it will only list device that are configured. Using True enable to list unconfigured devices and give the opportunity to configure them
Returns

dictionary of connected hvps-x

def connect_to_hvps(unconf=False):
1501def connect_to_hvps(unconf=False):
1502    """Scan for connected hvps-x and connects to it if a single hvps-x is found. User can choose to which hvps-x to
1503    connect if more than one is detected.
1504    :param unconf: True/False. If true will connect to an unconfigured hvps-x. If False (default) only connects to a
1505    configured device
1506    :return: a HVPS object (or None if could not connect to a valid hvps-x
1507    If more than 1 hvps-x is connected to the computed, a list of names is displayed and the user must choose which one
1508    they want to connect to"""
1509    cont = False  # flag to decide whether to continue the program or close the COM port
1510    dev = None
1511
1512    ports = list_hvps(list_all=unconf)
1513    keys = list(ports.keys())
1514    if len(ports) == 0:
1515        print("No HVPS connected to the device. Terminating program")
1516        exit()
1517    elif len(ports) == 1:
1518        dev = HVPS(ports[keys[0]])  # connects to the only available HVPS
1519    else:
1520        print("List of connected HVPS:")
1521        for i in range(len(ports)):
1522            print(str(i) + ": " + keys[i])
1523        print("Enter the number of the board you want to connect to")
1524        connect_to = 0
1525        try:
1526            connect_to = int(input())
1527        except ValueError:
1528            print("Invalid entry. Terminating program")
1529            exit()
1530        if connect_to >= len(ports):
1531            print("Invalid entry. Terminating program")
1532            exit()
1533        else:
1534            dev = HVPS(ports[keys[connect_to]])
1535    error = dev.err
1536    if error & ERR_PORT:
1537        print("Cannot open COM port. Probably busy with another process. Terminating program")
1538    if error & ERR_COM:
1539        print("Cannot communicate with hvps-x. Has firmware been flashed?. Terminating program")
1540    if error & ERR_TYPE:
1541        print("Device connected this port is not recognised. Terminating program")
1542    if error & ERR_CONF:
1543        print("This hvps-x is not configured. Please configure unit before using it. "
1544              "Terminating program.")
1545        cont = True
1546    elif error & ERR_FIRM:
1547        print("Warning: your hvps-x library is not optimal for the firmware or the firmware is not optimal for your "
1548              "hardware. Refer to the website for a compatibility table")
1549        cont = True
1550    if error == 0:
1551        cont = True
1552    if cont:
1553        return dev
1554    else:
1555        return None

Scan for connected hvps-x and connects to it if a single hvps-x is found. User can choose to which hvps-x to connect if more than one is detected.

Parameters
  • unconf: True/False. If true will connect to an unconfigured hvps-x. If False (default) only connects to a configured device
Returns

a HVPS object (or None if could not connect to a valid hvps-x If more than 1 hvps-x is connected to the computed, a list of names is displayed and the user must choose which one they want to connect to

def configure(dev):
1558def configure(dev):
1559    """Script to use to for the initial configuration (or re-configuration) of an hvps-x. Follow prompt on the console.
1560    :param dev: an HVPS object
1561    :return: nothing
1562    """
1563    if dev != -1:
1564        config = configparser.RawConfigParser()
1565        config.read('config.ini')
1566
1567        if dev.conf != CONF_UNCONF:
1568            answer = input("It appears this hvps-x is already configured. (C)ontinue with configuration or (A)bort?: ")
1569            if answer == 'a' or answer == 'A':
1570                print("exiting configuration")
1571                dev.close()
1572                exit(-1)
1573        dev.s_conf(bipolar=False)   # This is now a constant as there is no bipolar configuration
1574        answer = input("Enter the version of your hvps-x PCB (hardware version), as printed on the PCB "
1575                       "(format: X.Y; e.g. 1.2): ")
1576        hw_list = answer.split('.')
1577        hw = int(hw_list[0]) + int(hw_list[1]) / 10
1578        if len(hw_list) != 2:
1579            print("Error, the format of the hardware string must be X.Y, e.g. 1.2. Exiting")
1580            exit(0)
1581        dev.set_hardware_version(int(hw_list[0]), int(hw_list[1]))
1582        print("The current name of the hvps-x is " + dev.q_name().decode("utf-8"))
1583        answer = input("Do you want to set/change the name of this hvps-x? Y/N?: ")
1584        if answer == 'y' or answer == 'Y':
1585            name = input("Enter hvps-x name (12 char max): ")
1586            if len(name) > 12:
1587                print("Name too long. Ignoring name change")
1588            else:
1589                dev.s_name(name)
1590        else:
1591            print("Leaving name unchanged")
1592        print("\nThe current maximal voltage rating of this HVPS is " + str(dev.q_vmax()) + " V")
1593        answer = input("Do you want to set the maximal voltage of the HVPS? Y/N: ")
1594        if answer == 'y' or answer == 'Y':
1595            answer = input("Enter the maximal voltage of the hvps-x in Volt. It must match the voltage rating of the "
1596                           "Emco DC/DC converter: ")
1597            print("Setting Vmax to " + answer + "V")
1598            dev.s_vmax(int(answer))
1599        else:
1600            print("Leaving Vmax unchanged")
1601        vmax = dev.q_vmax()
1602        conf_section = 'hvps-x-' + str(vmax)
1603        if config.has_section(conf_section):
1604            print("Default values for this voltage found in configuration file\n")
1605            answer = input("Do you want to replace the voltage calibration values stored in the hvps-x by the one from the config file "
1606                           "Y/N (choose Y if configuring a new hvps-x): ")
1607            if answer == 'Y' or answer == 'y':
1608                c0p = config.getfloat(conf_section, 'C0P')
1609                c1p = config.getfloat(conf_section, 'C1P')
1610                c2p = config.getfloat(conf_section, 'C2P')
1611                c0o = config.getfloat(conf_section, 'C0O')
1612                c1o = config.getfloat(conf_section, 'C1O')
1613                c2o = config.getfloat(conf_section, 'C2O')
1614
1615                dev.s_cal(c0p, CAL_C0P)
1616                dev.s_cal(c1p, CAL_C1P)
1617                dev.s_cal(c2p, CAL_C2P)
1618                dev.s_cal(c0o, CAL_C0O)
1619                dev.s_cal(c1o, CAL_C1O)
1620                dev.s_cal(c2o, CAL_C2O)
1621                print("Voltage calibration values set...\n")
1622            answer = input(
1623                "Reset PID values to their default values? Y/N (choose Y when configuring a board for the first "
1624                "time): ")
1625            if answer == 'Y' or answer == 'y':
1626                kpp = config.getfloat(conf_section, 'KPP')
1627                kip = config.getfloat(conf_section, 'KIP')
1628                kdp = config.getfloat(conf_section, 'KDP')
1629
1630                dev.s_pid(kpp, PID_KPP)
1631                dev.s_pid(kip, PID_KIP)
1632                dev.s_pid(kdp, PID_KDP)
1633                print("PID values set...\n")
1634
1635            if hw >= 1.4:
1636                answer = input("Do you want to set the output current values? Y/N (if No, then default values will "
1637                               "be used): ")
1638                if answer == 'Y' or answer == 'y':
1639                    inorm = input("Normal current mode: Enter the output current in uA (e.g. 120): ")
1640                    ihigh = input("High current mode: Enter the output current in uA (e.g. 120): ")
1641                    inorm = 1e-6 * float(inorm)
1642                    ihigh = 1e-6 * float(ihigh)
1643                else:
1644                    inorm = config.getfloat(conf_section, 'INORM')
1645                    ihigh = config.getfloat(conf_section, 'IHIGH')
1646                dev.s_current(inorm, ihigh)
1647                print("Current values set...\n")
1648                curr_sel = input("Position of the output current selection jumper:\n1: 3V3 (Norm)\n2: 5V (High)?\n"
1649                                 "Choose 1/2: ")
1650                if curr_sel == '2':
1651                    curr_sel = 1
1652                else:
1653                    curr_sel = 0
1654                dev.s_current_selection(curr_sel)
1655                print("Current selection jumper position set...\n")
1656
1657            print("hvps-x configured. It is recommended to perform a calibration of the voltage readout circuit!")
1658            print("Saving information to hvps-x memory. Backup-settings and default settings")
1659            dev.s_settings(SETTINGS_BACKUP)
1660            dev.save()
1661            dev.s_settings(SETTINGS_CURRENT)
1662            dev.save()
1663        else:
1664            print("Cannot find a section in the config file for a {0} V hvps-x. You need to enter values for voltage "
1665                  "calibration and PID manually ".format(vmax))
1666            dev.save()  # saves previous parameters anyway

Script to use to for the initial configuration (or re-configuration) of an hvps-x. Follow prompt on the console.

Parameters
  • dev: an HVPS object
Returns

nothing

def check_functionality(dev):
1669def check_functionality(dev):
1670    """This script can be used after configuration to test the hvps-x functionality. Follow prompt on the console
1671
1672    :param dev: an HVPS object
1673    :return: nothing
1674    """
1675
1676    print("Important:\n"
1677          "This function is experimental and the threshold values are based on a limited number of assembled boards."
1678          "In particular:\n"
1679          "- If the test fails with a value very close to the threshold value, then your board is most likely fine.\n"
1680          "- This version of the script must be used with the bill of material revision 1. If you assembled your"
1681          "board with the original bill of material, this script will not return the correct output. You can use the"
1682          "script included with the library v1.0. Otherwise the website describe how the high-voltage functionality "
1683          "can be manually tested.\n"
1684          "- If unsure, contact Peta-pico-Voltron with test results (be sure to specify hardware version of PCB).\n\n")
1685    if not dev.q_kill():
1686        input("Place HV safety switch (S1) on position 0 and press any key.")
1687        if not dev.q_kill():
1688            input("Switch S1 appears not to be working. Check Switch S1 functionality and restart the test")
1689            dev.close()
1690            exit()
1691    print("During the course of this test, a moderate (~20% of full scale) voltage will be applied to the output of\n "
1692          "the hvps-x. Make sure that it nothing is connected to the output and that you are not touching the\n "
1693          "instrument\n")
1694    print("Voltage set point 0V. Output off")
1695    dev.s_vset(0, DCDC_BOTH)
1696    dev.s_sw_mode(SWMODE_OFF)
1697    dev.s_v_mode(VMODE_O)
1698    dev.s_sw_src(SWSRC_TMR)
1699
1700    print("---Testing HV enable switch (S1) ---")
1701    input("Place HV enable switch (S1) on position 1 and press any key")
1702    if dev.q_kill():
1703        print("S1 switch still reads off state. Check functionality of switch S1.\n Test FAILED. Exiting script...")
1704        dev.close()
1705        exit()
1706    else:
1707        print("***PASS\n")
1708
1709    print("---Testing Voltage monitoring at set point 0 ---")
1710    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1711    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1712    if vnow_p < 25:
1713        print("This is within the tolerance range V < 25. Continuing...")
1714
1715    else:
1716        print("This is outside of the tolerance range V < 25. Something appears to be wrong with the positive DCDC "
1717              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1718        dev.close()
1719        exit()
1720
1721    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1722    if vnow_o < 25:
1723        print("This is within the tolerance range V < 25. Continuing...")
1724    else:
1725        print("This is outside of the tolerance range V < 25. Something appears to be wrong with the output "
1726              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1727        dev.close()
1728        exit()
1729    print("***PASS\n")
1730
1731    print("---Testing Voltage monitoring at set point 20% FS Output off ---")
1732    print("Applying 20% PWM value")
1733    dev.s_vset(int(0.2 * dev.vmax), DCDC_POS)
1734    sleep(0.2)
1735    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1736    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1737    if vnow_p > 900:
1738        print("This is within the tolerance range V > 900. Continuing...")
1739    else:
1740        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1741              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1742        dev.close()
1743        exit()
1744
1745    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1746    if vnow_o < 100:
1747        print("This is within the tolerance range V < 100. Continuing...")
1748    else:
1749        print("This is outside of the tolerance range V < 100. Something appears to be wrong with the output "
1750              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1751        dev.close()
1752        exit()
1753    print("***PASS\n")
1754
1755    print("---Testing Voltage monitoring at set point 20% FS Output LOW ---")
1756    print("Output LOW")
1757    dev.s_sw_mode(SWMODE_LOW)
1758    sleep(0.2)
1759    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1760    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1761    if vnow_p > 900:
1762        print("This is within the tolerance range V > 900. Continuing...")
1763    else:
1764        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1765              "voltage monitoring circuit.\n Test Failed. Exiting script...")
1766        dev.close()
1767        exit()
1768
1769    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1770    if vnow_o < 25:
1771        print("This is within the tolerance range V < 25. Continuing...")
1772    else:
1773        print("This is outside of the tolerance range V < 25. Something appears to be wrong with the output "
1774              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1775        dev.close()
1776        exit()
1777    print("***PASS\n")
1778
1779    print("---Testing Voltage monitoring at set point 20% FS Output HIGH ---")
1780    print("Output HIGH")
1781    dev.s_sw_mode(SWMODE_HIGH)
1782    sleep(0.2)
1783    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1784    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1785    if vnow_p > 900:
1786        print("This is within the tolerance range V > 900. Continuing...")
1787    else:
1788        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1789              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1790        dev.close()
1791        exit()
1792
1793    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1794    if vnow_o > 900:
1795        print("This is within the tolerance range V > 900. Continuing...")
1796    else:
1797        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the output "
1798              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1799        dev.close()
1800        exit()
1801    print("***PASS\n")
1802    print("***hvps-x passes all tests***")
1803
1804    dev.s_vset(0, DCDC_BOTH)
1805    dev.close()

This script can be used after configuration to test the hvps-x functionality. Follow prompt on the console

Parameters
  • dev: an HVPS object
Returns

nothing

def main():
1808def main():
1809    dev = connect_to_hvps(unconf=True)
1810    if dev:
1811        print("Name: {0}".format(dev.q_name()))
1812        print("Vmax: {0} V".format(dev.vmax))
1813        print("\n Your choices:")
1814        print("[1] Initial configuration (to be performed after assembling the low-voltage components)")
1815        print("[2] Basic functionality test (to be performed after assembling the high-voltage components)")
1816        print("[q] quit\n")
1817        answer = input("Your choice: ")
1818        if answer == '1':
1819            configure(dev)
1820        elif answer == '2':
1821            check_functionality(dev)
1822        dev.close(zero=False)
def constrain(val, min_val, max_val):
1824def constrain(val, min_val, max_val):
1825    """A simple implementation to constrain a value between two boundaries"""
1826    return min(max_val, max(min_val, val))

A simple implementation to constrain a value between two boundaries