hvps_lib

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

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. In addition, a few higher-level functions are provided.

HVPS(port, init=True)
212    def __init__(self, port, init=True):
213        """ Initialises a HVPS object: dev = HVPS('/path/to/serial/port').
214
215        Input: COM port to which the HVPS is connected."""
216        self.name = ''
217        self.vmax = 0
218        self.swmode = 0
219        self.vsetp = 0
220        self.vsetn = 0  # negative voltage setpoint (for bipolar configuration)
221        self.vnowp = 0
222        self.vnown = 0
223        self.vnowo = 0
224        self.f = 0
225        self.duty = 0.5
226        self.cycles = 0
227        self.cycle_n = 0  # the current cycle number
228        self.swsrc = 0
229        self.vmode = 0
230        self.bttncfg = 0  # latching behaviour of the button
231        self.err = 0
232        self.stmode = 0  # Strobe mode
233        self.stpos = 0  # Strobe position
234        self.stdur = 0  # strobe duration
235        self.stsweep = 5000  # Strobe sweep time (default when HVPS starts)
236        self.ser = serial.Serial()  # the serial connection to the HVPS
237        self.waveform_pts = []  # list to store the set points of the custom waveform
238        self.waveform_meas = []  # list to store the measured points of the custom waveform.
239        self.lookup_adc_p = []  # lookup tables for linear interpolation
240        self.lookup_adc_o = []
241        self.lookup_v_out = []
242        self.cp = []  # calibration coefficients.
243        self.co = []
244        self.calmeth_p = -1
245        self.calmeth_n = -1
246        self.calmeth_o = -1
247
248        self.firmware = 0
249        self.hardware = 0
250        self.conf = 0  # configuration of the hvps-x
251        self.is_hvpsx = 0
252
253        try:
254            self.ser = serial.Serial(port, 115200, timeout=3)
255        except serial.SerialException:
256            self.err = self.err | ERR_PORT
257            if DEBUG:
258                print("Cannot connect to serial port. Probably busy.")
259        else:
260            self.ser.reset_input_buffer()
261            if DEBUG:
262                text = "connecting to " + port
263                print(text)
264                print("Serial port open")
265            z = self._send_receive_4bytes(SERIAL_QCONF)
266            if z == 0:  # no response: the device connected doesn't talk using this protocol
267                self.err = self.err | ERR_COM
268                if DEBUG:
269                    print("Communication error: the connected device doesn't reply to commands")
270            z_hvps = z & 0xFFFF
271            z_conf = (z & 0xFFFF0000) >> 16
272            if z_hvps == HVPSX_PID:
273                self.is_hvpsx = 1
274                if z_conf != CONF_UNCONF:
275                    self.conf = z_conf
276                else:
277                    self.err = self.err | ERR_CONF  # the hvps-x is not configured
278                    if DEBUG:
279                        print("The hvps is not configured")
280                x = self._send_receive_4bytes(SERIAL_QNAME, param1=0)
281                x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=4) << 32)
282                x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=8) << 64)
283                x = x.to_bytes(12, 'little')
284                z = 0
285                i = 0
286                for char in x:  # find the \0 termination
287                    if char != 0:
288                        z = (z << 8) + char  # shift previous character by 1 byte and add the new character
289                        i = i + 1
290                    else:
291                        break
292                z = z.to_bytes(i, 'big')
293                self.name = z
294            else:
295                self.err = self.err | ERR_TYPE  # The connected device replied with Err! but is not a hvps-x
296                if DEBUG:
297                    print("Type error: the device replies to commands, but is not a hvps-x.")
298            if init and not self.err:
299                self._initialise_hvpsx()

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

Input: COM port to which the HVPS is connected.

name
vmax
swmode
vsetp
vsetn
vnowp
vnown
vnowo
f
duty
cycles
cycle_n
swsrc
vmode
bttncfg
err
stmode
stpos
stdur
stsweep
ser
waveform_pts
waveform_meas
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):
301    def close(self, zero=True):  # closes connection with the HVPS
302        """Closes the connection to the HVPS. Sets voltage to 0 if board was connected
303
304        Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the
305        communication. If False, the hvps-x is left in its current state"""
306
307        if self.ser.is_open:
308            if not (self.err & ERR_COM):  # if connected board is not running the firmware, do not send command
309                if zero:
310                    self.s_vset(0, DCDC_BOTH)  # set the voltage to 0 as a safety measure
311                    self.s_sw_mode(SWMODE_OFF)  # for hvps-x, this will disable both optocouplers.
312            self.ser.close()
313
314        if DEBUG:
315            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):
384    def is_bipolar(self):  # return true if connected device is hvpsx and bipolar unit
385        """ Returns true if the hvps-x configuration is bipolar."""
386        if self.conf & 1:
387            return True
388        else:
389            return False

Returns true if the hvps-x configuration is bipolar.

def s_vset(self, x, polarity=1):
392    def s_vset(self, x, polarity=DCDC_POS):  # sets the output voltage
393        """Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or
394        when the HVPS is powered off. Using the
395        save() command enables to save this parameter in memory\n
396        Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is
397        regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the
398        output to the set point; see s_vmode() command.
399        :param x: voltage set point in volt (int)
400        :param polarity: which DC/DC converter to address (DCDC_POS, DCDC_NEG (bipolar config), DCDC_BOTH)
401        :return: voltage set point accepted by hvps-x
402        """
403        x = constrain(abs(x), 0, self.vmax)
404        x = int(x)
405        if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH:
406            polarity = DCDC_POS
407        z = self._send_receive_4bytes(SERIAL_SVSET, param1=polarity, param2=x)
408        if polarity == DCDC_POS:
409            self.vsetp = z
410        elif polarity == DCDC_NEG:
411            self.vsetn = z
412        else:  # DCDC_Both: i.e. set the positive and negative voltage to the same level
413            self.vsetp = z
414            self.vsetn = z
415        if DEBUG:
416            y = "s_vset(" + str(x) + ") -> " + str(z)
417            print(y)
418        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, DCDC_NEG (bipolar config), DCDC_BOTH)
Returns

voltage set point accepted by hvps-x

