Skip to content

Commit

Permalink
Add support for Agilent 34401A multimeter
Browse files Browse the repository at this point in the history
Each "instrument" slot now has a generic Multimeter associated with it,
which can be instantiated to one of the supported multimeters.

These two have completely different APIs, and very different
behaviours in their remote  interface.
  • Loading branch information
AlexanderWells-diamond committed Nov 15, 2024
1 parent a63056d commit 6cdcaf0
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 69 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

# psc_datalogger

Provide a GUI interface to allow logging voltages from one to three Agilent 3458A Multimeters.
Provide a GUI interface to allow logging measurements from one to three Multimeters. Supported multimeters are the
Agilent 3458A and 34401A devices.
Logging is done at a configurable interval. It can be also be configured to convert voltage readings into
a temperature, if a Type K Thermocouple is in use.
a temperature, if a Analog Devices AD8494 Thermocouple Amplifier is connected to the multimeter.
The data is output in a CSV format.

![GUI](images/gui.png)
Expand Down
Binary file modified images/gui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 35 additions & 34 deletions src/psc_datalogger/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@
from dataclasses import dataclass
from datetime import datetime
from threading import Event, RLock
from typing import List, Optional, TextIO, Tuple, cast
from typing import List, Optional, TextIO, Tuple, Type, cast

import pyvisa
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from pyvisa.resources import Resource, SerialInstrument

from .multimeter import Agilent3458A, InvalidNplcException, Multimeter
from .statusbar import StatusBar
from .temperature.converter import volts_to_celcius


class ConnectionManager:
"""Manage the connection to the instruments
NOTE: There is expected to only be 1 Prologix device connected, which
will talk to up to 3 Agilent3458A Multimeters"""
"""Manage the connection to the instruments, which is handled on a separate
thread."""

def __init__(self):
self.thread = QThread()
Expand Down Expand Up @@ -56,11 +55,12 @@ def set_instrument(
enabled: bool,
gpib_address: str,
measure_temp: bool,
multimeter_type: Type[Multimeter],
) -> None:
"""Configure the given instrument number with the provided parameters"""
try:
self._worker.set_instrument(
instrument_number, enabled, gpib_address, measure_temp
instrument_number, enabled, gpib_address, measure_temp, multimeter_type
)
except ConnectionNotInitialized:
logging.exception(
Expand Down Expand Up @@ -101,6 +101,8 @@ class InstrumentConfig:
address: int = -1
# Indicate whether the voltage read should be converted into a temperature
convert_to_temp: bool = False
# The multimeter in use. Default to 3458A, matching the GUI's default
multimeter: Multimeter = Agilent3458A()


class DataWriter(DictWriter):
Expand Down Expand Up @@ -220,6 +222,7 @@ def set_instrument(
enabled: bool,
gpib_address: str,
measure_temp: bool,
multimeter_type: Type[Multimeter],
) -> None:
"""Configure the given instrument number with the provided parameters"""
assert (
Expand All @@ -239,7 +242,7 @@ def set_instrument(
f"Address {gpib_address}, measure temp {measure_temp}"
)
self.instrument_configs[instrument_number] = InstrumentConfig(
enabled, address, measure_temp
enabled, address, measure_temp, multimeter_type()
)

self._init_instrument(self.instrument_configs[instrument_number])
Expand All @@ -261,23 +264,18 @@ def _init_instrument(self, instrument: InstrumentConfig) -> None:
f"_init_instrument called with invalid address '{gpib_address}'"
)

multimeter = instrument.multimeter

with self.lock:
self._set_prologix_address(gpib_address)

self._connection_write("PRESET NORM") # Set a variety of defaults
self._connection_write("BEEP 0") # Disable annoying beeps
multimeter.initialize(self._connection)

self._set_nplc(instrument)

self._connection_write("TRIG HOLD") # Disable triggering
# This means the instrument will stop collecting measurements, thus
# not filling its internal memory buffer. Later we will send single
# trigger events and immediately read it, thus keeping the buffer
# empty so we avoid reading stale results

# Finally, read all data remaining in the buffer; it is possible for
# samples to be taken in the time between us sending the various above
# commands
# Read all data remaining in the buffer; it is possible for
# samples to be taken while initializing the multimeters.
# (Mostly an issue with the 3458A but doesn't hurt for other types)
while self._connection_bytes_in_buffer():
try:
self._connection_read()
Expand All @@ -288,21 +286,25 @@ def _init_instrument(self, instrument: InstrumentConfig) -> None:

def set_nplc(self, nplc: str) -> None:
"""Set the NPLC for all configured instruments"""
nplc_int = int(nplc)
if not 1 <= nplc_int <= 2000:
self.error.emit(f"NPLC value {nplc_int} outside allowed range 1 - 2000")
try:
for instrument in self.instrument_configs.values():
if instrument.enabled:
self._set_nplc(instrument)
except InvalidNplcException as e:
self.error.emit(
f"NPLC value {self.nplc} outside allowed range "
f"{e.min_allowed} - {e.max_allowed}"
)
return
self.nplc = nplc_int

for instrument in self.instrument_configs.values():
if instrument.enabled:
self._set_nplc(instrument)

def _set_nplc(self, instrument: InstrumentConfig) -> None:
"""Set the NPLC for the given instrument"""
with self.lock:
self._set_prologix_address(instrument.address)
self._connection_write(f"NPLC {self.nplc}")

multimeter = instrument.multimeter

multimeter.set_nplc(self._connection, self.nplc)

# The number of samples is tied to the electrical frequency.
# i.e. an NPLC of 50 will take 1 second, as our mains runs at 50Hz.
Expand All @@ -316,11 +318,7 @@ def _set_prologix_address(self, gpib_address: int) -> None:
"""Configure the Prologix to point to the given GPIB address"""
with self.lock:
self._connection_write(f"++addr {gpib_address}")
# Instruct Prologix to enable read-after-write,
# which allows the controller to write data back to us!
self._connection_write("++auto 1")

# Prologix seems to need a moment to process previous commands
# Prologix seems to need a moment to process previous command
time.sleep(0.1)

def validate_parameters(self) -> bool:
Expand Down Expand Up @@ -468,8 +466,11 @@ def query_instruments(self) -> Tuple[datetime, str, str, str]:
self._connection_write(f"++addr {i.address}")

logging.debug(f"Triggering instrument {i.address}")

multimeter = i.multimeter

# Request a single measurement
val = self._connection_query("TRIG SGL")
val = multimeter.take_reading(self._connection)

logging.debug(f"Address {i.address} Value {val}")

Expand Down Expand Up @@ -506,7 +507,7 @@ def _exit(self) -> None:
if self.writer:
self.writer.close()
# Send relevant flags to allow run() to terminate
# Note it will do 1 more iteration of the loop, inlcuding the sleep
# Note it will do 1 more iteration of the loop, including the sleep
self.running = False
self.logging_signal.set()

Expand Down
53 changes: 44 additions & 9 deletions src/psc_datalogger/gui.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import logging
from typing import Callable, Optional
from typing import Callable, Optional, Type

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIntValidator
from PyQt5.QtWidgets import (
QApplication,
QButtonGroup,
QFileDialog,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
Expand All @@ -18,6 +21,8 @@
QWidget,
)

from psc_datalogger.multimeter import Agilent3458A, Agilent34401A, Multimeter

from . import __version__
from .connection import ConnectionManager
from .statusbar import StatusBar
Expand Down Expand Up @@ -136,6 +141,7 @@ def handle_instrument_changed(self):
i.isChecked(),
i.get_address(),
i.get_temperature_checked(),
i.get_multimeter(),
)
except ValueError:
# Expected when first activating an instrument; the Address field
Expand Down Expand Up @@ -188,7 +194,7 @@ def file_selected(self, new_path: str) -> None:


