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

Class to control a petapicovoltron hvps-x

The class implements the low-level functions to interface the hvps-x firmware for easy communication with the hvps-x. In addition, a few higher-level functions are provided.

HVPS(port, init=True)
232    def __init__(self, port, init=True):
233        """ Initialises a HVPS object: dev = HVPS('/path/to/serial/port').
234
235        Input: COM port to which the HVPS is connected."""
236        self.name = ''
237        self.vmax = 0
238        self.swmode = 0
239        self.vsetp = 0
240        self.vsetn = 0  # negative voltage setpoint (for bipolar configuration)
241        self.vnowp = 0
242        self.vnown = 0
243        self.vnowo = 0
244        self.f = 0
245        self.duty = 0.5
246        self.cycles = 0
247        self.cycle_n = 0  # the current cycle number
248        self.swsrc = 0
249        self.vmode = 0
250        self.bttncfg = 0  # latching behaviour of the button
251        self.err = 0
252        self.stmode = 0  # Strobe mode
253        self.stpos = 0  # Strobe position
254        self.stdur = 0  # strobe duration
255        self.stsweep = 5000  # Strobe sweep time (default when HVPS starts)
256        self.ser = serial.Serial()  # the serial connection to the HVPS
257        self.waveform_pts = []  # list to store the set points of the custom waveform
258        self.waveform_meas = []  # list to store the measured points of the custom waveform.
259        self.lookup_adc_p = []  # lookup tables for linear interpolation
260        self.lookup_adc_o = []
261        self.lookup_v_out = []
262        self.cp = []  # calibration coefficients.
263        self.co = []
264        self.calmeth_p = -1
265        self.calmeth_n = -1
266        self.calmeth_o = -1
267
268        self.firmware = 0
269        self.hardware = 0
270        self.conf = 0  # configuration of the hvps-x
271        self.is_hvpsx = 0
272
273        try:
274            self.ser = serial.Serial(port, 115200, timeout=3)
275        except serial.SerialException:
276            self.err = self.err | ERR_PORT
277            if DEBUG:
278                print("Cannot connect to serial port. Probably busy.")
279        else:
280            self.ser.reset_input_buffer()
281            if DEBUG:
282                text = "connecting to " + port
283                print(text)
284                print("Serial port open")
285            z = self._send_receive_4bytes(SERIAL_QCONF)
286            if z == 0:  # no response: the device connected doesn't talk using this protocol
287                self.err = self.err | ERR_COM
288                if DEBUG:
289                    print("Communication error: the connected device doesn't reply to commands")
290            z_hvps = z & 0xFFFF
291            z_conf = (z & 0xFFFF0000) >> 16
292            if z_hvps == HVPSX_PID:
293                self.is_hvpsx = 1
294                if z_conf != CONF_UNCONF:
295                    self.conf = z_conf
296                else:
297                    self.err = self.err | ERR_CONF  # the hvps-x is not configured
298                    if DEBUG:
299                        print("The hvps is not configured")
300                z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s')
301                z = z[0].split(b'\x00', 1)[0]
302                self.name = z
303            else:
304                self.err = self.err | ERR_TYPE  # The connected device replied with Err! but is not a hvps-x
305                if DEBUG:
306                    print("Type error: the device replies to commands, but is not a hvps-x.")
307            if init and not self.err:
308                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):
310    def close(self, zero=True):  # closes connection with the HVPS
311        """Closes the connection to the HVPS. Sets voltage to 0 if board was connected
312
313        Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the
314        communication. If False, the hvps-x is left in its current state"""
315
316        if self.ser.is_open:
317            if not (self.err & ERR_COM):  # if connected board is not running the firmware, do not send command
318                if zero:
319                    self.s_vset(0, DCDC_BOTH)  # set the voltage to 0 as a safety measure
320                    self.s_sw_mode(SWMODE_OFF)  # for hvps-x, this will disable both optocouplers.
321            self.ser.close()
322
323        if DEBUG:
324            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 convert_raw_to_voltage(self, vraw_main, vraw_out, which):
394    def convert_raw_to_voltage(self, vraw_main, vraw_out, which):
395        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
396            which = VNOW_POS
397        # Download calibration values the first time the function is used
398
399        if self.calmeth_p == -1:  # calibration method not yet fetched from HVPS
400            self.q_calibration_method(which=VNOW_POS)
401        if self.calmeth_n == -1:  # calibration method not yet fetched from HVPS
402            self.q_calibration_method(which=VNOW_NEG)
403        if self.calmeth_o == -1:  # calibration method not yet fetched from HVPS
404            self.q_calibration_method(which=VNOW_OUT)
405
406        if (self.calmeth_o == CALMETH_LOOKUP or self.calmeth_p == CALMETH_LOOKUP) and not self.lookup_v_out:
407            self.lookup_v_out = self.q_lookup(0)
408        if self.calmeth_p == CALMETH_LOOKUP and not self.lookup_adc_p:
409            self.lookup_adc_p = self.q_lookup(1)
410        if self.calmeth_o == CALMETH_LOOKUP and not self.lookup_adc_o:
411            self.lookup_adc_o = self.q_lookup(2)
412
413        if self.calmeth_p == CALMETH_POLYNOMIAL and not self.cp:  # if we don't know what are the calibration values
414            self.cp.append(self.q_cal(CAL_C0P))
415            self.cp.append(self.q_cal(CAL_C1P))
416            self.cp.append(self.q_cal(CAL_C2P))
417
418        if self.calmeth_o == CALMETH_POLYNOMIAL and not self.co:  # if we don't know what are the calibration values
419            self.co.append(self.q_cal(CAL_C0O))
420            self.co.append(self.q_cal(CAL_C1O))
421            self.co.append(self.q_cal(CAL_C2O))
422
423        if self.calmeth_o == CALMETH_LOOKUP:
424            v_out = np.interp(vraw_out, self.lookup_adc_o, self.lookup_v_out, left=None, right=None, period=None)
425        else:
426            v_out = self.co[0] + self.co[1] * vraw_out + self.co[2] * np.square(vraw_out)
427
428        if which == VNOW_POS:
429            if self.calmeth_p == CALMETH_LOOKUP:
430                v_main = np.interp(vraw_main, self.lookup_adc_p, self.lookup_v_out, left=None, right=None, period=None)
431            else:
432                v_main = self.cp[0] + self.cp[1] * vraw_main + self.cp[2] * np.square(vraw_main)
433        elif which == VNOW_NEG:
434            v_main = 0
435        else:
436            v_main = v_out
437        return v_main, v_out
def is_bipolar(self):
439    def is_bipolar(self):  # return true if connected device is hvpsx and bipolar unit
440        """ Returns true if the hvps-x configuration is bipolar."""
441        if self.conf & 1:
442            return True
443        else:
444            return False

Returns true if the hvps-x configuration is bipolar.

