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