def q_vset(self, polarity=1):
420    def q_vset(self, polarity=DCDC_POS):  # queries the voltage setpoint
421        """Queries the voltage set point. The returned value is in volts.
422        :param polarity: DCDC converter to be queried (DCDC_POS or DCDC_NEG). DCDC_NEG will return 0 for
423        unipolar configs
424        :return: Voltage set point in volts
425        """
426        if polarity != DCDC_POS and polarity != DCDC_NEG:
427            polarity = DCDC_POS
428        z = self._send_receive_4bytes(SERIAL_QVSET, param1=polarity)
429        if DEBUG:
430            y = "q_vset -> " + str(z)
431            print(y)
432        if polarity == DCDC_NEG:
433            self.vsetn = z
434        else:
435            self.vsetp = z
436        return z

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

Parameters
  • polarity: DCDC converter to be queried (DCDC_POS or DCDC_NEG). DCDC_NEG will return 0 for unipolar configs
Returns

Voltage set point in volts

def q_vnow(self, which=0):
438    def q_vnow(self, which=VNOW_POS):  # queries the voltage output
439        """Queries the feedback voltage of the HVPS.
440        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
441        positive DCDC converter, negative DCDC converter, or output of the HVPS
442        :return: voltage value in volts
443        """
444
445        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
446            which = VNOW_POS
447        z = self._send_4bytes_receive_nbytes(SERIAL_QVNOW, param1=which, fstring='<2h')
448        z_out = z[1]
449        z_main = z[0]
450        if DEBUG:
451            y = "q_vnow -> " + str(z_main) + " / " + str(z_out)
452            print(y)
453        self.vnowo = z_out
454        if which == VNOW_OUT:
455            self.vnowo = z_main
456        elif which == VNOW_NEG:
457            self.vnown = z_main
458        else:
459            self.vnowp = z_main
460        return z_main

Queries the feedback voltage of the HVPS.

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

voltage value in volts

def q_vnow_raw(self, which=0):
462    def q_vnow_raw(self, which=VNOW_POS):  # queries a raw value of the voltage output
463        """Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids
464        running slow floating point calculations on the
465        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
466        execute than q_vnow(). Useful for streaming voltage values
467        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
468        positive DCDC converter, negative DCDC converter, or output of the HVPS.
469        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
470        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
471        a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a
472        way to do this automatically"""
473
474        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
475            which = VNOW_POS
476        z = self._send_receive_4bytes(SERIAL_QVNOWRAW, param1=which, typ='uint')
477        z_out = z & 0xFFFF
478        z_main = (z & 0xFFFF0000) >> 16
479        if DEBUG:
480            y = "q_vnow_raw -> " + str(z_main) + " , " + str(z_out)
481            print(y)
482        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):
484    def q_vnow_fast(self, which=VNOW_POS):  # queries the voltage output
485        """Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage
486        This avoids running slow floating point calculations on the
487        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
488        execute than q_vnow(). Useful for streaming voltage values.   This method is similar to q_vnow(), except that
489        the conversion from a raw value to a calibrated value is done
490        on the computer rather than on the microcontroller. It can take up to 300us to convert a value on the MUC
491        (it depends on the method used (linear, quadratic, lookup table)
492        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
493        positive DCDC converter, negative DCDC converter, or output of the HVPS.
494        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
495        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
496        a 12 bit value that can be converted to voltage using the unit's calibration values The returned values
497        are calibrated voltages in Volt"""
498
499        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
500            which = VNOW_POS
501        # Download calibration values the first time the function is used
502
503        if self.calmeth_p == -1:  # calibration method not yet fetched from HVPS
504            self.q_calibration_method(which=VNOW_POS)
505        if self.calmeth_n == -1:  # calibration method not yet fetched from HVPS
506            self.q_calibration_method(which=VNOW_NEG)
507        if self.calmeth_o == -1:  # calibration method not yet fetched from HVPS
508            self.q_calibration_method(which=VNOW_OUT)
509
510        if (self.calmeth_o == CALMETH_LOOKUP or self.calmeth_p == CALMETH_LOOKUP) and not self.lookup_v_out:
511            self.lookup_v_out = self.q_lookup(0)
512        if self.calmeth_p == CALMETH_LOOKUP and not self.lookup_adc_p:
513            self.lookup_adc_p = self.q_lookup(1)
514        if self.calmeth_o == CALMETH_LOOKUP and not self.lookup_adc_o:
515            self.lookup_adc_o = self.q_lookup(2)
516
517        if self.calmeth_p == CALMETH_POLYNOMIAL and not self.cp:  # if we don't know what are the calibration values
518            self.cp.append(self.q_cal(CAL_C0P))
519            self.cp.append(self.q_cal(CAL_C1P))
520            self.cp.append(self.q_cal(CAL_C2P))
521
522        if self.calmeth_o == CALMETH_POLYNOMIAL and not self.co:  # if we don't know what are the calibration values
523            self.co.append(self.q_cal(CAL_C0O))
524            self.co.append(self.q_cal(CAL_C1O))
525            self.co.append(self.q_cal(CAL_C2O))
526
527        z_main, z_out = self.q_vnow_raw(which=which)
528        if self.calmeth_o == CALMETH_LOOKUP:
529            v_out = np.interp(z_out, self.lookup_adc_o, self.lookup_v_out, left=None, right=None, period=None)
530        else:
531            v_out = self.co[0] + self.co[1] * z_out + self.co[2] * np.square(z_out)
532
533        if which == VNOW_POS:
534            if self.calmeth_p == CALMETH_LOOKUP:
535                v_main = np.interp(z_main, self.lookup_adc_p, self.lookup_v_out, left=None, right=None, period=None)
536            else:
537                v_main = self.cp[0] + self.cp[1] * z_main + self.cp[2] * np.square(z_main)
538        elif which == VNOW_NEG:
539            v_main = 0
540        else:
541            v_main = v_out
542
543        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 computer 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):
545    def s_f(self, x):  # sets the frequency
546        """Sets the frequency of the signal when the HVPS is in switching mode (SWMODE_SW).\n
547        The value returned is the new frequency, taking quantification into account.
548        :param x: frequency in Hz between 0.001 and 1000
549        :return: frequency accepted by hvps-x in Hz
550        """
551        x = constrain(x, 0.001, 1000.0)
552
553        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
554        # Ex: 10.0: 0x41 0x20 0x00 0x00.
555        # 0 10000010 01000000000000000000000
556        # S=0 Positive
557        # Exp=130-127 (offset)=2^3=8
558        # Fraction=1+2^-2=1.25
559        # number=+1.25*8=10
560        z = 0
561        freq_byte = struct.pack('>f', x)  # transforms the float value to a byte string (4 bytes) big endian
562        freq_byte = int.from_bytes(freq_byte, 'big')
563        for i in range(2):  # send the frequency in 2 groups of 2 bytes
564            y = (freq_byte >> (i * 2 * 8) & 0xFFFF)  # isolate 2 bytes to transmit
565            z = self._send_receive_4bytes(SERIAL_SF, param1=0, param2=y, typ='float')
566
567        if DEBUG:
568            y = "s_f(" + str(x) + ") -> " + str(z)
569            print(y)
570        self.f = z
571        return z

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

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