def s_vset(self, x, polarity=1):
447    def s_vset(self, x, polarity=DCDC_POS):  # sets the output voltage
448        """Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or
449        when the HVPS is powered off. Using the
450        save() command enables to save this parameter in memory\n
451        Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is
452        regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the
453        output to the set point; see s_vmode() command.
454        :param x: voltage set point in volt (int)
455        :param polarity: which DC/DC converter to address (DCDC_POS, DCDC_NEG (bipolar config), DCDC_BOTH)
456        :return: voltage set point accepted by hvps-x
457        """
458        x = constrain(abs(x), 0, self.vmax)
459        x = int(x)
460        if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH:
461            polarity = DCDC_POS
462        z = self._send_receive_4bytes(SERIAL_SVSET, param1=polarity, param2=x)
463        if x == 0:
464            z = 0   # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in
465            # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a
466            # few 10s of volts when back calculated in volts
467        if polarity == DCDC_POS:
468            self.vsetp = z
469        elif polarity == DCDC_NEG:
470            self.vsetn = z
471        else:  # DCDC_Both: i.e. set the positive and negative voltage to the same level
472            self.vsetp = z
473            self.vsetn = z
474        if DEBUG:
475            y = "s_vset(" + str(x) + ") -> " + str(z)
476            print(y)
477        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):
479    def q_vset(self, polarity=DCDC_POS):  # queries the voltage setpoint
480        """Queries the voltage set point. The returned value is in volts.
481        :param polarity: DCDC converter to be queried (DCDC_POS or DCDC_NEG). DCDC_NEG will return 0 for
482        unipolar configs
483        :return: Voltage set point in volts
484        """
485        if polarity != DCDC_POS and polarity != DCDC_NEG:
486            polarity = DCDC_POS
487        z = self._send_receive_4bytes(SERIAL_QVSET, param1=polarity)
488        if DEBUG:
489            y = "q_vset -> " + str(z)
490            print(y)
491        if polarity == DCDC_NEG:
492            self.vsetn = z
493        else:
494            self.vsetp = z
495        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):
497    def q_vnow(self, which=VNOW_POS):  # queries the voltage output
498        """Queries the feedback voltage of the HVPS.
499        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
500        positive DCDC converter, negative DCDC converter, or output of the HVPS
501        :return: voltage value in volts
502        """
503
504        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
505            which = VNOW_POS
506        z = self._send_4bytes_receive_nbytes(SERIAL_QVNOW, param1=which, fstring='<2h')
507        z_out = z[1]
508        z_main = z[0]
509        if DEBUG:
510            y = "q_vnow -> " + str(z_main) + " / " + str(z_out)
511            print(y)
512        self.vnowo = z_out
513        if which == VNOW_OUT:
514            self.vnowo = z_main
515        elif which == VNOW_NEG:
516            self.vnown = z_main
517        else:
518            self.vnowp = z_main
519        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):
521    def q_vnow_raw(self, which=VNOW_POS):  # queries a raw value of the voltage output
522        """Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids
523        running slow floating point calculations on the
524        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
525        execute than q_vnow(). Useful for streaming voltage values
526        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
527        positive DCDC converter, negative DCDC converter, or output of the HVPS.
528        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
529        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
530        a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a
531        way to do this automatically"""
532
533        if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT:
534            which = VNOW_POS
535        z = self._send_receive_4bytes(SERIAL_QVNOWRAW, param1=which, typ='uint')
536        z_out = z & 0xFFFF
537        z_main = (z & 0xFFFF0000) >> 16
538        if DEBUG:
539            y = "q_vnow_raw -> " + str(z_main) + " , " + str(z_out)
540            print(y)
541        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):
543    def q_vnow_fast(self, which=VNOW_POS):  # queries the voltage output
544        """Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage
545        This avoids running slow floating point calculations on the
546        microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to
547        execute than q_vnow(). Useful for streaming voltage values.   This method is similar to q_vnow(), except that
548        the conversion from a raw value to a calibrated value is done
549        on the computer rather than on the microcontroller. It can take up to 300us to convert a value on the MUC
550        (it depends on the method used (linear, quadratic, lookup table)
551        :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the
552        positive DCDC converter, negative DCDC converter, or output of the HVPS.
553        :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT
554        (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is
555        a 12 bit value that can be converted to voltage using the unit's calibration values The returned values
556        are calibrated voltages in Volt"""
557
558        z_main, z_out = self.q_vnow_raw(which=which)
559        v_main, v_out = self.convert_raw_to_voltage(z_main, z_out, which=which)
560
561        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):
563    def s_f(self, x):  # sets the frequency
564        """Sets the frequency of the signal when the HVPS is in switching mode (SWMODE_SW).\n
565        The value returned is the new frequency, taking quantification into account.
566        :param x: frequency in Hz between 0.001 and 1000
567        :return: frequency accepted by hvps-x in Hz
568        """
569        x = constrain(x, 0.001, 1000.0)
570
571        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
572        # Ex: 10.0: 0x41 0x20 0x00 0x00.
573        # 0 10000010 01000000000000000000000
574        # S=0 Positive
575        # Exp=130-127 (offset)=2^3=8
576        # Fraction=1+2^-2=1.25
577        # number=+1.25*8=10
578        f_byte = struct.pack('<f', x)
579        z = self._send_nbytes_receive_4bytes(SERIAL_SF, f_byte, typ='float')
580
581        if DEBUG:
582            y = "s_f(" + str(x) + ") -> " + str(z)
583            print(y)
584        self.f = z
585        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):
587    def q_f(self):  # queries the frequency
588        """Queries the switching frequency. The returned value is in Hz."""
589        z = self._send_receive_4bytes(SERIAL_QF, param1=0, param2=0, typ='float')
590
591        if DEBUG:
592            y = "q_f -> " + str(z)
593            print(y)
594        self.f = z
595        return z

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

