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/MeasureAll.py b/MeasureAll.py
new file mode 100644
index 0000000..5a904fc
--- /dev/null
+++ b/MeasureAll.py
@@ -0,0 +1,167 @@
+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=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'
+
+# 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) # (time, value)
+data_2 = deque(maxlen=100) # (time, value)
+
+# 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 = 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),
+ textcoords="offset points",
+ bbox=dict(boxstyle="round", fc="w"),
+ 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()
+
+ 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()
+
+# 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()
+ 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()
+ 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))
+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_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
+
+# 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_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)
+
+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 (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, (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() # Aktualisiere die Darstellung
+
+# 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(2)
+finally:
+ # Sauberes Beenden der Threads
+ stop_threads = True
+ thread_1.join()
+ thread_2.join()
+ print("Programm beendet.")
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
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/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.py b/demo-hp3478a-1.py
similarity index 57%
rename from demo.py
rename to demo-hp3478a-1.py
index 0ee07cd..f096fea 100644
--- a/demo.py
+++ b/demo-hp3478a-1.py
@@ -2,24 +2,28 @@
from hp3478a import hp3478a
from time import sleep
-port = "/dev/ttyACM0"
+port = "COM12"
-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))
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("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/demo-hp3478a-2.py b/demo-hp3478a-2.py
new file mode 100644
index 0000000..2162ded
--- /dev/null
+++ b/demo-hp3478a-2.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/demo-pm2534-1.py b/demo-pm2534-1.py
new file mode 100644
index 0000000..c3d637d
--- /dev/null
+++ b/demo-pm2534-1.py
@@ -0,0 +1,31 @@
+import time
+
+from pm2534 import pm2534
+from time import sleep
+
+port = "COM11"
+
+test = pm2534(22, port, debug=True)
+test.callReset()
+#just a line added
+
+#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.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))
+
+#test.getCalibration("calibration.data")
diff --git a/demo-sdm3065x-1.py b/demo-sdm3065x-1.py
new file mode 100644
index 0000000..bb6516b
--- /dev/null
+++ b/demo-sdm3065x-1.py
@@ -0,0 +1,12 @@
+import sdm3065x
+
+dmm = 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/hp3478a.py b/hp3478a.py
index 45158b7..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
@@ -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..a3cea92
--- /dev/null
+++ b/pm2534.py
@@ -0,0 +1,626 @@
+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
+
+ 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
+
+ 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
+
+ class Speeds(Enum):
+ Speed1 = 1
+ Speed2 = 2
+ Speed3 = 3
+ Speed4 = 4
+
+
+ #functions = ['VDC', 'VAC', 'RTW', 'RFW', 'IDC', 'IAC', 'TDC']
+
+
+
+ # TRIG_INT = 1
+ # TRIG_EXT = 2
+ # TRIG_SIN = 3
+ # TRIG_HLD = 4
+ # TRIG_FST = 5
+
+ @dataclass
+ class pm2534Status:
+ """Current device status
+
+ 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.5,
+ 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:
+ #self.gpib.cmdClr()
+ return None
+
+ return float(measurement[6:])
+
+ 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
+ -------
+ """
+ status = self.gpib.cmdPoll("DIG ?", self.addr, binary=True)
+
+ 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
+ -------
+ Functions
+
+ """
+ if function is None:
+ function = self.status.function
+
+ if function == 1:
+ return "VDC"
+ elif function == 2:
+ return "VAC"
+ elif function == 3:
+ return "RTW"
+ elif function == 4:
+ return "RFW"
+ elif function == 5:
+ return "IDC"
+ elif function == 6:
+ return "IAC"
+ elif function == 7:
+ return "TDC"
+ 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
+ """
+ raise Exception("Function not implemented yet!")
+
+ 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
+ """
+ raise Exception("Function not implemented yet!")
+
+ 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
+ """
+ raise Exception("Function not implemented yet!")
+
+ def setFunction(self, function: Functions, 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 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
+
+ print("!! Invalid function")
+ return False
+
+ def setRange(self, range, noUpdate: bool = False) -> bool:
+ """Change current measurement range
+ range : str|float
+ Range as SI-Value or float
+ Valid values:
+ 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
+ """
+ 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: int, 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
+ """
+ if digits in range(1,7):
+ self.gpib.cmdWrite("DIG " + str(digits), self.addr)
+ return True
+ return False
+
+ def setTrigger(self, trigger: Triggers, noUpdate: bool = False) -> bool:
+ """Change current measurement trigger
+
+ Parameters
+ ----------
+ trigger : Triggers
+ different kinds of Trigger
+
+ 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 in self.Triggers:
+ self.gpib.cmdWrite("TRG " + str(trigger.name), self.addr)
+ 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
+
+ @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
+ """
+ raise Exception("Function not implemented yet!")
+
+ def clearSPR(self):
+ """Clear Serial Poll Register (SPR)
+ """
+ raise Exception("Function not implemented yet!")
+
+ def clearERR(self) -> bytearray:
+ """Clear Error Registers
+
+ Returns
+ -------
+ bytearray
+ Error register as octal digits
+ """
+ raise Exception("Function not implemented yet!")
+
+ 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)
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()
+