Parameters
  • x: frequency in Hz between 0.001 and 1000
Returns

frequency accepted by hvps-x in Hz

def q_f(self):
573    def q_f(self):  # queries the frequency
574        """Queries the switching frequency. The returned value is in Hz."""
575        z = self._send_receive_4bytes(SERIAL_QF, param1=0, param2=0, typ='float')
576
577        if DEBUG:
578            y = "q_f -> " + str(z)
579            print(y)
580        self.f = z
581        return z

Queries the switching frequency. The returned value is in Hz.

def s_duty(self, x):
583    def s_duty(self, x):  # sets the duty cycle of the switching signal
584        """Sets the duty cycle of the switching signal
585        :param x: the duty cycle (float in the range 0-1)
586        :return: the current duty cycle (float between 0 and 1)
587        """
588        duty = int(x * 1000)  # hvps-x is coding duty cycle on a 0-1000 scale
589
590        z = self._send_receive_4bytes(SERIAL_SDUTY, param1=0, param2=duty, typ='uint')
591        z = z / 1000
592        if DEBUG:
593            y = "s_duty(" + str(x) + ") -> " + str(z)
594            print(y)
595        self.duty = z
596        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):
598    def q_duty(self):  # queries the duty cycle of the switching signal
599        """queries the duty cycle of the switching signal
600        :return: the current duty cycle (float between 0 and 1)
601        """
602
603        z = self._send_receive_4bytes(SERIAL_QDUTY, param1=0, param2=0, typ='uint')
604        z = float(z) / 1000.0
605        if DEBUG:
606            y = "q_duty() -> " + str(z)
607            print(y)
608        self.duty = z
609        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):
612    def s_sw_mode(self, x):  # sets the switching mode
613        """Sets the switching mode of the hvps-x. \n
614        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
615        This effectively
616        connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter.
617        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and
618        is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage
619        produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW:
620        the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency.
621        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
622        save() command enables to save this parameter in memory
623        :param x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW
624        :return: Switching mode set by the HVPS
625        """
626
627        if x > SWMODE_LOW:
628            x = SWMODE_OFF
629
630        z = self._send_receive_4bytes(SERIAL_SSWMODE, param1=x, param2=0, typ='uint')
631        if DEBUG:
632            y = "s_sw_mode(" + str(x) + ") -> " + str(z)
633            print(y)
634        self.swmode = z
635        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. For unipolar hvps-x, this connects J4 to ground and is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency. 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
Returns

Switching mode set by the HVPS

def q_sw_mode(self):
637    def q_sw_mode(self):  # queries the switching mode
638        """queries the switching mode of the hvps-x. \n
639        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
640        This effectively
641        connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter.
642        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and
643        is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage
644        produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW:
645        the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency.
646        :return: Switching mode set by the HVPS
647        """
648        z = self._send_receive_4bytes(SERIAL_QSWMODE, param1=0, param2=0, typ='uint')
649        if DEBUG:
650            y = "q_sw_mode -> " + str(z)
651            print(y)
652        self.swmode = z
653        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. For unipolar hvps-x, this connects J4 to ground and is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency.

Returns

Switching mode set by the HVPS