def s_duty(self, x):
597    def s_duty(self, x):  # sets the duty cycle of the switching signal
598        """Sets the duty cycle of the switching signal
599        :param x: the duty cycle (float in the range 0-1)
600        :return: the current duty cycle (float between 0 and 1)
601        """
602        duty = int(x * 1000)  # hvps-x is coding duty cycle on a 0-1000 scale
603
604        z = self._send_receive_4bytes(SERIAL_SDUTY, param1=0, param2=duty, typ='uint')
605        z = z / 1000
606        if DEBUG:
607            y = "s_duty(" + str(x) + ") -> " + str(z)
608            print(y)
609        self.duty = z
610        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):
612    def q_duty(self):  # queries the duty cycle of the switching signal
613        """queries the duty cycle of the switching signal
614        :return: the current duty cycle (float between 0 and 1)
615        """
616
617        z = self._send_receive_4bytes(SERIAL_QDUTY, param1=0, param2=0, typ='uint')
618        z = float(z) / 1000.0
619        if DEBUG:
620            y = "q_duty() -> " + str(z)
621            print(y)
622        self.duty = z
623        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):
626    def s_sw_mode(self, x):  # sets the switching mode
627        """Sets the switching mode of the hvps-x. \n
628        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
629        This effectively
630        connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter.
631        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and
632        is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage
633        produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW:
634        the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency.
635        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
636        save() command enables to save this parameter in memory
637        :param x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW
638        :return: Switching mode set by the HVPS
639        """
640
641        if x > SWMODE_LOW:
642            x = SWMODE_OFF
643
644        z = self._send_receive_4bytes(SERIAL_SSWMODE, param1=x, param2=0, typ='uint')
645        if DEBUG:
646            y = "s_sw_mode(" + str(x) + ") -> " + str(z)
647            print(y)
648        self.swmode = z
649        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):
651    def q_sw_mode(self):  # queries the switching mode
652        """queries the switching mode of the hvps-x. \n
653        SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off.
654        This effectively
655        connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter.
656        SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and
657        is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage
658        produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW:
659        the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency.
660        :return: Switching mode set by the HVPS
661        """
662        z = self._send_receive_4bytes(SERIAL_QSWMODE, param1=0, param2=0, typ='uint')
663        if DEBUG:
664            y = "q_sw_mode -> " + str(z)
665            print(y)
666        self.swmode = z
667        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):
669    def s_sw_src(self, x):  # sets the switching source
670        """Sets the source of the switching signal.
671
672        Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard
673        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
674        Using the save() command enables to save this parameter in memory
675        :param x: SWSRC_TMR, or SWSRC_BTTN
676        :return: SWSRC_TMR, or SWSRC_BTTN
677        """
678        if x > SWSRC_BTTN:
679            x = SWSRC_TMR
680        z = self._send_receive_4bytes(SERIAL_SSWSRC, param1=x, param2=0, typ='uint')
681        if DEBUG:
682            y = "s_sw_src(" + str(x) + ") -> " + str(z)
683            print(y)
684        self.swsrc = z
685        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):
687    def q_sw_src(self):  # queries the switching source
688        """queries the source of the switching signal.
689
690        queries the source of the switching signal. Output values are: SWSRC_TMR for onboard
691        switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n
692        Using the save() command enables to save this parameter in memory
693        :return: SWSRC_TMR, or SWSRC_BTTN
694        """
695
696        z = self._send_receive_4bytes(SERIAL_QSWSRC, param1=0, param2=0, typ='uint')
697
698        if DEBUG:
699            y = "q_sw_src -> " + str(z)
700            print(y)
701        self.swsrc = z
702        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):
704    def s_bttn_cfg(self, x):  # sets the configuration of the push button
705        """Defines the behaviour of the push button
706
707        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
708        save() command enables to save this parameter in memory
709        :param x: 2-bit value (0x0 to 0x3). bit 0: Defines the behaviour of the push button, when the switching source
710        of the HVPS is set to the push button
711        (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour
712        (i.e. the high voltage is turned on as long as the button is pressed),
713        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
714        to turn it off).\n
715        bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
716        :return: the 2-bit button config value
717        """
718        if x > 3:
719            x = 3
720        z = self._send_receive_4bytes(SERIAL_SBTTNCFG, param1=x, param2=0, typ='uint')
721        if DEBUG:
722            y = "s_bttn_cfg(" + str(x) + ") -> " + str(z)
723            print(y)
724        self.bttncfg = z
725        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):
727    def q_bttn_cfg(self):  # queries the latch mode of the push button
728        """Queries the behaviour of the push button
729
730        :return: the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching
731        source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above).
732        Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed),
733        and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time
734        to turn it off).\n
735        bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
736        """
737        z = self._send_receive_4bytes(SERIAL_QBTTNCFG, param1=0, param2=0, typ='uint')
738        if DEBUG:
739            y = "q_bttn_cfg -> " + str(z)
740            print(y)
741        self.bttncfg = z
742        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):
744    def q_kill(self):  # queries the state of the kill button. Kill=1 means HV is disabled
745        """Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at
746        the hvps-x output, irrespective of any software setting.
747        :return: 1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise.
748        """
749        z = self._send_receive_4bytes(SERIAL_QKILL, param1=0, param2=0, typ='uint')
750
751        if DEBUG:
752            y = "q_kill -> " + str(z)
753            print(y)
754        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):
756    def s_v_mode(self, x):  # sets the voltage control mode
757        """Sets the voltage control mode
758
759        Sets the voltage control mode (i.e. how is the value of the output voltage controlled):\n
760        VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).\n
761        VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).\n
762        The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the
763        save() command enables to save this parameter in memory\n
764        VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However,
765        the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output
766        voltage remains within the allowed range
767        :param x: VMODE_R, VMODE_O
768        :return: VMODE_R, VMODE_O
769        """
770
771        if x > VMODE_O:
772            x = VMODE_R
773
774        z = self._send_receive_4bytes(SERIAL_SVMODE, param1=x, param2=0, typ='uint')
775        if DEBUG:
776            y = "s_v_mode(" + str(x) + ") -> " + str(z)
777            print(y)
778        self.vmode = z
779        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):
781    def q_v_mode(self):  # queries the switching source
782        """Queries the voltage control mode
783
784        :return: VMODE_R internal voltage regulator, VMODE_O internal
785        open loop control (on-board regulator disconnected).
786        """
787
788        z = self._send_receive_4bytes(SERIAL_QVMODE, param1=0, param2=0, typ='uint')
789
790        if DEBUG:
791            y = "q_v_mode -> " + str(z)
792            print(y)
793        self.vmode = z
794        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):
797    def s_pid(self, x, pid=PID_KPP):
798        """Sets the gains of the PIDs
799        Use save() command to save the new values to memory
800        :param x: the value (float) of the gain.
801        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O},
802        for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P)
803        :return: the gain value
804        """
805        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
806        # Ex: 10.0: 0x41 0x20 0x00 0x00.
807        # 0 10000010 01000000000000000000000
808        # S=0 Positive
809        # Exp=130-127 (offset)=2^3=8
810        # Fraction=1+2^-2=1.25
811        # number=+1.25*8=10
812        pid_byte = struct.pack('<Bf', pid, x)
813        z = self._send_nbytes_receive_4bytes(SERIAL_SPID, pid_byte, typ='float')
814
815        if DEBUG:
816            y = "s_pid(" + str(x) + "," + str(pid) + ") -> " + str(z)
817            print(y)
818        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):
820    def q_pid(self, pid=PID_KPP):  # queries the frequency
821        """returns the gains of the PIDs
822
823        :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O},
824        for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P)
825        :return: the gain value of the chosen parameter
826        """
827
828        z = self._send_receive_4bytes(SERIAL_QPID, param1=pid, param2=0, typ='float')
829
830        if DEBUG:
831            y = "q_pid(" + str(pid) + ") -> " + str(z)
832            print(y)
833
834        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):
836    def s_cal(self, x, cal=CAL_C1P):
837        """Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values)
838        Use save() to commit the setting to memory
839        :param x:  the value of the calibration coefficient (float)
840        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive
841        voltage measurement, negative voltage measurement, output voltage measurement
842        :return: the calibration value
843        """
844
845        # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction
846        # Ex: 10.0: 0x41 0x20 0x00 0x00.
847        # 0 10000010 01000000000000000000000
848        # S=0 Positive
849        # Exp=130-127 (offset)=2^3=8
850        # Fraction=1+2^-2=1.25
851        # number=+1.25*8=10
852        cal_byte = struct.pack('<Bf', cal, x)
853        z = self._send_nbytes_receive_4bytes(SERIAL_SCAL, cal_byte, typ='float')
854
855        if DEBUG:
856            y = "s_cal(" + str(x) + "," + str(cal) + ") -> " + str(z)
857            print(y)
858        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):
860    def q_cal(self, cal=CAL_C1P):  # queries the frequency
861        """queries the calibration constants of the analogue input signals (conversion to calibrated voltage values)
862
863        :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive
864        voltage measurement, negative voltage measurement, output voltage measurement
865        :return: the calibration value
866        """
867
868        z = self._send_receive_4bytes(SERIAL_QCAL, param1=cal, param2=0, typ='float')
869
870        if DEBUG:
871            y = "q_cal(" + str(cal) + ") -> " + str(z)
872            print(y)
873        self.f = z
874        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):
876    def s_calibration_method(self, which=VNOW_POS, calmeth=CALMETH_POLYNOMIAL):
877        if not (calmeth == CALMETH_POLYNOMIAL or calmeth == CALMETH_LOOKUP):
878            calmeth = CALMETH_POLYNOMIAL
879        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
880            which = VNOW_POS
881        z = self._send_receive_4bytes(SERIAL_SCALMETH, param1=which, param2=calmeth, typ='uint')
882
883        if DEBUG:
884            y = "s_calibration_method({0}, {1}) -> {2}".format(which, calmeth, z)
885            print(y)
886        if which == VNOW_POS:
887            self.calmeth_p = z
888        elif which == VNOW_OUT:
889            self.calmeth_o = z
890        else:
891            self.calmeth_n = z
def q_calibration_method(self, which=0):
893    def q_calibration_method(self, which=VNOW_POS):
894        if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG):
895            which = VNOW_POS
896        z = self._send_receive_4bytes(SERIAL_QCALMETH, param1=which, param2=0, typ='uint')
897
898        if DEBUG:
899            y = "q_calibration_method({0}) -> {1}".format(which, z)
900            print(y)
901        if which == VNOW_POS:
902            self.calmeth_p = z
903        elif which == VNOW_NEG:
904            self.calmeth_n = z
905        else:
906            self.calmeth_o = z
def s_lookup(self, x, n, table=1):
908    def s_lookup(self, x, n, table=LOOKUP_ADC_P):
909        if n > 20:
910            n = 20
911        n = n << 2  # shift left 2 bits
912        if table == LOOKUP_ADC_P:
913            n = n | 0b01
914        elif table == LOOKUP_ADC_O:
915            n = n | 0b10
916        x = int(x)
917        z = self._send_receive_4bytes(SERIAL_SLKUP, param1=n, param2=x, typ='int16')
918        if DEBUG:
919            y = "s_lookup({0},{1},{2}) - > {3}".format(x, n, table, z)
920            print(y)
def q_lookup(self, which):
922    def q_lookup(self, which):  # which=0: vout, which=1: ADC_p, which=2: ADC_o
923        format_string = f'<21h'
924        z = self._send_4bytes_receive_nbytes(SERIAL_QLKUP, param1=which, param2=0, fstring=format_string)
925        lookup_list = list(z)
926        if DEBUG:
927            y = "q_lookup({0}) - > {1}".format(which, lookup_list)
928            print(y)
929        return lookup_list
def save(self):
1100    def save(self):  # save current HVPS parameters into the memory
1101        """save active HVPS parameters into the memory
1102
1103        This command saves the active parameters as \'current\' settings. Current setting are the settings that are
1104        loaded when power is applied to the hvpsx
1105
1106        :return: SERIAL_OK or SERIAL_ERROR
1107        """
1108
1109        z = self._send_receive_4bytes(SERIAL_SAVE)
1110
1111        if DEBUG:
1112            y = "save -> " + str(z)
1113            print(y)
1114        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):
1116    def save_memory_to_file(self, settings=SETTINGS_CURRENT):
1117        """Dumps the content of the memory into a JSON file.
1118
1119        Dumps the content of the memory into a file. This is useful to keep a backup of the parameters on file.
1120        Files will be created in the interface folder and have the following format: Name_settings_type_date_time.json\n
1121        json files with settings can be transferred back to the hvps-x with the transfer_settings() method of the HVPS
1122        class, or the higher-level function transfer_file_to_memory()
1123        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
1124        :return: nothing
1125        """
1126
1127        memory = self.q_mem(settings)
1128        now = datetime.now()  # current date and time
1129        date_time = now.strftime("%Y_%m_%d_%H_%M_%S")
1130        if settings == SETTINGS_CURRENT:
1131            st_string = '_current_settings_'
1132        else:
1133            st_string = '_backup_settings_'
1134        file_name = self.name.decode("utf-8") + st_string + date_time + '.json'
1135        with open(file_name, "w") as write_file:
1136            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):
1138    def s_settings(self,
1139                   settings=SETTINGS_CURRENT):  # sets the active setting to a particular type (useful before saving)
1140        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_FACTORY or settings == SETTINGS_BACKUP):
1141            settings = SETTINGS_CURRENT
1142        x = self._send_receive_4bytes(SERIAL_SST, param1=settings)
1143        if DEBUG:
1144            print('s_settings({0}) -> {1}'.format(settings, x))
1145        return x
def load_settings(self, settings=0):
1147    def load_settings(self, settings=SETTINGS_CURRENT):  # Load a setting set from memory to the active setting
1148        """This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x
1149
1150        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
1151        :return:
1152        """
1153
1154        if not (settings == SETTINGS_CURRENT or settings == SETTINGS_BACKUP):
1155            settings = SETTINGS_CURRENT
1156        x = self._send_receive_4bytes(SERIAL_LST, param1=settings)
1157        if DEBUG:
1158            print('load_settings({0}) -> {1}'.format(settings, x))
1159
1160        self._initialise_hvpsx()  # need to reread all parameters
1161
1162        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):
1164    def transfer_settings(self, dict_settings):
1165        """ transfer a setting dictionary from the computer to the hvps-x memory
1166        :param dict_settings: a dictionary containing the settings values.
1167        :return: SERIAL_OK or SERIAL_ERROR
1168        The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together
1169        there two functions make it possible to backup the settings (this includes calibration and PID settings in a
1170        file, and gives the opportunity to restore the settings"""
1171
1172        error = False
1173        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1174        siz = struct.calcsize(memory_string[fw_string])
1175        buffer = ctypes.create_string_buffer(siz)
1176        memmap = memorymap[fw_string]
1177        counter = 0
1178        for x in memmap:
1179            n = x[0]
1180            key = x[1]
1181            data_format = x[2]
1182            if data_format == 'B' or data_format == 's':
1183                bytelength = 1
1184            elif data_format == 'h' or data_format == 'H':
1185                bytelength = 2
1186            elif data_format == 'f':
1187                bytelength = 4
1188            else:
1189                bytelength = 1
1190            data = dict_settings[key]
1191            if key == 'Name':
1192                data = bytes(data, 'utf-8')
1193                if len(data) > 12:
1194                    error = True
1195                    if DEBUG:
1196                        print("Error: name should not exceed 12 characters")
1197                else:
1198                    format_str = '<{0}{1}'.format(n, data_format)
1199                    struct.pack_into(format_str, buffer, counter, data)
1200            else:
1201                format_str = '<{0}'.format(data_format)
1202                if n == 1:
1203                    struct.pack_into(format_str, buffer, counter, data)
1204                else:
1205                    for i in range(n):
1206                        try:
1207                            struct.pack_into(format_str, buffer, counter + i * bytelength, data[i])
1208                        except IndexError:
1209                            error = True
1210                            if DEBUG:
1211                                print('setting dictionary does not fit the expected format')
1212
1213            counter = counter + n * bytelength
1214        data_fw = 'Fw-{0}.{1}'.format(dict_settings['fw_major'], dict_settings['fw_minor'])
1215        if data_fw != fw_string:
1216            error = True
1217            if DEBUG:
1218                print('Error: JSON file firmware version does not match firware currently on hvps-x. Exiting')
1219            exit(0)
1220        if not error:
1221            buffer = b'xxx' + buffer    # Adds 3 random bytes. CMD + 3 random bytes means that when mapping the transfer
1222            # buffer to a structure, it will on an aligned memory address
1223            x = self._send_nbytes_receive_4bytes(SERIAL_XFERST, buffer)
1224        else:
1225            x = SERIAL_ERROR
1226
1227        if DEBUG:
1228            print("transfer_settings(...) -> {0}".format(x))
1229        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):
1231    def copy_settings(self, src, dst):
1232        """
1233        :param src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS)
1234        :param dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by
1235        the source
1236        :return: nothing
1237        Copies one set of settings to another location:\n
1238        Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings
1239        (for example if some temporary settings were saved as current settings, for example to experiment with new PID
1240        gain values)\
1241        Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new
1242        calibration as a set of back-up settings"""
1243        if (src == SETTINGS_CURRENT or src == SETTINGS_BACKUP) and (dst == SETTINGS_CURRENT or dst == SETTINGS_BACKUP):
1244            x = self._send_receive_4bytes(SERIAL_CPYST, param1=src, param2=dst)
1245        else:
1246            x = SERIAL_ERROR
1247
1248        if DEBUG:
1249            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):
1251    def q_mem(self, settings=SETTINGS_CURRENT):  # queries the content of the memory
1252        """
1253        :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
1254        :return: A dictionary with the content of the memory. This is similar to the function save_memory_to_file()
1255        except that is doesn't save the content to a file"""
1256        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1257        memmap = memorymap[fw_string]
1258        dict_mem = {}
1259
1260        z = self._send_4bytes_receive_nbytes(SERIAL_QMEM, settings, param2=0, fstring=memory_string[fw_string])
1261        bytecount = 0
1262        for i in range(len(memmap)):
1263            length = memmap[i][0]
1264            field = memmap[i][1]
1265            if field == 'Name':
1266                length = 1
1267            y = z[bytecount:bytecount + length]
1268            # if field != 'Padding':
1269            if length == 1:
1270                dict_mem[field] = y[0]
1271            else:
1272                dict_mem[field] = y
1273            bytecount = bytecount + length
1274
1275        dict_mem['Name'] = dict_mem['Name'].decode('UTF-8')
1276        dict_mem['Name'] = dict_mem['Name'].split("\0")[0]
1277
1278        if DEBUG:
1279            print(dict_mem)
1280        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):
1282    def q_ver(self):  # queries the firmware version
1283        """returns the current version of the firmware / hardware running on the board."""
1284        z = self._send_receive_4bytes(SERIAL_QVER)
1285        Firm_minor = z & 0xFF
1286        Firm_major = (z >> 8) & 0xFF
1287        Hard_minor = (z >> 16) & 0xFF
1288        Hard_major = (z >> 24)
1289        self.firmware = Firm_major + Firm_minor / 10
1290        self.hardware = Hard_major + Hard_minor / 10
1291        if DEBUG:
1292            y = "q_ver -> {0} / {1}".format(self.firmware, self.hardware)
1293            print(y)
1294        return self.firmware, self.hardware

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

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

