From 0895a6f323b9a1fd8f61eca7dbfe3ed019bbdc5f Mon Sep 17 00:00:00 2001 From: SrR0 Date: Sat, 14 Sep 2024 18:56:56 +0200 Subject: [PATCH 1/6] A view adaptions to support the AR488 Interface --- .idea/.gitignore | 3 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/pyprologix.iml | 12 + .idea/vcs.xml | 6 + calibration.data | 1 + demo-3-pm2534.py | 26 + demo.py | 19 +- demo2.py | 47 + hp3478a.py | 2 +- pm2534.py | 886 ++++++++++++++++++ prologix.py | 7 +- 13 files changed, 1019 insertions(+), 11 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/pyprologix.iml create mode 100644 .idea/vcs.xml create mode 100644 calibration.data create mode 100644 demo-3-pm2534.py create mode 100644 demo2.py create mode 100644 pm2534.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1772847 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9cabb7e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/pyprologix.iml b/.idea/pyprologix.iml new file mode 100644 index 0000000..039314d --- /dev/null +++ b/.idea/pyprologix.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/calibration.data b/calibration.data new file mode 100644 index 0000000..7ce6ea0 --- /dev/null +++ b/calibration.data @@ -0,0 +1 @@ +@@@@A@BCLMBEMI@@@@AFBEEELMK@@@@@@BEE@MNFIIIIHICL@ALJNIIIIIICLBBLJJ@@@@@@@@@@@@@IIIGFFC@BDBLFIIIH@BAOLMEJLIIIIGI@ECL@KGIIIIIHALMDKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/demo-3-pm2534.py b/demo-3-pm2534.py new file mode 100644 index 0000000..775e92c --- /dev/null +++ b/demo-3-pm2534.py @@ -0,0 +1,26 @@ +from pm2534 import pm2534 +from time import sleep + +port = "COM11" + +test = pm2534(23, port, debug=True) +#test.callReset() +""" +test.setDisplay("ADLERWEB.INFO") +print(test.getStatus()) +print(test.getDigits(test.status.digits)) +print(test.getFunction(test.status.function)) +print(test.getRange(test.status.digits)) +print(test.setFunction(test.Ω2W)) +print(test.setTrigger(test.TRIG_INT)) +print(test.setRange("300")) +#print(test.setDigits(3.5)) + +for x in range(6): +""" +print(test.getMeasure()) + +#print(test.setRange("A")) +#print(test.setDigits(5)) + +#test.getCalibration("calibration.data") diff --git a/demo.py b/demo.py index 0ee07cd..71f5e6b 100644 --- a/demo.py +++ b/demo.py @@ -2,11 +2,12 @@ from hp3478a import hp3478a from time import sleep -port = "/dev/ttyACM0" +port = "COM11" -test = hp3478a(22, port, debug=True) +test = hp3478a(23, port, debug=False) -test.callReset() +#test.callReset() +""" test.setDisplay("ADLERWEB.INFO") print(test.getStatus()) print(test.getDigits(test.status.digits)) @@ -14,12 +15,14 @@ print(test.getRange(test.status.digits)) print(test.setFunction(test.Ω2W)) print(test.setTrigger(test.TRIG_INT)) -print(test.setRange("3M")) -print(test.setDigits(3.5)) +print(test.setRange("300")) +#print(test.setDigits(3.5)) +for x in range(6): +""" print(test.getMeasure()) -print(test.setRange("A")) -print(test.setDigits(5)) +#print(test.setRange("A")) +#print(test.setDigits(5)) -test.getCalibration("calibration.data") +#test.getCalibration("calibration.data") diff --git a/demo2.py b/demo2.py new file mode 100644 index 0000000..2162ded --- /dev/null +++ b/demo2.py @@ -0,0 +1,47 @@ +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation +from hp3478a import hp3478a +from time import sleep, time +# Initialize the connection and measurement parameters for the multimeter +port="COM11" +test = hp3478a(23, port, debug=False) + +test.callReset() + +test.setDisplay("ADLERWEB.INFO") +print(test.getStatus()) +print(test.getDigits(test.status.digits)) +print(test.getFunction(test.status.function)) +print(test.getRange(test.status.digits)) +print(test.setFunction(test.Ω2W)) +print(test.setTrigger(test.TRIG_INT)) +print(test.setRange("300")) +#print(test.setDigits(3.5)) +# Lists for storing the time and measurement values +times = [] +measurements = [] +# Function to get data from the multimeter +def get_measurement(): + return float(test.getMeasure()) +# Function to update the graph +def update(frame): + current_time = time() + measurement = get_measurement() + print(measurement) + times.append(current_time) + measurements.append(measurement) + ax.clear() + ax.plot(times, measurements) + ax.set(xlabel='Time (s)', ylabel='Measurement (Ohms)', + title='Multimeter Measurements Over Time') + plt.xticks(rotation=45) + plt.tight_layout() +# Setup the plot +fig, ax = plt.subplots() +ax.set_xlabel('Time (s)') +ax.set_ylabel('Measurement (Ohms)') +ax.set_title('Multimeter Measurements Over Time') +# Animation function calls update every 10000 ms (10 seconds) +ani = FuncAnimation(fig, update, interval=3000) +# Display the plot +plt.show() diff --git a/hp3478a.py b/hp3478a.py index 45158b7..93c97a8 100644 --- a/hp3478a.py +++ b/hp3478a.py @@ -144,7 +144,7 @@ class hp3478aStatus: fetched: datetime = None status = hp3478aStatus() - def __init__(self, addr: int, port: str=None, baud: int=921600, timeout: float=0.25, prologixGpib: prologix=None, debug: bool=False): + def __init__(self, addr: int, port: str=None, baud: int=115200, timeout: float=0.25, prologixGpib: prologix=None, debug: bool=False): """ Parameters diff --git a/pm2534.py b/pm2534.py new file mode 100644 index 0000000..63d1050 --- /dev/null +++ b/pm2534.py @@ -0,0 +1,886 @@ +from prologix import prologix +from dataclasses import dataclass +from time import sleep +import datetime + + +class pm2534(object): + """Control Philips/Fluke PM2534 multimeters using a Prologix or a AR488 compatible dongle + + Attributes + ---------- + + addr : int + Address of the targeted device + gpib : prologix/ar488 + Prologix object used to communicate with the prologix dongle + status : pm2534Status + Current device status + """ + + addr: int = None + gpib: prologix = None + + # VDC = 1 + # VAC = 2 + # Ω2W = 3 + # Ω4W = 4 + # ADC = 5 + # AAC = 6 + # EXTΩ = 7 + + # TRIG_INT = 1 + # TRIG_EXT = 2 + # TRIG_SIN = 3 + # TRIG_HLD = 4 + # TRIG_FST = 5 + + @dataclass + class pm2534Status: + """Current device status + + Attributes + ---------- + function : int + numeric representation of currently used measurement function: + 1: DC Voltage + 2: AC Voltage + 3: 2-Wire Resistance + 4: 4-Wire Resistance + 5: DC Current + 6: AC Current + 7: Extended Ohms + see also: getFunction + range : int + numeric representation of currenly used measurement range: + 1: 30mV DC, 300mV AC, 30Ω, 300mA, Extended Ohms + 2: 300mV DC, 3V AC, 300Ω, 3A + 3: 3V DC, 30V AC, 3kΩ + 4: 30V DC, 300V AC, 30kΩ + 5: 300V DC, 300kΩ + 6: 3MΩ + 7: 30MΩ + see also: getRange + digits : int + numeric representation of selected measurement resolution: + 1: 5½ Digits + 2: 4½ Digits + 3: 3½ Digits + Lower resoluton allows for faster measurements + see also: getDigits + triggerExternal : bool + External trigger enabled + + calRAM : bool + Cal RAM enabled + frontProts : bool + Front/Read switch selected front measurement connectors + True = Front Port + freq50Hz : bool + Device set up for 50Hz operation. False = 60Hz. + autoZero : bool + Auto-Zero is enabled + autoRange : bool + Auto-Range is enabled + triggerInternal : bool + Internal trigger is enabled. False = Single trigger. + + srqPon : bool + Device asserts SRQ on power-on or Test/Reset/SDC + Controlled by rear configuration switch 3 + srqCalFailed : bool + Device asserts SRQ if CAL procedure failes + srqKbd : bool + Device asserts SRQ if keyboar SRQ is pressed + srqHWErr : bool + Device asserts SRQ if a hardware error occurs + srqSyntaxErr : bool + Device asserts SRQ if a syntax error occurs + srqReading : bool + Device asserts SRQ every time a new reading is available + + errADLink: bool + Error while communicating with aDC + errADSelfTest: bool + ADC failed internal self-test + errADSlope: bool + ADC slope error + errROM: bool + ROM self-test failed + errRAM: bool + RAM self-test failed + errChecksum: bool + Self-test detecten an incorrect CAL RAM checksum + Re-Asserted every time you use an affected range afterwards + + dac: int + Raw DAC value + + fetched: datetime + Date and time this status was updated + """ + """ + + function: int = None + range: int = None + digits: int = None + triggerExternal: bool = None + calRAM: bool = None + frontPorts: bool = None + freq50Hz: bool = None + autoZero: bool = None + autoRange: bool = None + triggerInternal: bool = None + srqPon: bool = None + srqCalFailed: bool = None + srqKbd: bool = None + srqHWErr: bool = None + srqSyntaxErr: bool = None + srqReading: bool = None + errADLink: bool = None + errADSelfTest: bool = None + errADSlope: bool = None + errROM: bool = None + errRAM: bool = None + errChecksum: bool = None + dac: int = None + fetched: datetime = None + """ + + status = pm2534Status() + + def __init__(self, addr: int, port: str = None, baud: int = 115200, timeout: float = 0.25, + prologixGpib: prologix = None, debug: bool = False): + """ + + Parameters + ---------- + addr : int + Address of the targeted device + port : str, optional + path of the serial device to use. Example: `/dev/ttyACM0` or `COM3` + If set a new prologix instance will be created + Either port or prologixGpib must be given + by default None + baud : int, optional + baudrate used for serial communication + only used when port is given + 921600 should work with most USB dongles + 115200 or 9600 are common for devices using UART in between + by default 921600 + timeout : float, optional + number of seconds to wait at maximum for serial data to arrive + only used when port is given + by default 2.5 seconds + prologixGpib : prologix, optional + Prologix instance to use for communication + Ths may be shared between multiple devices with different addresses + Either port or prologixGpib must be given + by default None + debug : bool, optional + Whether to print verbose status messages and all communication + by default False + """ + if port == None and prologixGpib == None: + print("!! You must supply either a serial port or a prologix object") + + self.addr = addr + + if prologixGpib is None: + self.gpib = prologix(port=port, baud=baud, timeout=timeout, debug=debug) + else: + self.gpib = prologixGpib + + def getMeasure(self) -> float: + """Get last measurement as float + + Returns + ------- + float + last measurement + """ + measurement = self.gpib.cmdPoll(" ", self.addr) + + if measurement is None: + return None + + return float(measurement) + + def getDigits(self, digits: int = None) -> float: + """Get a human readable representation of currently used resolution + + Parameters + ---------- + digits : int, optional + numeric representation to interpret + If None is given the last status reading is used + by default None + + Returns + ------- + str|None + 3.5, 4.5 or 5.5 for the current resolution + None for invalid numbers + """ + if digits is None: + digits = self.status.digits + + if digits == 1: + return 5.5 + elif digits == 2: + return 4.5 + elif digits == 3: + return 3.5 + return None + + def getFunction(self, function: int = None) -> str: + """Get a human readable representation of currently used measurement function + + Parameters + ---------- + function : int, optional + numeric representation to interpret + If None is given the last status reading is used + by default None + + Returns + ------- + str|None + VDC for DC Volts + ADC for AC Volts + Ω2W for 2-Wire Resistance + Ω4W for 4-Wire Resistance + ADC for DC Current + AAC for AC Current + ExtΩ for extended Ohms + None for invalid numbers + + """ + if function is None: + function = self.status.function + + if function == 1: + return "VDC" + elif function == 2: + return "VAC" + elif function == 3: + return "Ω2W" + elif function == 4: + return "Ω4W" + elif function == 5: + return "ADC" + elif function == 6: + return "AAC" + elif function == 7: + return "ExtΩ" + else: + return None + + def getRange(self, range: int = None, function: int = None, numeric: bool = False): + """Get a human readable representation of currently used measurement range + + Parameters + ---------- + range : int, optional + numeric range representation to interpret + If None is given the last status reading is used + by default None + function : int, optional + numeric function representation to interpret + If None is given the last status reading is used + by default None + numeric : bool, optional + If True return the maximum value as Float instead + of a human readable verison using SI-prefixes + + Returns + ------- + str|float|None + Maximum measurement value in current range + """ + if range is None: + range = self.status.range + if function is None: + function = self.status.function + + if range == 1: + if function == 1: + if numeric: + return 0.03 + else: + return "30mV" + elif function == 2: + if numeric: + return 0.3 + else: + return "300mV" + elif function == 3 or function == 4: + if numeric: + return 30.0 + else: + return "30Ω" + elif function == 5 or function == 6: + if numeric: + return 0.3 + else: + return "300mA" + else: + return None + elif range == 2: + if function == 1: + if numeric: + return 0.3 + else: + return "300mV" + elif function == 2: + if numeric: + return 3.0 + else: + return "3V" + elif function == 3 or function == 4: + if numeric: + return 300.0 + else: + return "300Ω" + elif function == 5 or function == 6: + if numeric: + return 3.0 + else: + return "3A" + else: + return None + elif range == 3: + if function == 1: + if numeric: + return 3.0 + else: + return "3V" + elif function == 2: + if numeric: + return 30.0 + else: + return "30V" + elif function == 3 or function == 4: + if numeric: + return 3000.0 + else: + return "3kΩ" + else: + return None + elif range == 4: + if function == 1: + if numeric: + return 30.0 + else: + return "30V" + elif function == 2: + if numeric: + return 300.0 + else: + return "300V" + elif function == 3 or function == 4: + if numeric: + return 30000.0 + else: + return "30kΩ" + else: + return None + elif range == 5: + if function == 1: + if numeric: + return 300.0 + else: + return "300V" + elif function == 3 or function == 4: + if numeric: + return 300000.0 + else: + return "300kΩ" + else: + return None + elif range == 6: + if function == 3 or function == 4: + if numeric: + return 3000000.0 + else: + return "3MΩ" + else: + return None + elif range == 7: + if function == 3 or function == 4: + if numeric: + return 30000000.0 + else: + return "30MΩ" + else: + return None + + def getStatus(self) -> pm2534Status: + """Read current device status and populate status object + + Returns + ------- + pm2534Status + Updated status object + """ + status = self.gpib.cmdPoll("B", self.addr, binary=True) + + # Update last readout time + self.status.fetched = datetime.datetime.now() + + # Byte 5: RAW DAC value + self.status.dac = status[4] + + # Byte 4: Error Information + self.status.errChecksum = (status[3] & (1 << 0) != 0) + self.status.errRAM = (status[3] & (1 << 1) != 0) + self.status.errROM = (status[3] & (1 << 2) != 0) + self.status.errADSlope = (status[3] & (1 << 3) != 0) + self.status.errADSelfTest = (status[3] & (1 << 4) != 0) + self.status.errADLink = (status[3] & (1 << 5) != 0) + + # Byte 3: Serial Poll Mask + self.status.srqReading = (status[2] & (1 << 0) != 0) + # Bit 1 not used + self.status.srqSyntaxErr = (status[2] & (1 << 2) != 0) + self.status.srqHWErr = (status[2] & (1 << 3) != 0) + self.status.srqKbd = (status[2] & (1 << 4) != 0) + self.status.srqCalFailed = (status[2] & (1 << 5) != 0) + # Bit 6 always zero + self.status.srqPon = (status[2] & (1 << 7) != 0) + + # Byte 2: Status Bits + self.status.triggerInternal = (status[1] & (1 << 0) != 0) + self.status.autoRange = (status[1] & (1 << 1) != 0) + self.status.autoZero = (status[1] & (1 << 2) != 0) + self.status.freq50Hz = (status[1] & (1 << 3) != 0) + self.status.frontPorts = (status[1] & (1 << 4) != 0) + self.status.calRAM = (status[1] & (1 << 5) != 0) + self.status.triggerExternal = (status[1] & (1 << 6) != 0) + + # Byte 1: Function/Range/Digits + sb1 = status[0] + self.status.digits = (sb1 & 0b00000011) + sb1 = sb1 >> 2 + self.status.range = (sb1 & 0b00000111) + sb1 = sb1 >> 3 + self.status.function = (sb1 & 0b00000111) + + return self.status + + def getFrontRear(self) -> bool: + """Get position of Front/Rear switch + + May also be used to easily determine if the device is responding + + Returns + ------- + bool + True -> Front-Port + False -> Rear-Port + None -> Device did not respond + """ + check = self.gpib.cmdPoll("S") + if check == "1": + return True + elif check == "0": + return False + else: + return None + + def getCalibration(self, filename: str = None) -> bytearray: + """Read device calibration data + + Code based on work by + Steve1515 (EEVblog) + fenugrec (EEVblog) + Luke Mester (https://mesterhome.com/) + + Parameters + ---------- + filename : str, optional + filename to save calibration to + file will be overwritten if it exists + by default None + + Returns + ------- + bytearray + Raw calibration data + """ + + self.callReset() + self.setTrigger(self.TRIG_HLD) + + check = self.getFrontRear() + if check is None: + print("Can not connect to instrument") + return None + + self.setDisplay("CAL READ 00%") + + p = 0 + lp = 0 + cdata = b"" + + for dbyte in range(0, 255): + din = self.gpib.cmdPoll(self.gpib.escapeCmd("W" + chr(dbyte)), binary=True) + cdata += din + p = (int)(dbyte / 25.5) + if p != lp: + self.setDisplay("CAL READ " + str(p) + "0%") + lp = p + + self.setDisplay("CAL READ OK") + + if filename is not None: + fp = open("calibration.data", "wb") + for byte in cdata: + fp.write(byte.to_bytes(1, byteorder='big')) + fp.close() + + sleep(1) + self.setDisplay(None) + + self.callReset() + + return cdata + + def setAutoZero(self, autoZero: bool, noUpdate: bool = False) -> bool: + """change Auto-Zero setting + + Parameters + ---------- + autoZero : bool + Whether to enable or disable Auto-Zero + noUpdate : bool, optional + If True do not update status object to verify change was successful + by default False + + Returns + ------- + bool + new status of autoZero; presumed status if `noUpdate` was True + """ + setVal = 0 + if autoZero: setVal = 1 + + self.gpib.cmdWrite("Z" + str(autoZero), self.addr) + + if noUpdate: + if self.gpib.debug: + print(".. AutoZero changed to " + setVal + " without verification.") + return setVal + else: + self.getStatus() + if autoZero != self.status.autoZero: + print( + "!! Error while changing AutoZero - tried to set " + str(autoZero) + " but verification was " + str( + self.status.autoZero)) + elif self.gpib.debug: + print(".. AutoZero successfully changed to " + str(self.status.autoZero)) + return self.status.autoZero + + def setDisplay(self, text: str = None, online: bool = True) -> bool: + """Change device display + + Parameters + ---------- + text : str, optional + When text is None or empty device will resume standard display mode + as in show measurements + When text is set it will be displayed on the device + + Only ASCII 32-95 are valid. Function aborts for invalid characters + Must be <= 12 Characters while , and . do not count as character. + consecutive , and . may not work + using . or , after character 12 may not work + Function aborts for too long strings + online : bool, optional + When True the device just shows the text but keeps all functionality online + When False the device will turn off all dedicated annunciators and stop updating + the display once the text was drwn. This will free up ressources and enable + faster measurement speeds. Using False takes about 30mS to complete. If the + updating is stopped for over 10 minutes if will shut down as in blank screen. + by default True + + Returns + ------- + bool + Wheather setting the text worked as expected + """ + if text is None or text == "": + # Reset display + self.gpib.cmdWrite("D1", self.addr) + if self.gpib.debug: + print("Display reset to standard mode") + return True + + len = 0 + for c in text: + if ord(c) < 32 or ord(c) > 95: + print("!! Character '" + c + "' is not supported") + return False + if c != "," and c != ".": + len = len + 1 + + if len > 12: + print("!! Text too long; max 12 characters") + return False + + cmd = "D2" + dt = "" + if not online: + cmd = "D3" + dt = " (updates paused)" + + self.gpib.cmdWrite(cmd + text, self.addr) + + if self.gpib.debug: + print(".. Display changed to '" + text + "'" + dt) + + # @TODO we could check status/errors to catch syntax errors here + return True + + def setFunction(self, function: int, noUpdate: bool = False) -> bool: + """Change current measurement function + + Parameters + ---------- + function : int + numeric function representation + you may also use the following class constants: + VDC,VAC,Ω2W,Ω4W,ADC,AAC,EXTΩ + noUpdate : bool, optional + If True do not update status object to verify change was successful + by default False + + Returns + ------- + bool + Whether update succeeded or not; not verified if `noUpdate` was True + """ + if function <= 0 or function > 7: + print("!! Invalid function") + return False + + self.gpib.cmdWrite("F" + str(function), self.addr) + + if not noUpdate: + self.getStatus() + if self.status.function != function: + print("!! Set failed. Tried to set " + self.getFunction( + function) + " but device returned " + self.getFunction(self.status.function)) + return False + elif self.gpib.debug: + print(".. Changed to function " + self.getFunction(function)) + elif self.gpib.debug: + print(".. Probably changed to function " + self.getFunction(function)) + + return True + + def setRange(self, range: str, noUpdate: bool = False) -> bool: + """Change current measurement range + + Parameters + ---------- + range : str|float + Range as SI-Value or float + Valid values: + A or AUTO to enable Auto-Range + 30m,300m,3,30,300,3k,30k,300k,3M,30M + 0.03,0.3,3,30,300,3000,30000,300000,3000000,30000000 + Not all ranges can be used in all measurement functions + noUpdate : bool, optional + If True do not update status object to verify change was successful + by default False + + Returns + ------- + bool + Whether update succeeded or not; not verified if `noUpdate` was True + """ + newRange = None + newRangeF = None + if range == "30m" or range == 0.03: + newRange = -2 + newRangeF = 0.03 + elif range == "300m" or range == 0.3: + newRange = -1 + newRangeF = 0.3 + elif range == "3" or range == 3: + newRange = 0 + newRangeF = 3 + elif range == "30" or range == 30: + newRange = 1 + newRangeF = 30 + elif range == "300" or range == 300: + newRange = 2 + newRangeF = 300 + elif range == "3k" or range == 3000: + newRange = 3 + newRangeF = 3000 + elif range == "30k" or range == 30000: + newRange = 4 + newRangeF = 30000 + elif range == "300k" or range == 300000: + newRange = 5 + newRangeF = 300000 + elif range == "3M" or range == 3000000: + newRange = 6 + newRangeF = 3000000 + elif range == "30M" or range == 30000000: + newRange = 7 + newRangeF = 30000000 + elif range.lower() == "a" or range.lower() == "auto": + newRange = "A" + + if newRange is None: + print("!! Invalid range") + return False + + self.gpib.cmdWrite("R" + str(newRange), self.addr) + + if not noUpdate: + self.getStatus() + if newRange == "A": + if not self.status.autoRange: + print("!! Tried to enable Auto-Range but device refused") + return False + elif self.gpib.debug: + print(".. Enabled Auto-Range") + else: + newRangeC = self.getRange(numeric=True) + if newRangeF != newRangeC: + print("!! Tried to set range to " + str(range) + " but device reported " + self.getRange()) + return False + elif self.gpib.debug: + print(".. Set range to " + self.getRange()) + elif self.gpib.debug: + print(".. Probably changed to range " + str(range)) + + return True + + def setDigits(self, digits: float, noUpdate: bool = False) -> bool: + """Change current measurement resolution + + Parameters + ---------- + digits : float + desired measurement resolution + Valid values: 3,3.5,4,4.5,5,5.5 + noUpdate : bool, optional + If True do not update status object to verify change was successful + by default False + + Returns + ------- + bool + Whether update succeeded or not; not verified if `noUpdate` was True + """ + newDigits = None + if digits == 3 or digits == 3.5: + newDigits = "3" + elif digits == 4 or digits == 4.5: + newDigits = "4" + elif digits == 5 or digits == 5.5: + newDigits = "5" + else: + print("!! Invalid digits") + return False + + self.gpib.cmdWrite("N" + newDigits, self.addr) + + if not noUpdate: + self.getStatus() + if int(self.getDigits()) != int(newDigits): + print("!! Tried to set digits to " + str(int(newDigits)) + "½ but device reported " + str( + int(self.getDigits())) + "½") + return False + elif self.gpib.debug: + print(".. Set digits to " + str(int(self.getDigits())) + "½") + elif self.gpib.debug: + print(".. Probably changed digits to " + str(int(self.getDigits())) + "½") + + return True + + def setTrigger(self, trigger: int, noUpdate: bool = False) -> bool: + """Change current measurement trigger + + Parameters + ---------- + trigger : int + desired trigger mode + 1 or TRIG_INT -> Automatic internal trigger + 2 or TRIG_EXT -> Abort current measurements; Start on LOW-edge or via GPIB + 3 or TRIG_SIN -> Complete current measurement. New one on GPIB GET + 4 or TRIG_HLD -> Abort current measurement. New one on GPIB GET + 5 or TRIG_FST -> Same as TRIG_SIN but skip settling delay for AC + + noUpdate : bool, optional + If True do not update status object to verify change was successful + by default False + + Returns + ------- + bool + Whether update succeeded or not; not verified if `noUpdate` was True + """ + if trigger <= 0 or trigger > 5: + print("!! Invalid digits") + return False + + self.gpib.cmdWrite("T" + str(trigger), self.addr) + + if not noUpdate: + self.getStatus() + if trigger == self.TRIG_EXT and not self.status.triggerExternal: + print("!! Tried to enable external trigger but flag did not update") + return False + elif trigger == self.TRIG_SIN and self.status.triggerInternal: + print("!! Tried to enable singe trigger but auto trigger flag is still active") + return False + elif trigger == self.TRIG_INT and not self.status.triggerInternal: + print("!! Tried to enable internal trigger but auto trigger flag is not active") + return False + elif trigger == self.TRIG_HLD and self.status.triggerInternal: + print("!! Tried to enable trigger hold but auto trigger flag is still active") + return False + + if self.gpib.debug: + print(".. Probably changed trigger to " + str(trigger)) + + return True + + def setSRQ(self, srq: int): + """Set Serial Poll Register Mask + + @TODO Not tested and no validations + + Parameters + ---------- + srq : int + Parameter must be two digits exactly. Bits 0-5 of the binary representation + are used to set the mask + """ + self.gpib.cmdWrite(srq, self.addr) + + def clearSPR(self): + """Clear Serial Poll Register (SPR) + """ + self.gpib.cmdWrite("K", self.addr) + + def clearERR(self) -> bytearray: + """Clear Error Registers + + Returns + ------- + bytearray + Error register as octal digits + """ + return self.gpib.cmdPoll("E", self.addr, binary=True) + + def callReset(self): + """Reset the device + """ + self.gpib.cmdClr(self.addr) diff --git a/prologix.py b/prologix.py index cd70e77..4736715 100644 --- a/prologix.py +++ b/prologix.py @@ -1,6 +1,7 @@ import serial import datetime import os +import time class prologix(object): """Class for handling prologix protocol based GPIB communication @@ -25,7 +26,7 @@ class prologix(object): timeout: float = 2.5 EOL: str = "\n" - def __init__(self, port: str, baud: int=921600, timeout: float=2.5, debug: bool=False): + def __init__(self, port: str, baud: int=115200, timeout: float=2.5, debug: bool=False): """ Parameters @@ -59,12 +60,13 @@ def __init__(self, port: str, baud: int=921600, timeout: float=2.5, debug: bool= return None #Check for Prologix device + time.sleep(2.5) check = self.cmdPoll("++ver", read=False) if len(check)<=0: print("!! No responding device on port " + port + " found") self.serial = None return None - elif not "Prologix".casefold() in check.casefold(): + elif not ("Prologix".casefold() in check.casefold() or "AR488".casefold() in check.casefold()): print("!! Device on Port " + port + " does not seem to be Prologix compatible") print(check) self.serial = None @@ -98,6 +100,7 @@ def cmdWrite(self, cmd: str, addr: int=None): we're sure noone else is using the bus to reduce bus load. """ self.cmdWrite("++addr " + str(addr), addr=None) + self.serial.read() self.serial.write(str.encode(cmd+self.EOL)) if self.debug: print(">> " + cmd) From 3fe9a727b41265942d509dfc35859f3a73674113 Mon Sep 17 00:00:00 2001 From: SrR0 Date: Sat, 14 Sep 2024 19:24:19 +0200 Subject: [PATCH 2/6] Test check in --- demo-3-pm2534.py | 1 + 1 file changed, 1 insertion(+) diff --git a/demo-3-pm2534.py b/demo-3-pm2534.py index 775e92c..48ec32d 100644 --- a/demo-3-pm2534.py +++ b/demo-3-pm2534.py @@ -5,6 +5,7 @@ test = pm2534(23, port, debug=True) #test.callReset() +#just a line added """ test.setDisplay("ADLERWEB.INFO") print(test.getStatus()) From 5117d47e46dd4c06ba586868af3d2fd1cab5c3ac Mon Sep 17 00:00:00 2001 From: SrR0 Date: Sat, 14 Sep 2024 19:37:31 +0200 Subject: [PATCH 3/6] Rename Demo Classes Added SDM3065x --- demo.py => demo-hp3478a-1.py | 0 demo2.py => demo-hp3478a-2.py | 0 demo-3-pm2534.py => demo-pm2534-1.py | 0 demo-sdm3065x-1.py | 12 ++ sdm3065x.py | 179 +++++++++++++++++++++++++++ 5 files changed, 191 insertions(+) rename demo.py => demo-hp3478a-1.py (100%) rename demo2.py => demo-hp3478a-2.py (100%) rename demo-3-pm2534.py => demo-pm2534-1.py (100%) create mode 100644 demo-sdm3065x-1.py create mode 100644 sdm3065x.py diff --git a/demo.py b/demo-hp3478a-1.py similarity index 100% rename from demo.py rename to demo-hp3478a-1.py diff --git a/demo2.py b/demo-hp3478a-2.py similarity index 100% rename from demo2.py rename to demo-hp3478a-2.py diff --git a/demo-3-pm2534.py b/demo-pm2534-1.py similarity index 100% rename from demo-3-pm2534.py rename to demo-pm2534-1.py diff --git a/demo-sdm3065x-1.py b/demo-sdm3065x-1.py new file mode 100644 index 0000000..a927dc8 --- /dev/null +++ b/demo-sdm3065x-1.py @@ -0,0 +1,12 @@ +import sdm3065x + +dmm = siglent_sdm3065x.SDM3065X('10.0.0.114') +dmm.reset() + +# setup: +v = dmm.getVoltageDC('20V', 0.05) # set NPLC to 0.05 which is 1ms in a 50Hz grid +print(v) + +# further readings: +for i in range(10): + print(dmm.read()) \ No newline at end of file diff --git a/sdm3065x.py b/sdm3065x.py new file mode 100644 index 0000000..48e24ef --- /dev/null +++ b/sdm3065x.py @@ -0,0 +1,179 @@ +# Python3 Class for controlling a Siglent SDM3065x Bench Multimeter via Ethernet and SCPI + +# MIT License +# Copyright 2018 Karl Zeilhofer, Team14.at + +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import decimal +import socket +import sys + +import time + +_timeoutCmd = 10 # seconds +_timeoutQuery = 10 # seconds + + +class SDM3065X: + def __init__(self, ip): + self.ipAddress = ip + self._port = 5024 # TCP port, specific to these devices, the standard would be 5025 + + self.NPLC = ['100', '10', '1', '0.5', '0.05', + '0.005'] # available Integration durations in number of powerline cycles + # set with Voltage:DC:NPLC or similar + self.voltageRangeDC = ['200mV', '2V', '20V', '200V', '1000V', 'AUTO'] # set with VOLTage:DC:RANGe + self.voltageRangeAC = ['200mV', '2V', '20V', '200V', '750V', 'AUTO'] # set with VOLTage:AC:RANGe + self.currentRangeDC = ['200uA', '2mA', '20mA', '200mA', '2A', '10A', 'AUTO'] # set with CURRent:DC:RANGe + self.currentRangeAC = ['200uA', '2mA', '20mA', '200mA', '2A', '10A', 'AUTO'] # set with CURRent:AC:RANGe + + self._PrintDebug = True + + def _abort(self, msg): + # TODO 2: something like this: traceback.print_stack(sys.stdout) + print('Abort for device IP=' + self.ipAddress + ':' + str(self._port)) + print(msg) + print('ERROR') + sys.exit(-1) + + def _debug(self, msg): + if (self._PrintDebug): + print(self.ipAddress + ':' + str(self._port) + ': ' + msg) + + def reset(self): + self._runScpiCmd('*RST') + + def getVoltageDC(self, range='AUTO', integrationNPLC='10'): + self._runScpiCmd('abort') # stop active measurement + self._runScpiCmd('Sense:Function "Voltage:DC"') + + if str(integrationNPLC) not in self.NPLC: + self._abort('invalid integrationNPLC: ' + str(integrationNPLC) + ', use ' + str(self.NPLC)) + self._runScpiCmd('Sense:Voltage:DC:NPLC ' + str(integrationNPLC)) + + if range not in self.voltageRangeDC: + self._abort('invalid range: ' + range + ', use ' + str(self.voltageRangeDC)) + self._runScpiCmd('Sense:Voltage:DC:Range ' + range) + + if range == 'AUTO': + self._runScpiCmd('Sense:Voltage:DC:Range:AUTO ON') + else: + self._runScpiCmd('Sense:Voltage:DC:Range:AUTO OFF') + + self._runScpiCmd('Trigger:Source Bus') + self._runScpiCmd('Sample:Count MAX') # continiuous sampling, max. 600 Mio points + self._runScpiCmd('R?') # clear buffer + self._runScpiCmd('Initiate') # arm the trigger + self._runScpiCmd('*TRG') # send trigger (samples exact one value) + + return self.read() + + def getCurrentDC(self, range='AUTO', integrationNPLC='10'): + self._runScpiCmd('abort') # stop active measurement + self._runScpiCmd('Sense:Function "Current:DC"') + + if str(integrationNPLC) not in self.NPLC: + self._abort('invalid integrationNPLC: ' + str(integrationNPLC) + ', use ' + str(self.NPLC)) + self._runScpiCmd('Sense:Current:DC:NPLC ' + str(integrationNPLC)) + + if range not in self.currentRangeDC: + self._abort('invalid range: ' + range + ', use ' + str(self.currentRangeDC)) + self._runScpiCmd('Sense:Current:DC:Range ' + range) + + if range == 'AUTO': + self._runScpiCmd('Sense:Current:DC:Range:AUTO ON') + else: + self._runScpiCmd('Sense:Current:DC:Range:AUTO OFF') + + self._runScpiCmd('Trigger:Source Bus') + self._runScpiCmd('Sample:Count MAX') # continiuous sampling, max. 600 Mio points + self._runScpiCmd('R?') # clear buffer + self._runScpiCmd('Initiate') # arm the trigger + self._runScpiCmd('*TRG') # send trigger (samples exact one value) + + return self.read() + + # read() + # get latest value, with current settings + # saves a lot of time! + # the DMM aquires in the meanwhile, and read fetches the most recent value + # getVoltageDC() or getCurrentDC() must be called before! + # typical session with netcat: + # >>r? 1 + # #215+1.36239593E+00 + # ^ 2 = number of digits with describe the packet length + def read(self): + ans = '>>' + t0 = time.time() + while ans == '>>': + if (time.time() - t0) > 5: + self._abort('timeout in read(), forgot to start the measurement?') + ans = self._runScpiQuery('R? 1') # get latest data + if ans == '>>': + time.sleep(0.1) + ans = str(ans) + nDigits = int(ans[1]) + ans = ans[(2 + nDigits):] + + return self.str2engNumber(ans) + + def _runScpiCmd(self, cmd, timeout=_timeoutCmd): + self._debug('_runScpiCmd(' + cmd + ')') + + BUFFER_SIZE = 1024 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(timeout) + data = "" + try: + s.connect((self.ipAddress, self._port)) + data = s.recv(BUFFER_SIZE) # discard welcome message + s.send(bytes(cmd + '\n', 'utf-8')) + data = s.recv(BUFFER_SIZE) + except socket.timeout: + self._abort('timeout on ' + cmd) + + s.close() + retStr = data.decode() + self._debug('>>' + retStr) + return retStr + + def _runScpiQuery(self, query, timeout=_timeoutQuery): + self._debug('_runScpiQuery(' + query + ')') + + BUFFER_SIZE = 1024 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(timeout) + data = b"" + try: + s.connect((self.ipAddress, self._port)) + data = s.recv(BUFFER_SIZE) # discard welcome message + s.send(bytes(query + '\n', 'utf-8')) + data = s.recv(BUFFER_SIZE) + except socket.timeout: + self._abort('timeout on ' + query) + + s.close() + retStr = data.decode() + retStr = retStr.strip('\r\n') + return retStr + + def str2engNumber(self, str): + x = decimal.Decimal(str) + return x.normalize().to_eng_string() + From a4215c179bf67b993026233df3c449107437d90d Mon Sep 17 00:00:00 2001 From: SrR0 Date: Sat, 14 Sep 2024 19:41:24 +0200 Subject: [PATCH 4/6] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d15b720..42f1720 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ Most functions are supported. Additionally you can read calibration SRAM data to Not yet implemented, WIP +### Siglent SDM3065x + +Communicates over Network + ### IEEE488.2/SCPI standard Not yet implemented From 460d09e18e6d13d73ce7049342103ed9dd86dcfd Mon Sep 17 00:00:00 2001 From: SrR0 Date: Sat, 21 Sep 2024 21:03:54 +0200 Subject: [PATCH 5/6] Rename Demo Classes Initial general program for HP and PM --- MeasureAll.py | 184 +++++++++++++++++++++++++ bm869s.py | 299 ++++++++++++++++++++++++++++++++++++++++ demo-hp3478a-1.py | 7 +- demo-pm2534-1.py | 29 ++-- demo-sdm3065x-1.py | 2 +- hp3478a.py | 2 +- pm2534.py | 336 ++++++++++++--------------------------------- 7 files changed, 594 insertions(+), 265 deletions(-) create mode 100644 MeasureAll.py create mode 100644 bm869s.py diff --git a/MeasureAll.py b/MeasureAll.py new file mode 100644 index 0000000..09471de --- /dev/null +++ b/MeasureAll.py @@ -0,0 +1,184 @@ +import threading +import time +import matplotlib.pyplot as plt +from collections import deque +from matplotlib.widgets import Button +from pm2534 import pm2534 +from hp3478a import hp3478a + +# Initialisiere die Messgeräte (pm2534 und hp3478a) +device_1 = pm2534(22, "COM11", debug=False) +device_2 = hp3478a(23, "COM12", debug=False) + +Geraet1 = 'PM2534' +Geraet2 = 'HP3478A' + +# Betriebsmodus für die Geräte +mode_device_1 = "Mode A" +mode_device_2 = "Mode A" + +# Datenpuffer für jedes Messgerät (deque für die Echtzeitdaten) +data_1 = deque(maxlen=100) +data_2 = deque(maxlen=100) + +# Steuerung für das Beenden der Threads +stop_threads = False +pause_plotting = False # Flag für das Pausieren des Neuzeichnens + +# Echtzeitplot mit Matplotlib (muss im Hauptthread laufen) +plt.ion() # Interaktiver Modus für Echtzeitplot +fig, (ax, ax_hist) = plt.subplots(2, 1, gridspec_kw={'height_ratios': [2, 1]}) +plt.subplots_adjust(bottom=0.4, hspace=0.4) # Platz für Schaltflächen und Abstand zwischen Plots + +# Annotation für Mouseover +annotation = ax.annotate("", xy=(0,0), xytext=(20,20), + textcoords="offset points", + bbox=dict(boxstyle="round", fc="w"), + arrowprops=dict(arrowstyle="->")) +annotation.set_visible(False) + +# Hauptplot-Funktion, die die Werte in Echtzeit darstellt +def update_plot(): + ax.clear() + ax.plot(data_1, label=Geraet1, marker='*', linestyle='-') # Plot für Gerät 1 mit Sternen + ax.plot(data_2, label=Geraet2, marker='*', linestyle='-') # Plot für Gerät 2 mit Sternen + ax.legend() + ax.grid(True) # Gittermuster aktivieren + + # Histogramm aktualisieren + ax_hist.clear() + ax_hist.hist(data_1, bins=10, alpha=0.5, label=Geraet1) # Histogramm für Gerät 1 + ax_hist.hist(data_2, bins=10, alpha=0.5, label=Geraet2) # Histogramm für Gerät 2 + ax_hist.legend() + ax_hist.set_title('Histogramm der Messwerte') + ax_hist.grid(True) + + plt.draw() + +# Funktion, die in einem Thread für jedes Messgerät läuft und Werte liest +def read_device1(device, data_queue, mode): + global stop_threads + while not stop_threads: + value = device.getMeasure() + data_queue.append(value) + print(f"Device 1 running in {mode}") + time.sleep(1) + +def read_device2(device, data_queue, mode): + global stop_threads + while not stop_threads: + value = device.getMeasure() + data_queue.append(value) + print(f"Device 2 running in {mode}") + time.sleep(1) + +# Threads für jedes Gerät starten +thread_1 = threading.Thread(target=read_device1, args=(device_1, data_1, mode_device_1)) +thread_2 = threading.Thread(target=read_device2, args=(device_2, data_2, mode_device_2)) +thread_1.start() +thread_2.start() + +# Schaltflächen für das Umschalten der Betriebsmodi und Pausieren +ax_button_1_mode_a = plt.axes([0.1, 0.25, 0.1, 0.075]) +ax_button_1_mode_b = plt.axes([0.25, 0.25, 0.1, 0.075]) +ax_button_2_mode_a = plt.axes([0.55, 0.25, 0.1, 0.075]) +ax_button_2_mode_b = plt.axes([0.7, 0.25, 0.1, 0.075]) +ax_button_reset = plt.axes([0.4, 0.05, 0.2, 0.075]) # Position für den Reset-Button +ax_button_pause = plt.axes([0.1, 0.1, 0.2, 0.075]) # Position für den Pause-Button + +button_1_mode_a = Button(ax_button_1_mode_a, 'Gerät 1 Mode A') +button_1_mode_b = Button(ax_button_1_mode_b, 'Gerät 1 Mode B') +button_2_mode_a = Button(ax_button_2_mode_a, 'Gerät 2 Mode A') +button_2_mode_b = Button(ax_button_2_mode_b, 'Gerät 2 Mode B') +button_reset = Button(ax_button_reset, 'Reset Daten') # Reset-Button +button_pause = Button(ax_button_pause, 'Pause Plotting') # Pause-Button + +# Callback-Funktionen, die den Modus ändern +def set_device_1_mode_a(event): + global mode_device_1 + mode_device_1 = "Mode A" + print("Gerät 1 auf Mode A umgeschaltet") + +def set_device_1_mode_b(event): + global mode_device_1 + mode_device_1 = "Mode B" + print("Gerät 1 auf Mode B umgeschaltet") + +def set_device_2_mode_a(event): + global mode_device_2 + mode_device_2 = "Mode A" + print("Gerät 2 auf Mode A umgeschaltet") + +def set_device_2_mode_b(event): + global mode_device_2 + mode_device_2 = "Mode B" + print("Gerät 2 auf Mode B umgeschaltet") + +# Funktion zum Zurücksetzen der Datenpuffer +def reset_data(event): + global data_1, data_2 + data_1.clear() # Leert den Datenpuffer von Gerät 1 + data_2.clear() # Leert den Datenpuffer von Gerät 2 + print("Datenpuffer geleert") + +# Funktion zum Pausieren des Neuzeichnens +def toggle_pause(event): + global pause_plotting + pause_plotting = not pause_plotting # Toggle den Zustand + button_pause.label.set_text('Resume Plotting' if pause_plotting else 'Pause Plotting') # Ändere den Button-Text + print("Neuzeichnen der Grafik pausiert." if pause_plotting else "Neuzeichnen der Grafik fortgesetzt.") + +# Binde die Schaltflächen an die Callback-Funktionen +button_1_mode_a.on_clicked(set_device_1_mode_a) +button_1_mode_b.on_clicked(set_device_1_mode_b) +button_2_mode_a.on_clicked(set_device_2_mode_a) +button_2_mode_b.on_clicked(set_device_2_mode_b) +button_reset.on_clicked(reset_data) # Reset-Button an die Funktion binden +button_pause.on_clicked(toggle_pause) # Pause-Button an die Funktion binden + +# Funktion, die ausgeführt wird, wenn das Fenster geschlossen wird +def on_close(event): + global stop_threads + stop_threads = True # Threads stoppen + plt.close('all') + +# Event-Handler für das Schließen des Fensters binden +fig.canvas.mpl_connect('close_event', on_close) + +# Funktion zum Mouseover-Event +def on_mouse_move(event): + if event.inaxes == ax: # Überprüfen, ob die Maus innerhalb der Achsen ist + # Überprüfen, ob ein Punkt in der Nähe ist + for i, (x, y) in enumerate(zip(range(len(data_1)), data_1)): + if abs(event.xdata - x) < 0.2 and abs(event.ydata - y) < 0.2: + annotation.xy = (x, y) + annotation.set_text(f"{Geraet1}: {y:.2f}") + annotation.set_visible(True) + break + else: # Nur wenn kein Punkt gefunden wurde, wird die Annotation ausgeblendet + annotation.set_visible(False) + # Überprüfen für Gerät 2 + for i, (x, y) in enumerate(zip(range(len(data_2)), data_2)): + if abs(event.xdata - x) < 0.2 and abs(event.ydata - y) < 0.2: + annotation.xy = (x, y) + annotation.set_text(f"{Geraet2}: {y:.2f}") + annotation.set_visible(True) + break + + fig.canvas.draw_idle() # Zeichne das Fenster neu + +# Event-Handler für Mouseover binden +fig.canvas.mpl_connect('motion_notify_event', on_mouse_move) + +# Endlos-Schleife zur Aktualisierung des Plots +try: + while not stop_threads: + if not pause_plotting: + update_plot() + plt.pause(1) +finally: + # Sauberes Beenden der Threads + stop_threads = True + thread_1.join() + thread_2.join() + print("Programm beendet.") diff --git a/bm869s.py b/bm869s.py new file mode 100644 index 0000000..42b9e00 --- /dev/null +++ b/bm869s.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +# MIT License +# +# Copyright (c) 2021 TheHWcave +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +# +# Implements a class that can read Brymen BM869S (and similar) meters +# using the Brymen USB interface cable +# +import argparse +import hid, sys +from time import sleep, time, localtime, strftime, perf_counter + +VID = 0x0820 +PID = 0x0001 + +# +# Note: +# The remote interface of the BM meters is actually a copy +# of the segments activated on the LCD display. This means for +# the values we have to convert 7-segment back into numbers. +# and interpret the various annouciators on the LCD. This makes +# unfortunately for a very dense piece of software. You need to +# consult the Brymen documentation of the LCD to understand what +# is going on... +# +# aaaaa +# f b +# f b +# ggggg +# e c +# e c +# xx ddddd +# +# bgcdafex +SEVSEG = {'00000000': ' ', + '00000001': '. ', + '10111110': ' 0', + '10111111': '.0', + '10100000': ' 1', + '10100001': '.1', + '11011010': ' 2', + '11011011': '.2', + '11111000': ' 3', + '11111001': '.3', + '11100100': ' 4', + '11100101': '.4', + '01111100': ' 5', + '01111101': '.5', + '01111110': ' 6', + '01111111': '.6', + '10101000': ' 7', + '10101001': '.7', + '11111110': ' 8', + '11111111': '.8', + '11111100': ' 9', + '11111101': '.9', + '00010110': ' L', + '00010111': '.L', + '11110010': ' d', + '00100000': ' i', + '01110010': ' o', + '11110010': ' d', + '01100010': ' n', + '01011110': ' E', + '01000010': ' r', + '00011110': ' C', + '01001110': ' F'} + + +class BM869S: + _h: None + _DBYTES = bytearray(24) + _DBITS = ['00000000'] * 24 + _mdsp = '' + _mmode = '' + _sdsp = '' + _smode = '' + _msg = '\x00\x00\x86\x66' + + def __init__(self): + + self._h = hid.Device(VID, PID) + + def Store(self, chunk, data): + # print(str(chunk)+' '+str(len(data))+':',end='') + # for b in data: + # print(hex(b)+' ',end='') + # print() + self._DBYTES[8 * chunk:8 * chunk + 7] = data + n = 0 + for b in data: + self._DBITS[8 * chunk + n] = format(b, '08b') + n = n + 1 + + def Decode(self): + # print(self._DBITS) + + # ------------------------------------------------------------------ + # main display mode and range + self._mmode = '' + if self._DBITS[1][3] == '1' and self._DBITS[2][7] == '1': + self._mmode = 'AC+DC ' + elif self._DBITS[1][3] == '1': + self._mmode = 'DC ' + elif self._DBITS[2][7] == '1': + self._mmode = 'AC ' + if self._mmode != '': + if self._DBITS[15][4] == '1': self._mmode = self._mmode + 'u' + if self._DBITS[15][5] == '1': self._mmode = self._mmode + 'm' + if self._DBITS[14][0] == '1': self._mmode = self._mmode + 'A' + if self._DBITS[8][7] == '1': self._mmode = self._mmode + 'V' + else: + if self._DBITS[15][7] == '1': + self._mmode = self._mmode + 'HZ' + if self._DBITS[15][1] == '1': self._mmode = 'k' + self._mmode + if self._DBITS[15][2] == '1': self._mmode = 'M' + self._mmode + elif self._DBITS[15][6] == '1': + self._mmode = self._mmode + 'dB' + elif self._DBITS[15][0] == '1': + self._mmode = self._mmode + 'D%' + if self._mmode == '': + if self._DBITS[2][5] == '1': + self._mmode = 'T1-T2' + elif self._DBITS[2][4] == '1': + self._mmode = 'T2' + elif self._DBITS[2][6] == '1': + self._mmode = 'T1' + if self._mmode == '': + if self._DBITS[15][3] == '1': + self._mmode = 'OHM' + if self._DBITS[15][1] == '1': self._mmode = 'k' + self._mmode + if self._DBITS[15][2] == '1': self._mmode = 'M' + self._mmode + + elif self._DBITS[14][2] == '1': + self._mmode = 'F' + if self._DBITS[14][1] == '1': self._mmode = 'n' + self._mmode + if self._DBITS[15][4] == '1': self._mmode = 'u' + self._mmode + if self._DBITS[15][5] == '1': self._mmode = 'm' + self._mmode + elif self._DBITS[14][3] == '1': + self._mmode = 'S' + if self._DBITS[14][1] == '1': self._mmode = 'n' + self._mmode + # ------------------------------------------------------------------ + # secondary display mode and range + + self._smode = '' + if self._DBITS[9][2] == '1': + self._smode = 'AC ' + elif self._DBITS[14][4] == '1' or self._DBITS[9][5] == '1' or self._DBITS[9][4] == '1': + self._smode = 'DC ' + if self._smode != '': + if self._DBITS[9][7] == '1': self._smode = self._smode + 'u' + if self._DBITS[9][6] == '1': self._smode = self._smode + 'm' + if self._DBITS[9][5] == '1': self._smode = self._smode + 'A' + if self._DBITS[9][4] == '1': self._smode = self._smode + '%4-20mA' + if self._DBITS[14][4] == '1': self._smode = self._smode + 'V' + else: + if self._DBITS[14][5] == '1': + self._smode = self._smode + 'HZ' + if self._DBITS[14][6] == '1': self._smode = 'k' + self._smode + if self._DBITS[14][7] == '1': self._smode = 'M' + self._smode + if self._smode == '': + if self._DBITS[9][1] == '1': self._smode = 'T2' + + # ------------------------------------------------------------------ + # signs for main and secondary displays + if self._DBITS[2][0] == '1': + self._mdsp = '-' + else: + self._mdsp = '' + + if self._DBITS[9][3] == '1': + self._sdsp = '-' + else: + self._sdsp = '' + # ------------------------------------------------------------------ + # main display digits + for n in range(3, 9): + v = self._DBITS[n] + # print(str(n)+' '+v+' = ',end='') + if v in SEVSEG: + digit = SEVSEG[v] + else: + digit = ' ?' + if digit[0] == ' ' or (n == 3) or (n == 8): + self._mdsp = self._mdsp + digit[1] + else: + self._mdsp = self._mdsp + digit + if self._mmode.startswith('T'): + self._mmode = self._mmode + ' ' + self._mdsp[-1:] + self._mdsp = self._mdsp[:-1] + # print(self._mdsp+' '+self._mmode) + # ------------------------------------------------------------------ + # secondary display digits + for n in range(10, 14): + v = self._DBITS[n] + # print(str(n)+' '+v+' = ',end='') + if v in SEVSEG: + digit = SEVSEG[v] + else: + digit = ' ?' + if digit[0] == ' ' or n == 10: + self._sdsp = self._sdsp + digit[1] + else: + self._sdsp = self._sdsp + digit + # print(self._sdsp+' '+self._smode) + return (self._mdsp, self._mmode, self._sdsp, self._smode) + + def readdata(self): + """ + returns the data from the BM869S in form of a list with 4 entries + entry + 0: the number as shown on the main display of the BM869S + 1: the unit&mode belonging to the main display, e.g. "mVDC" or "OHM" + 2: the number as shown on the secondary display (or blank if no secondary display is active) + 3: the unit&mode belonging to the secondary display, e.g. "HZ" (or blank) + """ + self._h.write(self._msg.encode('latin1')) + chunk = 0 + res = '' + Done = False + while not Done: + x = self._h.read(24, 4000) + if len(x) > 0: + self.Store(chunk, x) + chunk = chunk + 1 + if chunk > 2: + Done = True + res = self.Decode() + return res + + +if __name__ == "__main__": + # + # This implements a sample implementation in form of a logger + # it reads the BM869s periodically, determined by the --time setting + # and writes the data from primary and secondary displays into a + # a CSV file (and shows them on the screen) + # + + parser = argparse.ArgumentParser() + + parser.add_argument('--out', '-o', help='output filename (default=BM869s_.csv)', + dest='out_name', action='store', type=str, default='!') + parser.add_argument('--time', '-t', help='interval time in seconds between measurements (def=1.0)', + dest='int_time', action='store', type=float, default=1.0) + + arg = parser.parse_args() + + BM = BM869S() + PRI_READING = 0 + PRI_UNIT = 1 + SEC_READING = 2 + SEC_UNIT = 3 + + if arg.out_name == '!': + out_name = 'BM869s_' + strftime('%Y%m%d%H%M%S', localtime()) + '.csv' + else: + out_name = arg.out_name + + f = open(out_name, 'w') + f.write('Time[S],Main,Main unit,Secondary,Secondary Unit\n') + start = perf_counter() + now = perf_counter() - start + try: + while True: + now = perf_counter() - start + meas = BM.readdata() + s = '{:5.1f},{:s},{:s},{:s},{:s}'.format( + now, + meas[PRI_READING], meas[PRI_UNIT], + meas[SEC_READING], meas[SEC_UNIT]) + + f.write(s + '\n') + print(s) + elapsed = (perf_counter() - start) - now + if elapsed < arg.int_time: + sleep(arg.int_time - elapsed) + except KeyboardInterrupt: + f.close() \ No newline at end of file diff --git a/demo-hp3478a-1.py b/demo-hp3478a-1.py index 71f5e6b..f096fea 100644 --- a/demo-hp3478a-1.py +++ b/demo-hp3478a-1.py @@ -2,7 +2,7 @@ from hp3478a import hp3478a from time import sleep -port = "COM11" +port = "COM12" test = hp3478a(23, port, debug=False) @@ -13,13 +13,14 @@ print(test.getDigits(test.status.digits)) print(test.getFunction(test.status.function)) print(test.getRange(test.status.digits)) +""" print(test.setFunction(test.Ω2W)) print(test.setTrigger(test.TRIG_INT)) print(test.setRange("300")) #print(test.setDigits(3.5)) -for x in range(6): -""" +#for x in range(6): + print(test.getMeasure()) #print(test.setRange("A")) diff --git a/demo-pm2534-1.py b/demo-pm2534-1.py index 48ec32d..ab3f9b9 100644 --- a/demo-pm2534-1.py +++ b/demo-pm2534-1.py @@ -3,22 +3,25 @@ port = "COM11" -test = pm2534(23, port, debug=True) -#test.callReset() +test = pm2534(22, port, debug=True) +test.callReset() #just a line added -""" -test.setDisplay("ADLERWEB.INFO") + +#test.setDisplay("ADLERWEB.INFO") + print(test.getStatus()) print(test.getDigits(test.status.digits)) -print(test.getFunction(test.status.function)) -print(test.getRange(test.status.digits)) -print(test.setFunction(test.Ω2W)) -print(test.setTrigger(test.TRIG_INT)) -print(test.setRange("300")) -#print(test.setDigits(3.5)) - -for x in range(6): -""" +#print(test.getFunction(test.status.function)) +#print(test.getRange(test.status.digits)) +print(test.setFunction(test.Functions.RTW)) +print(test.setTrigger(test.Triggers.K)) +print(test.setDigits(3)) +print(test.getDigits()) +print(test.setRange(3E0)) + + +#for x in range(6): + print(test.getMeasure()) #print(test.setRange("A")) diff --git a/demo-sdm3065x-1.py b/demo-sdm3065x-1.py index a927dc8..bb6516b 100644 --- a/demo-sdm3065x-1.py +++ b/demo-sdm3065x-1.py @@ -1,6 +1,6 @@ import sdm3065x -dmm = siglent_sdm3065x.SDM3065X('10.0.0.114') +dmm = sdm3065x.SDM3065X('10.0.0.114') dmm.reset() # setup: diff --git a/hp3478a.py b/hp3478a.py index 93c97a8..9ac6c82 100644 --- a/hp3478a.py +++ b/hp3478a.py @@ -26,7 +26,7 @@ class hp3478a(object): Ω4W = 4 ADC = 5 AAC = 6 - EXTΩ = 7 + TEM = 7 TRIG_INT = 1 TRIG_EXT = 2 diff --git a/pm2534.py b/pm2534.py index 63d1050..e568891 100644 --- a/pm2534.py +++ b/pm2534.py @@ -1,9 +1,15 @@ +from numpy.core.numeric import True_ +from numpy.f2py.auxfuncs import throw_error + from prologix import prologix from dataclasses import dataclass from time import sleep +from enum import Enum import datetime + + class pm2534(object): """Control Philips/Fluke PM2534 multimeters using a Prologix or a AR488 compatible dongle @@ -21,13 +27,26 @@ class pm2534(object): addr: int = None gpib: prologix = None - # VDC = 1 - # VAC = 2 - # Ω2W = 3 - # Ω4W = 4 - # ADC = 5 - # AAC = 6 - # EXTΩ = 7 + class Functions(Enum): + VDC = 1 + VAC = 2 + RTW = 3 + RFW = 4 + IDC = 5 + IAC = 6 + TDC = 7 + + + class Triggers(Enum): + I = 1 + B = 2 + E = 3 + K = 4 + + + #functions = ['VDC', 'VAC', 'RTW', 'RFW', 'IDC', 'IAC', 'TDC'] + + # TRIG_INT = 1 # TRIG_EXT = 2 @@ -39,18 +58,6 @@ class pm2534(object): class pm2534Status: """Current device status - Attributes - ---------- - function : int - numeric representation of currently used measurement function: - 1: DC Voltage - 2: AC Voltage - 3: 2-Wire Resistance - 4: 4-Wire Resistance - 5: DC Current - 6: AC Current - 7: Extended Ohms - see also: getFunction range : int numeric representation of currenly used measurement range: 1: 30mV DC, 300mV AC, 30Ω, 300mA, Extended Ohms @@ -119,7 +126,7 @@ class pm2534Status: fetched: datetime Date and time this status was updated """ - """ + function: int = None range: int = None @@ -145,7 +152,7 @@ class pm2534Status: errChecksum: bool = None dac: int = None fetched: datetime = None - """ + status = pm2534Status() @@ -204,7 +211,7 @@ def getMeasure(self) -> float: if measurement is None: return None - return float(measurement) + return float(measurement[6:]) def getDigits(self, digits: int = None) -> float: """Get a human readable representation of currently used resolution @@ -218,19 +225,9 @@ def getDigits(self, digits: int = None) -> float: Returns ------- - str|None - 3.5, 4.5 or 5.5 for the current resolution - None for invalid numbers """ - if digits is None: - digits = self.status.digits - - if digits == 1: - return 5.5 - elif digits == 2: - return 4.5 - elif digits == 3: - return 3.5 + status = self.gpib.cmdPoll("DIG ?", self.addr, binary=True) + return None def getFunction(self, function: int = None) -> str: @@ -245,15 +242,7 @@ def getFunction(self, function: int = None) -> str: Returns ------- - str|None - VDC for DC Volts - ADC for AC Volts - Ω2W for 2-Wire Resistance - Ω4W for 4-Wire Resistance - ADC for DC Current - AAC for AC Current - ExtΩ for extended Ohms - None for invalid numbers + Functions """ if function is None: @@ -264,15 +253,15 @@ def getFunction(self, function: int = None) -> str: elif function == 2: return "VAC" elif function == 3: - return "Ω2W" + return "RTW" elif function == 4: - return "Ω4W" + return "RFW" elif function == 5: - return "ADC" + return "IDC" elif function == 6: - return "AAC" + return "IAC" elif function == 7: - return "ExtΩ" + return "TDC" else: return None @@ -562,24 +551,7 @@ def setAutoZero(self, autoZero: bool, noUpdate: bool = False) -> bool: bool new status of autoZero; presumed status if `noUpdate` was True """ - setVal = 0 - if autoZero: setVal = 1 - - self.gpib.cmdWrite("Z" + str(autoZero), self.addr) - - if noUpdate: - if self.gpib.debug: - print(".. AutoZero changed to " + setVal + " without verification.") - return setVal - else: - self.getStatus() - if autoZero != self.status.autoZero: - print( - "!! Error while changing AutoZero - tried to set " + str(autoZero) + " but verification was " + str( - self.status.autoZero)) - elif self.gpib.debug: - print(".. AutoZero successfully changed to " + str(self.status.autoZero)) - return self.status.autoZero + raise Exception("Function not implemented yet!") def setDisplay(self, text: str = None, online: bool = True) -> bool: """Change device display @@ -609,40 +581,9 @@ def setDisplay(self, text: str = None, online: bool = True) -> bool: bool Wheather setting the text worked as expected """ - if text is None or text == "": - # Reset display - self.gpib.cmdWrite("D1", self.addr) - if self.gpib.debug: - print("Display reset to standard mode") - return True - - len = 0 - for c in text: - if ord(c) < 32 or ord(c) > 95: - print("!! Character '" + c + "' is not supported") - return False - if c != "," and c != ".": - len = len + 1 - - if len > 12: - print("!! Text too long; max 12 characters") - return False - - cmd = "D2" - dt = "" - if not online: - cmd = "D3" - dt = " (updates paused)" + raise Exception("Function not implemented yet!") - self.gpib.cmdWrite(cmd + text, self.addr) - - if self.gpib.debug: - print(".. Display changed to '" + text + "'" + dt) - - # @TODO we could check status/errors to catch syntax errors here - return True - - def setFunction(self, function: int, noUpdate: bool = False) -> bool: + def setFunction(self, function: Functions, noUpdate: bool = False) -> bool: """Change current measurement function Parameters @@ -660,110 +601,58 @@ def setFunction(self, function: int, noUpdate: bool = False) -> bool: bool Whether update succeeded or not; not verified if `noUpdate` was True """ - if function <= 0 or function > 7: - print("!! Invalid function") - return False - - self.gpib.cmdWrite("F" + str(function), self.addr) - - if not noUpdate: - self.getStatus() - if self.status.function != function: - print("!! Set failed. Tried to set " + self.getFunction( - function) + " but device returned " + self.getFunction(self.status.function)) - return False - elif self.gpib.debug: - print(".. Changed to function " + self.getFunction(function)) - elif self.gpib.debug: - print(".. Probably changed to function " + self.getFunction(function)) + if function in self.Functions: + self.gpib.cmdWrite("FNC " + str(function.name), self.addr) + """ + if not noUpdate: + self.getStatus() + if self.status.function != function: + print("!! Set failed. Tried to set " + self.getFunction( + function) + " but device returned " + self.getFunction(self.status.function)) + return False + elif self.gpib.debug: + print(".. Changed to function " + self.getFunction(function)) + elif self.gpib.debug: + print(".. Probably changed to function " + self.getFunction(function)) + """ + return True - return True + print("!! Invalid function") + return False - def setRange(self, range: str, noUpdate: bool = False) -> bool: + def setRange(self, range, noUpdate: bool = False) -> bool: """Change current measurement range - - Parameters - ---------- range : str|float Range as SI-Value or float Valid values: - A or AUTO to enable Auto-Range - 30m,300m,3,30,300,3k,30k,300k,3M,30M - 0.03,0.3,3,30,300,3000,30000,300000,3000000,30000000 + AUTO to enable Auto-Range + Not all ranges can be used in all measurement functions + VDC: 300E-3,3E0,30E0,300E0 + VAC: 300E-3,3E0,30E0,300E0 + RTW: 3E3,30E3,300E3,3E6,30E6,300E6 + RFW: 3E3,30E3,300E3,3E6 + IDC: 30E-3, 3E0 + IAC: 30E-3, 3E0 + TDC: 0 noUpdate : bool, optional If True do not update status object to verify change was successful by default False - Returns ------- bool Whether update succeeded or not; not verified if `noUpdate` was True """ - newRange = None - newRangeF = None - if range == "30m" or range == 0.03: - newRange = -2 - newRangeF = 0.03 - elif range == "300m" or range == 0.3: - newRange = -1 - newRangeF = 0.3 - elif range == "3" or range == 3: - newRange = 0 - newRangeF = 3 - elif range == "30" or range == 30: - newRange = 1 - newRangeF = 30 - elif range == "300" or range == 300: - newRange = 2 - newRangeF = 300 - elif range == "3k" or range == 3000: - newRange = 3 - newRangeF = 3000 - elif range == "30k" or range == 30000: - newRange = 4 - newRangeF = 30000 - elif range == "300k" or range == 300000: - newRange = 5 - newRangeF = 300000 - elif range == "3M" or range == 3000000: - newRange = 6 - newRangeF = 3000000 - elif range == "30M" or range == 30000000: - newRange = 7 - newRangeF = 30000000 - elif range.lower() == "a" or range.lower() == "auto": - newRange = "A" - - if newRange is None: - print("!! Invalid range") - return False - - self.gpib.cmdWrite("R" + str(newRange), self.addr) - - if not noUpdate: - self.getStatus() - if newRange == "A": - if not self.status.autoRange: - print("!! Tried to enable Auto-Range but device refused") - return False - elif self.gpib.debug: - print(".. Enabled Auto-Range") - else: - newRangeC = self.getRange(numeric=True) - if newRangeF != newRangeC: - print("!! Tried to set range to " + str(range) + " but device reported " + self.getRange()) - return False - elif self.gpib.debug: - print(".. Set range to " + self.getRange()) - elif self.gpib.debug: - print(".. Probably changed to range " + str(range)) - - return True + if range=='AUTO': + self.gpib.cmdWrite("RNG " + range, self.addr) + return True + else: + self.gpib.cmdWrite('RNG {:1.3E}'.format(range)) + return True + return False - def setDigits(self, digits: float, noUpdate: bool = False) -> bool: + def setDigits(self, digits: int, noUpdate: bool = False) -> bool: """Change current measurement resolution - Parameters ---------- digits : float @@ -778,44 +667,18 @@ def setDigits(self, digits: float, noUpdate: bool = False) -> bool: bool Whether update succeeded or not; not verified if `noUpdate` was True """ - newDigits = None - if digits == 3 or digits == 3.5: - newDigits = "3" - elif digits == 4 or digits == 4.5: - newDigits = "4" - elif digits == 5 or digits == 5.5: - newDigits = "5" - else: - print("!! Invalid digits") - return False - - self.gpib.cmdWrite("N" + newDigits, self.addr) - - if not noUpdate: - self.getStatus() - if int(self.getDigits()) != int(newDigits): - print("!! Tried to set digits to " + str(int(newDigits)) + "½ but device reported " + str( - int(self.getDigits())) + "½") - return False - elif self.gpib.debug: - print(".. Set digits to " + str(int(self.getDigits())) + "½") - elif self.gpib.debug: - print(".. Probably changed digits to " + str(int(self.getDigits())) + "½") - - return True + if digits in range(1,7): + self.gpib.cmdWrite("DIG " + str(digits), self.addr) + return True + return False - def setTrigger(self, trigger: int, noUpdate: bool = False) -> bool: + def setTrigger(self, trigger: Triggers, noUpdate: bool = False) -> bool: """Change current measurement trigger Parameters ---------- - trigger : int - desired trigger mode - 1 or TRIG_INT -> Automatic internal trigger - 2 or TRIG_EXT -> Abort current measurements; Start on LOW-edge or via GPIB - 3 or TRIG_SIN -> Complete current measurement. New one on GPIB GET - 4 or TRIG_HLD -> Abort current measurement. New one on GPIB GET - 5 or TRIG_FST -> Same as TRIG_SIN but skip settling delay for AC + trigger : Triggers + different kinds of Trigger noUpdate : bool, optional If True do not update status object to verify change was successful @@ -826,31 +689,10 @@ def setTrigger(self, trigger: int, noUpdate: bool = False) -> bool: bool Whether update succeeded or not; not verified if `noUpdate` was True """ - if trigger <= 0 or trigger > 5: - print("!! Invalid digits") - return False - - self.gpib.cmdWrite("T" + str(trigger), self.addr) - - if not noUpdate: - self.getStatus() - if trigger == self.TRIG_EXT and not self.status.triggerExternal: - print("!! Tried to enable external trigger but flag did not update") - return False - elif trigger == self.TRIG_SIN and self.status.triggerInternal: - print("!! Tried to enable singe trigger but auto trigger flag is still active") - return False - elif trigger == self.TRIG_INT and not self.status.triggerInternal: - print("!! Tried to enable internal trigger but auto trigger flag is not active") - return False - elif trigger == self.TRIG_HLD and self.status.triggerInternal: - print("!! Tried to enable trigger hold but auto trigger flag is still active") - return False - - if self.gpib.debug: - print(".. Probably changed trigger to " + str(trigger)) - - return True + if trigger in self.Triggers: + self.gpib.cmdWrite("TRG " + str(trigger.name), self.addr) + return True + return False def setSRQ(self, srq: int): """Set Serial Poll Register Mask @@ -863,12 +705,12 @@ def setSRQ(self, srq: int): Parameter must be two digits exactly. Bits 0-5 of the binary representation are used to set the mask """ - self.gpib.cmdWrite(srq, self.addr) + raise Exception("Function not implemented yet!") def clearSPR(self): """Clear Serial Poll Register (SPR) """ - self.gpib.cmdWrite("K", self.addr) + raise Exception("Function not implemented yet!") def clearERR(self) -> bytearray: """Clear Error Registers @@ -878,7 +720,7 @@ def clearERR(self) -> bytearray: bytearray Error register as octal digits """ - return self.gpib.cmdPoll("E", self.addr, binary=True) + raise Exception("Function not implemented yet!") def callReset(self): """Reset the device From 4a39fb83d2b698f163a5d9c63b9f8fc156b7433d Mon Sep 17 00:00:00 2001 From: SrR0 Date: Sat, 21 Sep 2024 23:11:57 +0200 Subject: [PATCH 6/6] Changed Plot into Timescale on x-Axis Fixed Bug on PM2534 due to short timeout on reading when in 6.5 Digit mode --- MeasureAll.py | 121 +++++++++++++++++++------------------------ demo-pm2534-1.py | 23 +++++---- pm2534.py | 132 ++++++----------------------------------------- 3 files changed, 79 insertions(+), 197 deletions(-) diff --git a/MeasureAll.py b/MeasureAll.py index 09471de..5a904fc 100644 --- a/MeasureAll.py +++ b/MeasureAll.py @@ -7,8 +7,11 @@ from hp3478a import hp3478a # Initialisiere die Messgeräte (pm2534 und hp3478a) -device_1 = pm2534(22, "COM11", debug=False) +device_1 = pm2534(22, "COM11", debug=True) +device_1.setSpeed(device_1.Speeds.Speed1) +device_1.setRange(3E0) device_2 = hp3478a(23, "COM12", debug=False) +device_2.setRange("3") Geraet1 = 'PM2534' Geraet2 = 'HP3478A' @@ -18,8 +21,8 @@ mode_device_2 = "Mode A" # Datenpuffer für jedes Messgerät (deque für die Echtzeitdaten) -data_1 = deque(maxlen=100) -data_2 = deque(maxlen=100) +data_1 = deque(maxlen=100) # (time, value) +data_2 = deque(maxlen=100) # (time, value) # Steuerung für das Beenden der Threads stop_threads = False @@ -27,8 +30,8 @@ # Echtzeitplot mit Matplotlib (muss im Hauptthread laufen) plt.ion() # Interaktiver Modus für Echtzeitplot -fig, (ax, ax_hist) = plt.subplots(2, 1, gridspec_kw={'height_ratios': [2, 1]}) -plt.subplots_adjust(bottom=0.4, hspace=0.4) # Platz für Schaltflächen und Abstand zwischen Plots +fig, ax = plt.subplots() # Nur eine Achse ohne Histogramm +plt.subplots_adjust(bottom=0.4) # Platz für Schaltflächen # Annotation für Mouseover annotation = ax.annotate("", xy=(0,0), xytext=(20,20), @@ -37,21 +40,32 @@ arrowprops=dict(arrowstyle="->")) annotation.set_visible(False) +# Funktion zur Berechnung der Zeitdifferenz in Sekunden +def time_diff(start_time): + return [t - start_time for t, _ in data_1], [t - start_time for t, _ in data_2] + # Hauptplot-Funktion, die die Werte in Echtzeit darstellt def update_plot(): ax.clear() - ax.plot(data_1, label=Geraet1, marker='*', linestyle='-') # Plot für Gerät 1 mit Sternen - ax.plot(data_2, label=Geraet2, marker='*', linestyle='-') # Plot für Gerät 2 mit Sternen - ax.legend() - ax.grid(True) # Gittermuster aktivieren - - # Histogramm aktualisieren - ax_hist.clear() - ax_hist.hist(data_1, bins=10, alpha=0.5, label=Geraet1) # Histogramm für Gerät 1 - ax_hist.hist(data_2, bins=10, alpha=0.5, label=Geraet2) # Histogramm für Gerät 2 - ax_hist.legend() - ax_hist.set_title('Histogramm der Messwerte') - ax_hist.grid(True) + + if data_1 and data_2: + start_time = data_1[0][0] # Starte von der ersten Messung des ersten Geräts + + # Extrahiere Zeiten und Werte + times_1, values_1 = zip(*data_1) + times_2, values_2 = zip(*data_2) + + # Konvertiere Zeiten in Differenzen zur ersten Messung (in Sekunden) + times_1 = [t - start_time for t in times_1] + times_2 = [t - start_time for t in times_2] + + ax.plot(times_1, values_1, label=Geraet1, marker='*', linestyle='-') + ax.plot(times_2, values_2, label=Geraet2, marker='*', linestyle='-') + + ax.legend() + ax.set_xlabel('Zeit (s)') + ax.set_ylabel('Messwert') + ax.grid(True) plt.draw() @@ -60,17 +74,17 @@ def read_device1(device, data_queue, mode): global stop_threads while not stop_threads: value = device.getMeasure() - data_queue.append(value) - print(f"Device 1 running in {mode}") - time.sleep(1) + timestamp = time.time() # Erfasse die aktuelle Zeit + data_queue.append((timestamp, value)) # Speichere Zeit und Wert + time.sleep(3) def read_device2(device, data_queue, mode): global stop_threads while not stop_threads: value = device.getMeasure() - data_queue.append(value) - print(f"Device 2 running in {mode}") - time.sleep(1) + timestamp = time.time() # Erfasse die aktuelle Zeit + data_queue.append((timestamp, value)) # Speichere Zeit und Wert + time.sleep(3) # Threads für jedes Gerät starten thread_1 = threading.Thread(target=read_device1, args=(device_1, data_1, mode_device_1)) @@ -79,41 +93,12 @@ def read_device2(device, data_queue, mode): thread_2.start() # Schaltflächen für das Umschalten der Betriebsmodi und Pausieren -ax_button_1_mode_a = plt.axes([0.1, 0.25, 0.1, 0.075]) -ax_button_1_mode_b = plt.axes([0.25, 0.25, 0.1, 0.075]) -ax_button_2_mode_a = plt.axes([0.55, 0.25, 0.1, 0.075]) -ax_button_2_mode_b = plt.axes([0.7, 0.25, 0.1, 0.075]) -ax_button_reset = plt.axes([0.4, 0.05, 0.2, 0.075]) # Position für den Reset-Button -ax_button_pause = plt.axes([0.1, 0.1, 0.2, 0.075]) # Position für den Pause-Button - -button_1_mode_a = Button(ax_button_1_mode_a, 'Gerät 1 Mode A') -button_1_mode_b = Button(ax_button_1_mode_b, 'Gerät 1 Mode B') -button_2_mode_a = Button(ax_button_2_mode_a, 'Gerät 2 Mode A') -button_2_mode_b = Button(ax_button_2_mode_b, 'Gerät 2 Mode B') +ax_button_reset = plt.axes([0.4, 0.1, 0.15, 0.075]) # Position für den Reset-Button +ax_button_pause = plt.axes([0.1, 0.1, 0.15, 0.075]) # Position für den Pause-Button + button_reset = Button(ax_button_reset, 'Reset Daten') # Reset-Button button_pause = Button(ax_button_pause, 'Pause Plotting') # Pause-Button -# Callback-Funktionen, die den Modus ändern -def set_device_1_mode_a(event): - global mode_device_1 - mode_device_1 = "Mode A" - print("Gerät 1 auf Mode A umgeschaltet") - -def set_device_1_mode_b(event): - global mode_device_1 - mode_device_1 = "Mode B" - print("Gerät 1 auf Mode B umgeschaltet") - -def set_device_2_mode_a(event): - global mode_device_2 - mode_device_2 = "Mode A" - print("Gerät 2 auf Mode A umgeschaltet") - -def set_device_2_mode_b(event): - global mode_device_2 - mode_device_2 = "Mode B" - print("Gerät 2 auf Mode B umgeschaltet") - # Funktion zum Zurücksetzen der Datenpuffer def reset_data(event): global data_1, data_2 @@ -129,10 +114,6 @@ def toggle_pause(event): print("Neuzeichnen der Grafik pausiert." if pause_plotting else "Neuzeichnen der Grafik fortgesetzt.") # Binde die Schaltflächen an die Callback-Funktionen -button_1_mode_a.on_clicked(set_device_1_mode_a) -button_1_mode_b.on_clicked(set_device_1_mode_b) -button_2_mode_a.on_clicked(set_device_2_mode_a) -button_2_mode_b.on_clicked(set_device_2_mode_b) button_reset.on_clicked(reset_data) # Reset-Button an die Funktion binden button_pause.on_clicked(toggle_pause) # Pause-Button an die Funktion binden @@ -145,27 +126,29 @@ def on_close(event): # Event-Handler für das Schließen des Fensters binden fig.canvas.mpl_connect('close_event', on_close) -# Funktion zum Mouseover-Event def on_mouse_move(event): if event.inaxes == ax: # Überprüfen, ob die Maus innerhalb der Achsen ist - # Überprüfen, ob ein Punkt in der Nähe ist - for i, (x, y) in enumerate(zip(range(len(data_1)), data_1)): - if abs(event.xdata - x) < 0.2 and abs(event.ydata - y) < 0.2: - annotation.xy = (x, y) + # Überprüfen, ob ein Punkt in der Nähe ist (für Gerät 1) + for i, (t, y) in enumerate(data_1): + if abs(event.xdata - (t - data_1[0][0])) < 0.2 and abs(event.ydata - y) < 0.2: + annotation.xy = (t - data_1[0][0], y) annotation.set_text(f"{Geraet1}: {y:.2f}") annotation.set_visible(True) break else: # Nur wenn kein Punkt gefunden wurde, wird die Annotation ausgeblendet annotation.set_visible(False) + # Überprüfen für Gerät 2 - for i, (x, y) in enumerate(zip(range(len(data_2)), data_2)): - if abs(event.xdata - x) < 0.2 and abs(event.ydata - y) < 0.2: - annotation.xy = (x, y) + for i, (t, y) in enumerate(data_2): + if abs(event.xdata - (t - data_2[0][0])) < 0.2 and abs(event.ydata - y) < 0.2: + annotation.xy = (t - data_2[0][0], y) annotation.set_text(f"{Geraet2}: {y:.2f}") annotation.set_visible(True) break + else: + annotation.set_visible(False) - fig.canvas.draw_idle() # Zeichne das Fenster neu + fig.canvas.draw_idle() # Aktualisiere die Darstellung # Event-Handler für Mouseover binden fig.canvas.mpl_connect('motion_notify_event', on_mouse_move) @@ -175,7 +158,7 @@ def on_mouse_move(event): while not stop_threads: if not pause_plotting: update_plot() - plt.pause(1) + plt.pause(2) finally: # Sauberes Beenden der Threads stop_threads = True diff --git a/demo-pm2534-1.py b/demo-pm2534-1.py index ab3f9b9..c3d637d 100644 --- a/demo-pm2534-1.py +++ b/demo-pm2534-1.py @@ -1,3 +1,5 @@ +import time + from pm2534 import pm2534 from time import sleep @@ -10,19 +12,18 @@ #test.setDisplay("ADLERWEB.INFO") print(test.getStatus()) -print(test.getDigits(test.status.digits)) +#print(test.getDigits(test.status.digits)) #print(test.getFunction(test.status.function)) #print(test.getRange(test.status.digits)) -print(test.setFunction(test.Functions.RTW)) -print(test.setTrigger(test.Triggers.K)) -print(test.setDigits(3)) -print(test.getDigits()) -print(test.setRange(3E0)) - - -#for x in range(6): - -print(test.getMeasure()) +print(test.setFunction(test.Functions.VDC)) +#print(test.setTrigger(test.Triggers.K)) +print(test.setSpeed(test.Speeds.Speed1)) +#print(test.getDigits()) +print(test.setRange(30E0)) + +for x in range(100): + print(test.getMeasure()) + time.sleep(3) #print(test.setRange("A")) #print(test.setDigits(5)) diff --git a/pm2534.py b/pm2534.py index e568891..a3cea92 100644 --- a/pm2534.py +++ b/pm2534.py @@ -43,6 +43,12 @@ class Triggers(Enum): E = 3 K = 4 + class Speeds(Enum): + Speed1 = 1 + Speed2 = 2 + Speed3 = 3 + Speed4 = 4 + #functions = ['VDC', 'VAC', 'RTW', 'RFW', 'IDC', 'IAC', 'TDC'] @@ -156,7 +162,7 @@ class pm2534Status: status = pm2534Status() - def __init__(self, addr: int, port: str = None, baud: int = 115200, timeout: float = 0.25, + def __init__(self, addr: int, port: str = None, baud: int = 115200, timeout: float = 0.5, prologixGpib: prologix = None, debug: bool = False): """ @@ -209,6 +215,7 @@ def getMeasure(self) -> float: measurement = self.gpib.cmdPoll(" ", self.addr) if measurement is None: + #self.gpib.cmdClr() return None return float(measurement[6:]) @@ -287,122 +294,7 @@ def getRange(self, range: int = None, function: int = None, numeric: bool = Fals str|float|None Maximum measurement value in current range """ - if range is None: - range = self.status.range - if function is None: - function = self.status.function - - if range == 1: - if function == 1: - if numeric: - return 0.03 - else: - return "30mV" - elif function == 2: - if numeric: - return 0.3 - else: - return "300mV" - elif function == 3 or function == 4: - if numeric: - return 30.0 - else: - return "30Ω" - elif function == 5 or function == 6: - if numeric: - return 0.3 - else: - return "300mA" - else: - return None - elif range == 2: - if function == 1: - if numeric: - return 0.3 - else: - return "300mV" - elif function == 2: - if numeric: - return 3.0 - else: - return "3V" - elif function == 3 or function == 4: - if numeric: - return 300.0 - else: - return "300Ω" - elif function == 5 or function == 6: - if numeric: - return 3.0 - else: - return "3A" - else: - return None - elif range == 3: - if function == 1: - if numeric: - return 3.0 - else: - return "3V" - elif function == 2: - if numeric: - return 30.0 - else: - return "30V" - elif function == 3 or function == 4: - if numeric: - return 3000.0 - else: - return "3kΩ" - else: - return None - elif range == 4: - if function == 1: - if numeric: - return 30.0 - else: - return "30V" - elif function == 2: - if numeric: - return 300.0 - else: - return "300V" - elif function == 3 or function == 4: - if numeric: - return 30000.0 - else: - return "30kΩ" - else: - return None - elif range == 5: - if function == 1: - if numeric: - return 300.0 - else: - return "300V" - elif function == 3 or function == 4: - if numeric: - return 300000.0 - else: - return "300kΩ" - else: - return None - elif range == 6: - if function == 3 or function == 4: - if numeric: - return 3000000.0 - else: - return "3MΩ" - else: - return None - elif range == 7: - if function == 3 or function == 4: - if numeric: - return 30000000.0 - else: - return "30MΩ" - else: - return None + raise Exception("Function not implemented yet!") def getStatus(self) -> pm2534Status: """Read current device status and populate status object @@ -694,6 +586,12 @@ def setTrigger(self, trigger: Triggers, noUpdate: bool = False) -> bool: return True return False + def setSpeed(self, speed:Speeds, noUpdate: bool = False) -> bool: + if speed in self.Speeds: + self.gpib.cmdWrite("MSP " + str(speed.value), self.addr) + return True + return False + def setSRQ(self, srq: int): """Set Serial Poll Register Mask