def s_sw_src(self, x):
655    def s_sw_src(self, x):  # sets the switching source
656        """Sets the source of the switching signal.
657
658        Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard
659        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
660        Using the save() command enables to save this parameter in memory
661        :param x: SWSRC_TMR, or SWSRC_BTTN
662        :return: SWSRC_TMR, or SWSRC_BTTN
663        """
664        if x > SWSRC_BTTN:
665            x = SWSRC_TMR
666        z = self._send_receive_4bytes(SERIAL_SSWSRC, param1=x, param2=0, typ='uint')
667        if DEBUG:
668            y = "s_sw_src(" + str(x) + ") -> " + str(z)
669            print(y)
670        self.swsrc = z
671        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):
673    def q_sw_src(self):  # queries the switching source
674        """queries the source of the switching signal.
675
676        queries the source of the switching signal. Output values are: SWSRC_TMR for onboard
677        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
678        Using the save() command enables to save this parameter in memory
679        :return: SWSRC_TMR, or SWSRC_BTTN
680        """
681
682        z = self._send_receive_4bytes(SERIAL_QSWSRC, param1=0, param2=0, typ='uint')
683
684        if DEBUG:
685            y = "q_sw_src -> " + str(z)
686            print(y)
687        self.swsrc = z
688        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):
690    def s_bttn_cfg(self, x):  # sets the configuration of the push button
691        """Defines the behaviour of the push button
692
693        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
694        save() command enables to save this parameter in memory
695        :param x: 2-bit value (0x0 to 0x3). bit 0: Defines the behaviour of the push button, when the switching source
696        of the HVPS is set to the push button
697        (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour
698        (i.e. the high voltage is turned on as long as the button is pressed),
699        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
700        to turn it off).\n
701        bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
702        :return: the 2-bit button config value
703        """
704        if x > 3:
705            x = 3
706        z = self._send_receive_4bytes(SERIAL_SBTTNCFG, param1=x, param2=0, typ='uint')
707        if DEBUG:
708            y = "s_bttn_cfg(" + str(x) + ") -> " + str(z)
709            print(y)
710        self.bttncfg = z
711        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):
713    def q_bttn_cfg(self):  # queries the latch mode of the push button
714        """Queries the behaviour of the push button
715
716        :return: the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching
717        source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above).
718        Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed),
719        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
720        to turn it off).\n
721        bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
722        """
723        z = self._send_receive_4bytes(SERIAL_QBTTNCFG, param1=0, param2=0, typ='uint')
724        if DEBUG:
725            y = "q_bttn_cfg -> " + str(z)
726            print(y)
727        self.bttncfg = z
728        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):
730    def q_kill(self):  # queries the state of the kill button. Kill=1 means HV is disabled
731        """Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at
732        the hvps-x output, irrespective of any software setting.
733        :return: 1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise.
734        """
735        z = self._send_receive_4bytes(SERIAL_QKILL, param1=0, param2=0, typ='uint')
736
737        if DEBUG:
738            y = "q_kill -> " + str(z)
739            print(y)
740        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):
742    def s_v_mode(self, x):  # sets the voltage control mode
743        """Sets the voltage control mode
744
745        Sets the voltage control mode (i.e. how is the value of the output voltage controlled):\n
746        VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).\n
747        VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).\n
748        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
749        save() command enables to save this parameter in memory\n
750        VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However,
751        the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output
752        voltage remains within the allowed range
753        :param x: VMODE_R, VMODE_O
754        :return: VMODE_R, VMODE_O
755        """
756
757        if x > VMODE_O:
758            x = VMODE_R
759
760        z = self._send_receive_4bytes(SERIAL_SVMODE, param1=x, param2=0, typ='uint')
761        if DEBUG:
762            y = "s_v_mode(" + str(x) + ") -> " + str(z)
763            print(y)
764        self.vmode = z
765        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):
767    def q_v_mode(self):  # queries the switching source
768        """Queries the voltage control mode
769
770        :return: VMODE_R internal voltage regulator, VMODE_O internal
771        open loop control (on-board regulator disconnected).
772        """
773
774        z = self._send_receive_4bytes(SERIAL_QVMODE, param1=0, param2=0, typ='uint')
775
776        if DEBUG:
777            y = "q_v_mode -> " + str(z)
778            print(y)
779        self.vmode = z
780        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):
783    def s_pid(self, x, pid=PID_KPP):
784        """Sets the gains of the PIDs
785        Use save() command to save the new values to memory
786        :param x: the value (float) of the gain.
787        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O},
788        for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P)
789        :return: the gain value
790        """
791        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
792        # Ex: 10.0: 0x41 0x20 0x00 0x00.
793        # 0 10000010 01000000000000000000000
794        # S=0 Positive
795        # Exp=130-127 (offset)=2^3=8
796        # Fraction=1+2^-2=1.25
797        # number=+1.25*8=10
798        z = 0
799        pid_byte = struct.pack('>f', x)  # transforms the float value to a byte string (4 bytes) big endian
800        pid_byte = int.from_bytes(pid_byte, 'big')
801        for i in range(2):  # send the frequency in 2 groups of 2 bytes
802            y = (pid_byte >> (i * 2 * 8) & 0xFFFF)  # isolate 2 bytes to transmit
803            z = self._send_receive_4bytes(SERIAL_SPID, param1=pid, param2=y, typ='float')
804
805        if DEBUG:
806            y = "s_pid(" + str(x) + "," + str(pid) + ") -> " + str(z)
807            print(y)
808        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,N,O}, for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P)
Returns

the gain value

def q_pid(self, pid=0):
810    def q_pid(self, pid=PID_KPP):  # queries the frequency
811        """returns the gains of the PIDs
812
813        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O},
814        for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P)
815        :return: the gain value of the chosen parameter
816        """
817
818        z = self._send_receive_4bytes(SERIAL_QPID, param1=pid, param2=0, typ='float')
819
820        if DEBUG:
821            y = "q_pid(" + str(pid) + ") -> " + str(z)
822            print(y)
823
824        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,N,O}, for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P)
Returns

the gain value of the chosen parameter

def s_cal(self, x, cal=1):
826    def s_cal(self, x, cal=CAL_C1P):
827        """Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values)
828        Use save() to commit the setting to memory
829        :param x:  the value of the calibration coefficient (float)
830        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive
831        voltage measurement, negative voltage measurement, output voltage measurement
832        :return: the calibration value
833        """
834
835        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
836        # Ex: 10.0: 0x41 0x20 0x00 0x00.
837        # 0 10000010 01000000000000000000000
838        # S=0 Positive
839        # Exp=130-127 (offset)=2^3=8
840        # Fraction=1+2^-2=1.25
841        # number=+1.25*8=10
842        z = 0
843        cal_byte = struct.pack('>f', x)  # transforms the float value to a byte string (4 bytes) big endian
844        cal_byte = int.from_bytes(cal_byte, 'big')
845        for i in range(2):  # send the float in 2 groups of 2 bytes
846            y = (cal_byte >> (i * 2 * 8) & 0xFFFF)  # isolate 2 bytes to transmit
847            z = self._send_receive_4bytes(SERIAL_SCAL, param1=cal, param2=y, typ='float')
848
849        if DEBUG:
850            y = "s_cal(" + str(x) + "," + str(cal) + ") -> " + str(z)
851            print(y)
852        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,N,O}, for positive voltage measurement, negative voltage measurement, output voltage measurement
Returns

the calibration value

def q_cal(self, cal=1):
854    def q_cal(self, cal=CAL_C1P):  # queries the frequency
855        """queries the calibration constants of the analogue input signals (conversion to calibrated voltage values)
856
857        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive
858        voltage measurement, negative voltage measurement, output voltage measurement
859        :return: the calibration value
860        """
861
862        z = self._send_receive_4bytes(SERIAL_QCAL, param1=cal, param2=0, typ='float')
863
864        if DEBUG:
865            y = "q_cal(" + str(cal) + ") -> " + str(z)
866            print(y)
867        self.f = z
868        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,N,O}, for positive voltage measurement, negative voltage measurement, output voltage measurement
Returns

the calibration value