returns the configuration of the board.

Returns

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

def s_conf(self, bipolar):
1312    def s_conf(self, bipolar):
1313        """Sets the configuration of the board
1314        :param bipolar: boolean. Is board bipolar (True), or unipolar (false)
1315        :return: bipolar: boolean
1316        """
1317        if bipolar:
1318            param = CONF_BIPOLAR
1319        else:
1320            param = CONF_UNIPOLAR
1321        z = self._send_receive_4bytes(SERIAL_SCONF, param1=param)
1322        if DEBUG:
1323            y = "s_conf -> " + hex(z)
1324            print(y)
1325            if z == SERIAL_ERROR:
1326                print("Error: this configuration is not recognised")
1327        self.conf = z
1328        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):
1330    def s_vmax(self, x):  # sets the maximum voltage rating of the board
1331        """ sets the voltage rating of the hvps-x. Must match the EMCO DC/DC converter rating.
1332        :param x: Voltage rating of hvps-x in Volt
1333        :return: Voltage rating of hvps-x in Volt
1334        """
1335
1336        x = constrain(x, 0, 6000)
1337        z = self._send_receive_4bytes(SERIAL_SVMAX, param2=x)
1338        if DEBUG:
1339            y = "s_vmax(" + str(x) + ") -> " + str(z)
1340            print(y)
1341        self.vmax = z
1342        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):
1344    def q_vmax(self):  # queries the voltage rating of the board
1345        """ Queries the maximal voltage of the board. The returned value is in volts.
1346        :return: board maximal voltage (V)
1347        """
1348        z = self._send_receive_4bytes(SERIAL_QVMAX)
1349        if DEBUG:
1350            y = "q_vmax -> " + str(z)
1351            print(y)
1352        self.vmax = z
1353        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):
1355    def s_name(self, x):  # set the name of the HVPS
1356        """ Sets the name of the HVPS.
1357        :param x: Name of the hvps-x. 11 characters maximum
1358        :return: name accepted by hvps-x
1359        """
1360        ll = len(x)
1361        if ll <= 12:
1362            x = bytearray(x, 'utf-8')
1363            for i in range(12 - ll):  # pad the string with 0s
1364                x = x + b'\0'
1365            self._send_nbytes_receive_4bytes(SERIAL_SNAME, x, typ='uint')
1366            z = self.q_name()
1367        else:
1368            z = 'too long'
1369        if DEBUG:
1370            y = "s_name(" + str(x) + ") -> " + str(z)
1371            print(y)
1372        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):
1374    def q_name(self):  # queries the name of the board
1375        """queries the name of the board
1376        :return: Name of the board
1377        """
1378
1379        # x = self._send_receive_4bytes(SERIAL_QNAME, param1=0)
1380        # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=4) << 32)
1381        # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=8) << 64)
1382        # x = x.to_bytes(12, 'little')
1383
1384        z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s')
1385        z = z[0].split(b'\x00', 1)[0]   # take first (only) element of the tuple and remove the 0s at the end of string
1386
1387        self.name = z
1388        if DEBUG:
1389            y = "q_name -> " + str(z)
1390            print(y)
1391        return z