class AgilentWidgets(QGroupBox):
"""Contains widgets describing a single Agilent 3458A instrument"""
"""Contains widgets to configure a single instrument"""

def __init__(
self, instrument_number: int, checked: bool, instrument_changed: Callable
Expand All @@ -197,7 +203,7 @@ def __init__(
Args:
instrument_number: The number to use to identify this instance
checked: True if the widgets should be enabled by default
instrument_changed: Callback to be called whenever an address changes
instrument_changed: Callback to be called whenever any configuration changes
"""
super().__init__(f"Instrument {instrument_number}")
self.instrument_number = instrument_number
Expand All @@ -222,12 +228,38 @@ def create_widgets(self, instrument_changed: Callable) -> None:
# Don't need to .connect() the second button as changing the first one
# triggers a callback that will check both buttons

layout = QHBoxLayout(self)
layout.addWidget(address_label)
layout.addWidget(self.address_input_box)
layout.addWidget(self.voltage_radiobutton)
layout.addWidget(self.temperature_radiobutton)
self.setLayout(layout)
grid_layout = QGridLayout(self)

first_row_layout = QHBoxLayout()

self.A3458_radiobutton = QRadioButton("3458A")
self.A3458_radiobutton.setChecked(True)
self.A3458_radiobutton.toggled.connect(instrument_changed)
self.A34401_radiobutton = QRadioButton("34401A")

device_groupbox = QButtonGroup(self)
device_groupbox.addButton(self.A3458_radiobutton)
device_groupbox.addButton(self.A34401_radiobutton)

first_row_layout.addWidget(self.A3458_radiobutton)
first_row_layout.addWidget(self.A34401_radiobutton)

grid_layout.addLayout(first_row_layout, 0, 0, Qt.AlignmentFlag.AlignCenter)

second_row_layout = QHBoxLayout()

measurement_type_groupbox = QButtonGroup(self)
measurement_type_groupbox.addButton(self.voltage_radiobutton)
measurement_type_groupbox.addButton(self.temperature_radiobutton)

second_row_layout.addWidget(address_label)
second_row_layout.addWidget(self.address_input_box)
second_row_layout.addWidget(self.voltage_radiobutton)
second_row_layout.addWidget(self.temperature_radiobutton)

grid_layout.addLayout(second_row_layout, 1, 0, Qt.AlignmentFlag.AlignCenter)

self.setLayout(grid_layout)

def get_address(self) -> str:
return self.address_input_box.text()
Expand All @@ -236,3 +268,6 @@ def get_temperature_checked(self) -> bool:
"""Returns True if this instrument is configured to read temperature instead of
voltage"""
return self.temperature_radiobutton.isChecked()

def get_multimeter(self) -> Type[Multimeter]:
return Agilent3458A if self.A3458_radiobutton.isChecked() else Agilent34401A
Loading

0 comments on commit 6cdcaf0

Please sign in to comment.