def s_calibration_method(self, which=0, calmeth=0):
870    def s_calibration_method(self, which=VNOW_POS, calmeth=CALMETH_POLYNOMIAL):
871        if not (calmeth == CALMETH_POLYNOMIAL or calmeth == CALMETH_LOOKUP):
872            calmeth = CALMETH_POLYNOMIAL
873        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
874            which = VNOW_POS
875        z = self._send_receive_4bytes(SERIAL_SCALMETH, param1=which, param2=calmeth, typ='uint')
876
877        if DEBUG:
878            y = "s_calibration_method({0}, {1}) -> {2}".format(which, calmeth, z)
879            print(y)
880        if which == VNOW_POS:
881            self.calmeth_p = z
882        elif which == VNOW_OUT:
883            self.calmeth_o = z
884        else:
885            self.calmeth_n = z
def q_calibration_method(self, which=0):
887    def q_calibration_method(self, which=VNOW_POS):
888        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
889            which = VNOW_POS
890        z = self._send_receive_4bytes(SERIAL_QCALMETH, param1=which, param2=0, typ='uint')
891
892        if DEBUG:
893            y = "q_calibration_method({0}) -> {1}".format(which, z)
894            print(y)
895        if which == VNOW_POS:
896            self.calmeth_p = z
897        elif which == VNOW_NEG:
898            self.calmeth_n = z
899        else:
900            self.calmeth_o = z
def s_lookup(self, x, n, table=1):
902    def s_lookup(self, x, n, table=LOOKUP_ADC_P):
903        if n > 20:
904            n = 20
905        n = n << 2  # shift left 2 bits
906        if table == LOOKUP_ADC_P:
907            n = n | 0b01
908        elif table == LOOKUP_ADC_O:
909            n = n | 0b10
910        x = int(x)
911        z = self._send_receive_4bytes(SERIAL_SLKUP, param1=n, param2=x, typ='int16')
912        if DEBUG:
913            y = "s_lookup({0},{1},{2}) - > {3}".format(x, n, table, z)
914            print(y)
def q_lookup(self, which):
916    def q_lookup(self, which):  # which=0: vout, which=1: ADC_p, which=2: ADC_o
917        format_string = f'<21h'
918        z = self._send_4bytes_receive_nbytes(SERIAL_QLKUP, param1=which, param2=0, fstring=format_string)
919        lookup_list = list(z)
920        if DEBUG:
921            y = "q_lookup({0}) - > {1}".format(which, lookup_list)
922            print(y)
923        return lookup_list
def save(self):
1094    def save(self):  # save current HVPS parameters into the memory
1095        """save active HVPS parameters into the memory
1096
1097        This command saves the active parameters as \'current\' settings. Current setting are the settings that are
1098        loaded when power is applied to the hvpsx
1099
1100        :return: SERIAL_OK or SERIAL_ERROR
1101        """
1102
1103        z = self._send_receive_4bytes(SERIAL_SAVE)
1104
1105        if DEBUG:
1106            y = "save -> " + str(z)
1107            print(y)
1108        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 hvpsx

Returns

SERIAL_OK or SERIAL_ERROR

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

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

nothing

def s_settings(self, settings=0):
1132    def s_settings(self,
1133                   settings=SETTINGS_CURRENT):  # sets the active setting to a particular type (useful before saving)
1134        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_FACTORY or settings == SETTINGS_BACKUP):
1135            settings = SETTINGS_CURRENT
1136        x = self._send_receive_4bytes(SERIAL_SST, param1=settings)
1137        if DEBUG:
1138            print('s_settings({0}) -> {1}'.format(settings, x))
1139        return x
def load_settings(self, settings=0):
1141    def load_settings(self, settings=SETTINGS_CURRENT):  # Load a setting set from memory to the active setting
1142        """This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x
1143
1144        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
1145        :return:
1146        """
1147
1148        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_BACKUP):
1149            settings = SETTINGS_CURRENT
1150        x = self._send_receive_4bytes(SERIAL_LST, param1=settings)
1151        if DEBUG:
1152            print('load_settings({0}) -> {1}'.format(settings, x))
1153
1154        self._initialise_hvpsx()  # need to reread all parameters
1155
1156        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):
1158    def transfer_settings(self, dict_settings):
1159        """ transfer a setting dictionary from the computer to the hvps-x memory
1160        :param dict_settings: a dictionary containing the settings values.
1161        :return: SERIAL_OK or SERIAL_ERROR
1162        The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together
1163        there two functions make it possible to backup the settings (this includes calibration and PID settings in a
1164        file, and gives the opportunity to restore the settings"""
1165
1166        error = False
1167        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1168        siz = struct.calcsize(memory_string[fw_string])
1169        buffer = ctypes.create_string_buffer(siz)
1170        memmap = memorymap[fw_string]
1171        counter = 0
1172        for x in memmap:
1173            n = x[0]
1174            key = x[1]
1175            data_format = x[2]
1176            if data_format == 'B' or data_format == 's':
1177                bytelength = 1
1178            elif data_format == 'h' or data_format == 'H':
1179                bytelength = 2
1180            elif data_format == 'f':
1181                bytelength = 4
1182            else:
1183                bytelength = 1
1184            data = dict_settings[key]
1185            if key == 'Name':
1186                data = bytes(data, 'utf-8')
1187                if len(data) > 12:
1188                    error = True
1189                    if DEBUG:
1190                        print("Error: name should not exceed 12 characters")
1191                else:
1192                    format_str = '<{0}{1}'.format(n, data_format)
1193                    struct.pack_into(format_str, buffer, counter, data)
1194            else:
1195                format_str = '<{0}'.format(data_format)
1196                if n == 1:
1197                    struct.pack_into(format_str, buffer, counter, data)
1198                else:
1199                    for i in range(n):
1200                        try:
1201                            struct.pack_into(format_str, buffer, counter + i * bytelength, data[i])
1202                        except IndexError:
1203                            error = True
1204                            if DEBUG:
1205                                print('setting dictionary does not fit the expected format')
1206
1207            counter = counter + n * bytelength
1208        data_fw = 'Fw-{0}.{1}'.format(dict_settings['fw_major'], dict_settings['fw_minor'])
1209        if data_fw != fw_string:
1210            error = True
1211            if DEBUG:
1212                print('Error: JSON file firmware version does not match firware currently on hvps-x. Exiting')
1213            exit(0)
1214        if not error:
1215            buffer = b'xxx' + buffer    # Adds 3 random bytes. CMD + 3 random bytes means that when mapping the transfer
1216            # buffer to a structure, it will on an aligned memory address
1217            x = self._send_nbytes_receive_4bytes(SERIAL_XFERST, buffer)
1218        else:
1219            x = SERIAL_ERROR
1220
1221        if DEBUG:
1222            print("transfer_settings(...) -> {0}".format(x))
1223        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 there 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):
1225    def copy_settings(self, src, dst):
1226        """
1227        :param src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS)
1228        :param dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by
1229        the source
1230        :return: nothing
1231        Copies one set of settings to another location:\n
1232        Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings
1233        (for example if some temporary settings were saved as current settings, for example to experiment with new PID
1234        gain values)\
1235        Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new
1236        calibration as a set of back-up settings"""
1237        if (src == SETTINGS_CURRENT or src == SETTINGS_BACKUP) and (dst == SETTINGS_CURRENT or dst == SETTINGS_BACKUP):
1238            x = self._send_receive_4bytes(SERIAL_CPYST, param1=src, param2=dst)
1239        else:
1240            x = SERIAL_ERROR
1241
1242        if DEBUG:
1243            print('copy_settings({0},{1}) -> {2}'.format(src, dst, x))
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