queries the name of the board

Returns

Name of the board

def set_hardware_version(self, hw_major, hw_minor):
1393    def set_hardware_version(self, hw_major, hw_minor):
1394        param = (hw_major << 8) + hw_minor
1395        self._send_receive_4bytes(SERIAL_SHW, param2=param)
def check_version_compatibility(self):
1397    def check_version_compatibility(self):
1398        self.q_ver()
1399        lib_string = 'Lib-' + LIB_VER
1400        fw_string = 'Fw-{0:.1f}'.format(self.firmware)
1401        hw_string = 'Hw-{0:.1f}'.format(self.hardware)
1402
1403        list_compatible_fw = compatibility_dict[lib_string]
1404        if fw_string in list_compatible_fw:
1405            list_compatible_hw = compatibility_dict[fw_string]
1406            if hw_string not in list_compatible_hw:
1407                self.err |= ERR_FIRM
1408        else:
1409            self.err |= ERR_FIRM
def diagnostic_ADC(self, which_vnow=0):
1411    def diagnostic_ADC(self, which_vnow=VNOW_POS):
1412        x = self._send_4bytes_receive_nbytes(SERIAL_DADC, which_vnow, 0, '<40H')
1413        df = pd.DataFrame(x)
1414        df.to_excel('diagnostic_adc.xlsx')
def q_trace(self, which_vnow=0):
1416    def q_trace(self, which_vnow=VNOW_POS):
1417        x = self._send_4bytes_receive_nbytes(SERIAL_QTRACE, which_vnow, 0, '<100H')
1418        x = np.array(x)
1419        if DEBUG:
1420            y = "trace({0}) -> {1}".format(which_vnow, x)
1421            print(y)
1422        return x
def s_trace_rate(self, rate=5):
1424    def s_trace_rate(self, rate=5):
1425        """Sets the rate at which trace points are stored in memory
1426        :param rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is
1427        calculating the voltages; currently evey 5ms.
1428        :return the rate accepted by the hvps-x"""
1429
1430        rate = int(rate)
1431        if rate < 2:
1432            rate = 2
1433        x = self._send_receive_4bytes(SERIAL_STRRATE, param1=0, param2=rate, typ='uint')
1434        x = x / 10      # the function returns the number of ticks of the counter, which counts at 10kHz.
1435        if DEBUG:
1436            y = "s_trace_rate({0}) -> {1}".format(rate, x)
1437            print(y)
1438        return x

Sets the rate at which trace points are stored in memory

Parameters
  • rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is calculating the voltages; currently evey 5ms. :return the rate accepted by the hvps-x
def q_trace_rate(self):
1440    def q_trace_rate(self):
1441        """queries the rate at which trace points are stored in memory
1442        :param none
1443        :return the rate at which trace points are stored in memory (in ms)"""
1444
1445        x = self._send_receive_4bytes(SERIAL_QTRRATE, param1=0, param2=0, typ='uint')
1446        x = x / 10  # the function returns the number of ticks of the counter, which counts at 10kHz.
1447        if DEBUG:
1448            y = "q_trace_rate() -> {0}".format(x)
1449            print(y)
1450        return x

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

def s_waveform_pwm(self, value):
1452    def s_waveform_pwm(self, value):
1453
1454        value = constrain(value, -4095, 4095)
1455        value = int(value)
1456        x = self._send_receive_4bytes(SERIAL_WFMPWM, param1=0, param2=value, typ='uint')
1457        if DEBUG:
1458            y = "s_waveform_pwm({0}) -> {1}".format(value, x)
1459            print(y)
1460        return x
def list_hvps(list_all=False):
1463def list_hvps(list_all=False):
1464    """lists the hvps-x connected to PC in a dictionary.The key is the name of the HVPS, and the parameter is
1465    the associated serial port
1466    :param list_all: if True list all connected HVPS.
1467    if list_all is false, it will only list device that are configured. Using True enable to list unconfigured
1468    devices and give the opportunity to configure them
1469    :return: dictionary of connected hvps-x"""
1470
1471    hvpsx_ports = [  # creates a list with all STM32 VCP devices connected
1472        p.device
1473        for p in serial.tools.list_ports.comports()
1474        if (p.pid == 0x5740 and p.vid == 0x483)
1475    ]
1476    dict_hvps = {}
1477    for port in hvpsx_ports:
1478        dev = HVPS(port, init=False)  # Attempts to connect to HVPS but without initialisation
1479        if dev.is_hvpsx:
1480            if dev.conf != CONF_UNCONF or list_all:
1481                dict_hvps[dev.name.decode()] = port  # add an entry in the dictionary with the name of the
1482            # HVPS and the port
1483        dev.close(zero=False)
1484
1485    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):
1488def connect_to_hvps(unconf=False):
1489    """Scan for connected hvps-x and connects to it if a single hvps-x is found. User can chose to which hvps-x to
1490    connect if more than one is detected.
1491    :param unconf: True/False. If true will connect to an unconfigured hvps-x. If False (default) only connects to a
1492    configured device
1493    :return: a HVPS object (or None if could not connect to a valid hvps-x
1494    If more than 1 hvps-x is connected to the computed, a list of names is displayed and the user must choose which one
1495    they want to connect to"""
1496    cont = False  # flag to decide whether to continue the programme or close the COM port
1497    dev = None
1498
1499    ports = list_hvps(list_all=unconf)
1500    keys = list(ports.keys())
1501    if len(ports) == 0:
1502        print("No HVPS connected to the device. Terminating programme")
1503        exit()
1504    elif len(ports) == 1:
1505        dev = HVPS(ports[keys[0]])  # connects to the only available HVPS
1506    else:
1507        print("List of connected HVPS:")
1508        for i in range(len(ports)):
1509            print(str(i) + ": " + keys[i])
1510        print("Enter the number of the board you want to connect to")
1511        connect_to = 0
1512        try:
1513            connect_to = int(input())
1514        except ValueError:
1515            print("Invalid entry. Terminating programme")
1516            exit()
1517        if connect_to >= len(ports):
1518            print("Invalid entry. Terminating programme")
1519            exit()
1520        else:
1521            dev = HVPS(ports[keys[connect_to]])
1522    error = dev.err
1523    if error & ERR_PORT:
1524        print("Cannot open COM port. Probably busy with another process. Terminating Programme")
1525    if error & ERR_COM:
1526        print("Cannot communicate with hvps-x. Has firmware been flashed?. Terminating programme")
1527    if error & ERR_TYPE:
1528        print("Device connected this port is not recognised. Terminating programme")
1529    if error & ERR_CONF:
1530        print("This hvps-x is not configured. Please configure unit before using it. "
1531              "Terminating programme.")
1532        cont = True
1533    elif error & ERR_FIRM:
1534        print("Warning: your hvps-x library is not optimal for the firmware or the firmware is not optimal for your "
1535              "hardware. Refer to the website for a compatibility table")
1536        cont = True
1537    if error == 0:
1538        cont = True
1539    if cont:
1540        return dev
1541    else:
1542        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):
1545def configure(dev):
1546    """Script to use to for the initial configuration (or re-configuration) of an hvps-x. Follow prompt on the console.
1547    :param dev: an HVPS object
1548    :return: nothing
1549    """
1550    if dev != -1:
1551        config = configparser.RawConfigParser()
1552        config.read('config.ini')
1553
1554        if dev.conf != CONF_UNCONF:
1555            answer = input("It appears this hvps-x is already configured. (C)ontinue with configuration or (A)bort?")
1556            if answer == 'a' or answer == 'A':
1557                print("exiting configuration")
1558                dev.close()
1559                exit(-1)
1560        answer = input("Is the hvps-x (U)nipolar or (B)ipolar? ")
1561        print("Setting configuration")
1562        if answer == 'B' or answer == 'b':
1563            dev.s_conf(bipolar=True)
1564        else:
1565            dev.s_conf(bipolar=False)
1566        answer = input("Enter the version of your hvps-x PCB (hardware version), as printed on the PCB "
1567                       "(format: X.Y; e.g. 1.2): ")
1568        hw_list = answer.split('.')
1569        if len(hw_list) != 2:
1570            print("Error, the format of the hardware string must be X.Y, e.g. 1.2. Exiting")
1571            exit(0)
1572        dev.set_hardware_version(int(hw_list[0]), int(hw_list[1]))
1573        print("The current name of the hvps-x is " + dev.q_name().decode("utf-8"))
1574        answer = input("Do you want to set/change the name of this hvps-x? (Y)/(N)?")
1575        if answer == 'y' or answer == 'Y':
1576            name = input("Enter hvps-x name (12 char max): ")
1577            if len(name) > 12:
1578                print("Name too long. Ignoring name change")
1579            else:
1580                dev.s_name(name)
1581        else:
1582            print("Leaving name unchanged")
1583        print("\nThe current maximal voltage rating of this HVPS is " + str(dev.q_vmax()) + " V")
1584        answer = input("Do you want to set the maximal voltage of the HVPS? (Y)/(N)")
1585        if answer == 'y' or answer == 'Y':
1586            answer = input("Enter the maximal voltage of the hvps-x in Volt. It must match the voltage rating of the "
1587                           "Emco DC/DC converter:")
1588            print("Setting Vmax to " + answer + "V")
1589            dev.s_vmax(int(answer))
1590        else:
1591            print("Leaving Vmax unchanged")
1592        vmax = dev.q_vmax()
1593        conf_section = 'hvps-x-' + str(vmax)
1594        if config.has_section(conf_section):
1595            print("Default values for this voltage found in configuration file\n")
1596            answer = input("Do you want to replace the values stored in the hvps-x by the one from the config file "
1597                           "(Y/N) (choose Y if configuring a new hvps-x)")
1598            if answer == 'Y' or answer == 'y':
1599                c0p = config.getfloat(conf_section, 'C0P')
1600                c1p = config.getfloat(conf_section, 'C1P')
1601                c2p = config.getfloat(conf_section, 'C2P')
1602                c0o = config.getfloat(conf_section, 'C0O')
1603                c1o = config.getfloat(conf_section, 'C1O')
1604                c2o = config.getfloat(conf_section, 'C2O')
1605
1606                dev.s_cal(c0p, CAL_C0P)
1607                dev.s_cal(c1p, CAL_C1P)
1608                dev.s_cal(c2p, CAL_C2P)
1609                dev.s_cal(c0o, CAL_C0O)
1610                dev.s_cal(c1o, CAL_C1O)
1611                dev.s_cal(c2o, CAL_C2O)
1612                print("Voltage calibration values set...\n")
1613            answer = input(
1614                "Reset PID values to their default values? (Y)/(N) (choose (Y) when configuring a board for the first "
1615                "time)")
1616            if answer == 'Y' or answer == 'y':
1617                kpp = config.getfloat(conf_section, 'KPP')
1618                kip = config.getfloat(conf_section, 'KIP')
1619                kdp = config.getfloat(conf_section, 'KDP')
1620
1621                dev.s_pid(kpp, PID_KPP)
1622                dev.s_pid(kip, PID_KIP)
1623                dev.s_pid(kdp, PID_KDP)
1624                print("PID values set...\n")
1625
1626            print("hvps-x configured. It is recommended to perform a calibration of the voltage readout circuit!")
1627            print("Saving information to hvps-x memory. Backup-settings and default settings")
1628            dev.s_settings(SETTINGS_BACKUP)
1629            dev.save()
1630            dev.s_settings(SETTINGS_CURRENT)
1631            dev.save()
1632        else:
1633            print("Cannot find a section in the config file for a {0} V hvps-x. You need to enter values for voltage "
1634                  "calibration and PID manually ".format(vmax))
1635            dev.save()  # saves previous parameters anyway

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