nothing 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

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

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

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

def q_conf(self):
1291    def q_conf(self):  # queries the configuration
1292        """returns the configuration of the board.
1293        :return:
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
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 < 11:
1355            x = bytearray(x, 'utf-8')
1356            for i in range(12 - ll):  # pad the string with 0s
1357                x = x + b'\0'
1358            x = int.from_bytes(x, 'little')
1359            for i in range(6):  # send the name by groups of 2 bytes
1360                y = (x >> (i * 2 * 8) & 0xFFFF)  # isolate 2 bytes to transmit
1361                self._send_receive_4bytes(SERIAL_SNAME, 2 * i, y)
1362            z = self.q_name()
1363        else:
1364            z = 'too long'
1365        if DEBUG:
1366            y = "s_name(" + str(x) + ") -> " + str(z)
1367            print(y)
1368        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):
1370    def q_name(self):  # queries the name of the board
1371        """queries the name of the board
1372        :return: Name of the board
1373        """
1374
1375        x = self._send_receive_4bytes(SERIAL_QNAME, param1=0)
1376        x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=4) << 32)
1377        x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=8) << 64)
1378        x = x.to_bytes(12, 'little')
1379        z = 0
1380        i = 0
1381        for char in x:  # find the \0 termination
1382            if char != 0:
1383                z = (z << 8) + char  # shift previous character by 1 byte and add the new character
1384                i = i + 1
1385            else:
1386                break
1387        z = z.to_bytes(i, 'big')
1388
1389        self.name = z
1390        if DEBUG:
1391            y = "q_name -> " + str(z)
1392            print(y)
1393        return z

queries the name of the board

Returns

Name of the board

def set_hardware_version(self, hw_major, hw_minor):
1395    def set_hardware_version(self, hw_major, hw_minor):
1396        param = (hw_major << 8) + hw_minor
1397        self._send_receive_4bytes(SERIAL_SHW, param2=param)
def check_version_compatibility(self):
1399    def check_version_compatibility(self):
1400        self.q_ver()
1401        lib_string = 'Lib-' + LIB_VER
1402        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1403        hw_string = 'Hw-{0:.1f}'.format(self.hardware)
1404
1405        list_compatible_fw = compatibility_dict[lib_string]
1406        if fw_string in list_compatible_fw:
1407            list_compatible_hw = compatibility_dict[fw_string]
1408            if hw_string not in list_compatible_hw:
1409                self.err |= ERR_FIRM
1410        else:
1411            self.err |= ERR_FIRM
def list_hvps(list_all=False):
1414def list_hvps(list_all=False):
1415    """lists the hvps-x connected to PC in a dictionary.The key is the name of the HVPS, and the parameter is
1416    the associated serial port
1417    :param list_all: if True list all connected HVPS.
1418    if list_all is false, it will only list device that are configured. Using True enable to list unconfigured
1419    devices and give the opportunity to configure them
1420    :return: dictionary of connected hvps-x"""
1421
1422    hvpsx_ports = [  # creates a list with all of the serial adapters connected to the computer
1423        p.device
1424        for p in serial.tools.list_ports.comports()
1425        if ('Serial' in p.description or 'STLink' in p.description or 'STM32' in p.description)
1426    ]
1427    dict_hvps = {}
1428    for port in hvpsx_ports:
1429        dev = HVPS(port, init=False)  # Attempts to connect to HVPS but without initialisation
1430        if dev.is_hvpsx:
1431            if dev.conf != CONF_UNCONF or list_all:
1432                dict_hvps[dev.name.decode()] = port  # add an entry in the dictionary with the name of the
1433            # HVPS and the port
1434        dev.close()
1435
1436    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):
1439def connect_to_hvps(unconf=False):
1440    """Scan for connected hvps-x and connects to it if a single hvps-x is found. User can chose to which hvps-x to
1441    connect if more than one is detected.
1442    :param unconf: True/False. If true will connect to an unconfigured hvps-x. If False (default) only connects to a
1443    configured device
1444    :return: a HVPS object (or None if could not connect to a valid hvps-x
1445    If more than 1 hvps-x is connected to the computed, a list of names is displayed and the user must choose which one
1446    they want to connect to"""
1447    cont = False  # flag to decide whether to continue the programme or close the COM port
1448    dev = None
1449
1450    ports = list_hvps(list_all=unconf)
1451    keys = list(ports.keys())
1452    if len(ports) == 0:
1453        print("No HVPS connected to the device. Terminating programme")
1454        exit()
1455    elif len(ports) == 1:
1456        dev = HVPS(ports[keys[0]])  # connects to the only available HVPS
1457    else:
1458        print("List of connected HVPS:")
1459        for i in range(len(ports)):
1460            print(str(i) + ": " + keys[i])
1461        print("Enter the number of the board you want to connect to")
1462        connect_to = 0
1463        try:
1464            connect_to = int(input())
1465        except ValueError:
1466            print("Invalid entry. Terminating programme")
1467            exit()
1468        if connect_to >= len(ports):
1469            print("Invalid entry. Terminating programme")
1470            exit()
1471        else:
1472            dev = HVPS(ports[keys[connect_to]])
1473    error = dev.err
1474    if error & ERR_PORT:
1475        print("Cannot open COM port. Probably busy with another process. Terminating Programme")
1476    if error & ERR_COM:
1477        print("Cannot communicate with hvps-x. Has firmware been flashed?. Terminating programme")
1478    if error & ERR_TYPE:
1479        print("Device connected this port is not recognised. Terminating programme")
1480    if error & ERR_CONF:
1481        print("This hvps-x is not configured. Please configure unit before using it. "
1482              "Terminating programme.")
1483        cont = True
1484    elif error & ERR_FIRM:
1485        print("Warning: your hvps-x library is not optimal for the firmware or the firmware is not optimal for your "
1486              "hardware. Refer to the website for a compatibility table")
1487        cont = True
1488    if error == 0:
1489        cont = True
1490    if cont:
1491        return dev
1492    else:
1493        return None

Scan for connected hvps-x and connects to it if a single hvps-x is found. User can chose 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):
1496def configure(dev):
1497    """Script to use to for the initial configuration (or re-configuration) of an hvps-x. Follow prompt on the console.
1498    :param dev: an HVPS object
1499    :return: nothing
1500    """
1501    if dev != -1:
1502        config = configparser.RawConfigParser()
1503        config.read('config.ini')
1504
1505        if dev.conf != CONF_UNCONF:
1506            answer = input("It appears this hvps-x is already configured. (C)ontinue with configuration or (A)bort?")
1507            if answer == 'a' or answer == 'A':
1508                print("exiting configuration")
1509                dev.close()
1510                exit(-1)
1511        answer = input("Is the hvps-x (U)nipolar or (B)ipolar? ")
1512        print("Setting configuration")
1513        if answer == 'B' or answer == 'b':
1514            dev.s_conf(bipolar=True)
1515        else:
1516            dev.s_conf(bipolar=False)
1517        answer = input("Enter the version of your hvps-x PCB (hardware version), as printed on the PCB "
1518                       "(format: X.Y; e.g. 1.2): ")
1519        hw_list = answer.split('.')
1520        if len(hw_list) != 2:
1521            print("Error, the format of the hardware string must be X.Y, e.g. 1.2. Exiting")
1522            exit(0)
1523        dev.set_hardware_version(int(hw_list[0]), int(hw_list[1]))
1524        print("The current name of the hvps-x is " + dev.q_name().decode("utf-8"))
1525        answer = input("Do you want to set/change the name of this hvps-x? (Y)/(N)?")
1526        if answer == 'y' or answer == 'Y':
1527            name = input("Enter hvps-x name (12 char max): ")
1528            if len(name) > 12:
1529                print("Name too long. Ignoring name change")
1530            else:
1531                dev.s_name(name)
1532        else:
1533            print("Leaving name unchanged")
1534        print("\nThe current maximal voltage rating of this HVPS is " + str(dev.q_vmax()) + " V")
1535        answer = input("Do you want to set the maximal voltage of the HVPS? (Y)/(N)")
1536        if answer == 'y' or answer == 'Y':
1537            answer = input("Enter the maximal voltage of the hvps-x in Volt. It must match the voltage rating of the "
1538                           "Emco DC/DC converter:")
1539            print("Setting Vmax to " + answer + "V")
1540            dev.s_vmax(int(answer))
1541        else:
1542            print("Leaving Vmax unchanged")
1543        vmax = dev.q_vmax()
1544        conf_section = 'hvps-x-' + str(vmax)
1545        if config.has_section(conf_section):
1546            print("Default values for this voltage found in configuration file\n")
1547            answer = input("Do you want to replace the values stored in the hvps-x by the one from the config file "
1548                           "(Y/N) (choose Y if configuring a new hvps-x)")
1549            if answer == 'Y' or answer == 'y':
1550                c0p = config.getfloat(conf_section, 'C0P')
1551                c1p = config.getfloat(conf_section, 'C1P')
1552                c2p = config.getfloat(conf_section, 'C2P')
1553                c0o = config.getfloat(conf_section, 'C0O')
1554                c1o = config.getfloat(conf_section, 'C1O')
1555                c2o = config.getfloat(conf_section, 'C2O')
1556
1557                dev.s_cal(c0p, CAL_C0P)
1558                dev.s_cal(c1p, CAL_C1P)
1559                dev.s_cal(c2p, CAL_C2P)
1560                dev.s_cal(c0o, CAL_C0O)
1561                dev.s_cal(c1o, CAL_C1O)
1562                dev.s_cal(c2o, CAL_C2O)
1563                print("Voltage calibration values set...\n")
1564            answer = input(
1565                "Reset PID values to their default values? (Y)/(N) (choose (Y) when configuring a board for the first "
1566                "time)")
1567            if answer == 'Y' or answer == 'y':
1568                kpp = config.getfloat(conf_section, 'KPP')
1569                kip = config.getfloat(conf_section, 'KIP')
1570                kdp = config.getfloat(conf_section, 'KDP')
1571
1572                dev.s_pid(kpp, PID_KPP)
1573                dev.s_pid(kip, PID_KIP)
1574                dev.s_pid(kdp, PID_KDP)
1575                print("PID values set...\n")
1576
1577            print("hvps-x configured. It is recommended to perform a calibration of the voltage readout circuit!")
1578            print("Saving information to hvps-x memory. Backup-settings and default settings")
1579            dev.s_settings(SETTINGS_BACKUP)
1580            dev.save()
1581            dev.s_settings(SETTINGS_CURRENT)
1582            dev.save()
1583        else:
1584            print("Cannot find a section in the config file for a {0} V hvps-x. You need to enter values for voltage "
1585                  "calibration and PID manually ")

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):
1588def check_functionality(dev):
1589    """This script can be used after configuration to test the hvps-x functionality. Follow prompt on the console
1590
1591    :param dev: an HVPS object
1592    :return: nothing
1593    """
1594    if not dev.q_kill():
1595        input("Place HV safety switch (S1) on position 0 and press any key.")
1596        if not dev.q_kill():
1597            input("Switch S1 appears not to be working. Check Switch S1 functionality and restart the test")
1598            dev.close()
1599            exit()
1600    print("During the course of this test, a moderate (~20% of full scale) voltage will be applied to the output of\n "
1601          "the hvps-x. Make sure that it nothing is connected to the output and that you are not touching the\n "
1602          "instrument\n")
1603    print("Voltage set point 0V. Output off")
1604    dev.s_vset(0, DCDC_BOTH)
1605    dev.s_sw_mode(SWMODE_OFF)
1606    dev.s_v_mode(VMODE_O)
1607    dev.s_sw_src(SWSRC_TMR)
1608
1609    print("---Testing HV enable switch (S1) ---")
1610    input("Place HV enable switch (S1) on position 1 and press any key")
1611    if dev.q_kill():
1612        print("S1 switch still reads off state. Check functionality of switch S1.\n Test FAILED. Exiting script...")
1613        dev.close()
1614        exit()
1615    else:
1616        print("***PASS\n")
1617
1618    print("---Testing Voltage monitoring at set point 0 ---")
1619    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1620    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1621    if vnow_p < 5:
1622        print("This is within the tolerance range V < 5. Continuing...")
1623
1624    else:
1625        print("This is outside of the tolerance range V < 5. Something appears to be wrong with the positive DCDC "
1626              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1627        dev.close()
1628        exit()
1629
1630    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1631    if 2045 < vnow_o < 2051:
1632        print("This is within the tolerance range 2045 < V < 2051. Continuing...")
1633    else:
1634        print("This is outside of the tolerance range 2045 < V < 2051. Something appears to be wrong with the output "
1635              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1636        dev.close()
1637        exit()
1638    print("***PASS\n")
1639
1640    print("---Testing Voltage monitoring at set point 20% FS Output off ---")
1641    print("Applying 20% PWM value")
1642    dev.s_vset(int(0.2 * dev.vmax), DCDC_POS)
1643    sleep(0.2)
1644    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1645    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1646    if vnow_p > 900:
1647        print("This is within the tolerance range V > 900. Continuing...")
1648    else:
1649        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1650              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1651        dev.close()
1652        exit()
1653
1654    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1655    if 2036 < vnow_o < 2070:
1656        print("This is within the tolerance range 2036 < V < 2070. Continuing...")
1657    else:
1658        print("This is outside of the tolerance range 2036 < V < 2070. Something appears to be wrong with the output "
1659              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1660        dev.close()
1661        exit()
1662    print("***PASS\n")
1663
1664    print("---Testing Voltage monitoring at set point 20% FS Output LOW ---")
1665    print("Output LOW")
1666    dev.s_sw_mode(SWMODE_LOW)
1667    sleep(0.2)
1668    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1669    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1670    if vnow_p > 900:
1671        print("This is within the tolerance range V > 900. Continuing...")
1672    else:
1673        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1674              "voltage monitoring circuit.\n Test Failed. Exiting script...")
1675        dev.close()
1676        exit()
1677
1678    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1679    if 2036 < vnow_o < 2070:
1680        print("This is within the tolerance range 2036 < V < 2070. Continuing...")
1681    else:
1682        print("This is outside of the tolerance range 2036 < V < 2070. Something appears to be wrong with the output "
1683              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1684        dev.close()
1685        exit()
1686    print("***PASS\n")
1687
1688    print("---Testing Voltage monitoring at set point 20% FS Output HIGH ---")
1689    print("Output HIGH")
1690    dev.s_sw_mode(SWMODE_HIGH)
1691    sleep(0.2)
1692    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1693    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1694    if vnow_p > 900:
1695        print("This is within the tolerance range V > 900. Continuing...")
1696    else:
1697        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1698              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1699        dev.close()
1700        exit()
1701
1702    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1703    if vnow_o > 2400:
1704        print("This is within the tolerance range 2400 < V. Continuing...")
1705    else:
1706        print("This is outside of the tolerance range 2400 < V. Something appears to be wrong with the output "
1707              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1708        dev.close()
1709        exit()
1710    print("***PASS\n")
1711    print("***hvps-x passes all tests***")
1712
1713    dev.s_vset(0, DCDC_BOTH)
1714    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 transfer_file_to_memory(dev: hvps_lib.HVPS):
1717def transfer_file_to_memory(dev: HVPS):
1718    """Displays a list of json file in the interface folder and gives the user the option to transfer one to the hvps-x
1719    memory
1720    :param dev: an HVPS object
1721    :return: nothing
1722    """
1723    json_files = list_json_files()
1724    if len(json_files) > 0 and dev != -1:
1725        selected_file = select_file(json_files)
1726        with open(selected_file) as file:
1727            data_dict = json.load(file)
1728            dev.transfer_settings(data_dict)
1729    else:
1730        print("No .json files in the interface root folder")

Displays a list of json file in the interface folder and gives the user the option to transfer one to the hvps-x memory

Parameters
  • dev: an HVPS object
Returns

nothing

def list_json_files():
1733def list_json_files():
1734    json_files = []
1735    for file in os.listdir('.'):
1736        if file.endswith('.json'):
1737            json_files.append(file)
1738    return json_files
def select_file(files):
1741def select_file(files):
1742    print("Available Files:")
1743    for i, file in enumerate(files):
1744        print(f"{i + 1}. {file}")
1745    while True:
1746        try:
1747            choice = int(input("Enter the number corresponding to the file you want to open: "))
1748            if 1 <= choice <= len(files):
1749                return files[choice - 1]
1750            else:
1751                print("Invalid choice. Please enter a valid number.")
1752        except ValueError:
1753            print("Invalid choice. Please enter a valid number.")
def main():
1756def main():
1757    dev = connect_to_hvps(unconf=True)
1758    if dev:
1759        print("Name: {0}".format(dev.q_name()))
1760        print("Vmax: {0} V".format(dev.vmax))
1761        print("\n Your choices:")
1762        print("[1] Initial configuration (to be performed after assembling the low-voltage components)")
1763        print("[2] Basic functionality test (to be performed after assembling the high-voltage components)")
1764        print("[3] Transfer a json file to memory")
1765        print("[q] quit\n")
1766        answer = input("Your choice: ")
1767        if answer == '1':
1768            configure(dev)
1769        elif answer == '2':
1770            check_functionality(dev)
1771        elif answer == '3':
1772            transfer_file_to_memory(dev)
1773        dev.close()
def constrain(val, min_val, max_val):
1776def constrain(val, min_val, max_val):
1777    """A simple implementation to constrain a value between two boundaries"""
1778    return min(max_val, max(min_val, val))

A simple implementation to constrain a value between two boundaries