Parameters
  • dev: an HVPS object
Returns

nothing

def check_functionality(dev):
1638def check_functionality(dev):
1639    """This script can be used after configuration to test the hvps-x functionality. Follow prompt on the console
1640
1641    :param dev: an HVPS object
1642    :return: nothing
1643    """
1644
1645    print("Important:\n"
1646          "This function is experimental and the threshold values are based on a limited number of assembled boards."
1647          "In particular:\n"
1648          "- If the test fails with a value very close to the threshold value, then your board is most likely fine.\n"
1649          "- This version of the script must be used with the bill of material revision 1. If you assembled your"
1650          "board with the original bill of material, this script will not return the correct output. You can use the"
1651          "script included with the library v1.0. Otherwise the website describe how the high-voltage functionality "
1652          "can be manually tested.\n"
1653          "- If unsure, contact Peta-pico-Voltron with test results (be sure to specify hardware version of PCB).\n\n")
1654    if not dev.q_kill():
1655        input("Place HV safety switch (S1) on position 0 and press any key.")
1656        if not dev.q_kill():
1657            input("Switch S1 appears not to be working. Check Switch S1 functionality and restart the test")
1658            dev.close()
1659            exit()
1660    print("During the course of this test, a moderate (~20% of full scale) voltage will be applied to the output of\n "
1661          "the hvps-x. Make sure that it nothing is connected to the output and that you are not touching the\n "
1662          "instrument\n")
1663    print("Voltage set point 0V. Output off")
1664    dev.s_vset(0, DCDC_BOTH)
1665    dev.s_sw_mode(SWMODE_OFF)
1666    dev.s_v_mode(VMODE_O)
1667    dev.s_sw_src(SWSRC_TMR)
1668
1669    print("---Testing HV enable switch (S1) ---")
1670    input("Place HV enable switch (S1) on position 1 and press any key")
1671    if dev.q_kill():
1672        print("S1 switch still reads off state. Check functionality of switch S1.\n Test FAILED. Exiting script...")
1673        dev.close()
1674        exit()
1675    else:
1676        print("***PASS\n")
1677
1678    print("---Testing Voltage monitoring at set point 0 ---")
1679    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1680    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1681    if vnow_p < 5:
1682        print("This is within the tolerance range V < 5. Continuing...")
1683
1684    else:
1685        print("This is outside of the tolerance range V < 5. Something appears to be wrong with the positive DCDC "
1686              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1687        dev.close()
1688        exit()
1689
1690    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1691    if vnow_o < 5:
1692        print("This is within the tolerance range V < 5. Continuing...")
1693    else:
1694        print("This is outside of the tolerance range V < 5. Something appears to be wrong with the output "
1695              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1696        dev.close()
1697        exit()
1698    print("***PASS\n")
1699
1700    print("---Testing Voltage monitoring at set point 20% FS Output off ---")
1701    print("Applying 20% PWM value")
1702    dev.s_vset(int(0.2 * dev.vmax), DCDC_POS)
1703    sleep(0.2)
1704    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1705    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1706    if vnow_p > 900:
1707        print("This is within the tolerance range V > 900. Continuing...")
1708    else:
1709        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1710              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1711        dev.close()
1712        exit()
1713
1714    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1715    if vnow_o < 5:
1716        print("This is within the tolerance range V < 5. Continuing...")
1717    else:
1718        print("This is outside of the tolerance range V < 5. Something appears to be wrong with the output "
1719              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1720        dev.close()
1721        exit()
1722    print("***PASS\n")
1723
1724    print("---Testing Voltage monitoring at set point 20% FS Output LOW ---")
1725    print("Output LOW")
1726    dev.s_sw_mode(SWMODE_LOW)
1727    sleep(0.2)
1728    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1729    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1730    if vnow_p > 900:
1731        print("This is within the tolerance range V > 900. Continuing...")
1732    else:
1733        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1734              "voltage monitoring circuit.\n Test Failed. Exiting script...")
1735        dev.close()
1736        exit()
1737
1738    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1739    if vnow_o < 5:
1740        print("This is within the tolerance range V < 5. Continuing...")
1741    else:
1742        print("This is outside of the tolerance range V < 5. Something appears to be wrong with the output "
1743              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1744        dev.close()
1745        exit()
1746    print("***PASS\n")
1747
1748    print("---Testing Voltage monitoring at set point 20% FS Output HIGH ---")
1749    print("Output HIGH")
1750    dev.s_sw_mode(SWMODE_HIGH)
1751    sleep(0.2)
1752    vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS)
1753    print("Raw reading at the output of the converter is {0} ".format(vnow_p))
1754    if vnow_p > 900:
1755        print("This is within the tolerance range V > 900. Continuing...")
1756    else:
1757        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC "
1758              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1759        dev.close()
1760        exit()
1761
1762    print("Raw reading at the output of optocouplers {0} ".format(vnow_o))
1763    if vnow_o > 900:
1764        print("This is within the tolerance range V > 900. Continuing...")
1765    else:
1766        print("This is outside of the tolerance range V > 900. Something appears to be wrong with the output "
1767              "voltage monitoring circuit.\n Test FAILED. Exiting script...")
1768        dev.close()
1769        exit()
1770    print("***PASS\n")
1771    print("***hvps-x passes all tests***")
1772
1773    dev.s_vset(0, DCDC_BOTH)
1774    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):
1777def transfer_file_to_memory(dev: HVPS):
1778    """Displays a list of json file in the interface folder and gives the user the option to transfer one to the hvps-x
1779    memory
1780    :param dev: an HVPS object
1781    :return: nothing
1782    """
1783    json_files = list_json_files()
1784    if len(json_files) > 0 and dev != -1:
1785        selected_file = select_file(json_files)
1786        with open(selected_file) as file:
1787            data_dict = json.load(file)
1788            dev.transfer_settings(data_dict)
1789    else:
1790        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():
1793def list_json_files():
1794    json_files = []
1795    for file in os.listdir('.'):
1796        if file.endswith('.json'):
1797            json_files.append(file)
1798    return json_files
def select_file(files):
1801def select_file(files):
1802    print("Available Files:")
1803    for i, file in enumerate(files):
1804        print(f"{i + 1}. {file}")
1805    while True:
1806        try:
1807            choice = int(input("Enter the number corresponding to the file you want to open: "))
1808            if 1 <= choice <= len(files):
1809                return files[choice - 1]
1810            else:
1811                print("Invalid choice. Please enter a valid number.")
1812        except ValueError:
1813            print("Invalid choice. Please enter a valid number.")
def main():
1816def main():
1817    dev = connect_to_hvps(unconf=True)
1818    if dev:
1819        print("Name: {0}".format(dev.q_name()))
1820        print("Vmax: {0} V".format(dev.vmax))
1821        print("\n Your choices:")
1822        print("[1] Initial configuration (to be performed after assembling the low-voltage components)")
1823        print("[2] Basic functionality test (to be performed after assembling the high-voltage components)")
1824        print("[3] Transfer a json file to memory")
1825        print("[q] quit\n")
1826        answer = input("Your choice: ")
1827        if answer == '1':
1828            configure(dev)
1829        elif answer == '2':
1830            check_functionality(dev)
1831        elif answer == '3':
1832            transfer_file_to_memory(dev)
1833        dev.close(zero=False)
def constrain(val, min_val, max_val):
1836def constrain(val, min_val, max_val):
1837    """A simple implementation to constrain a value between two boundaries"""
1838    return min(max_val, max(min_val, val))

A simple implementation to constrain a value between two boundaries