"""
Hewlett Packard 34401A digital multimeter.
"""
import re
import warnings
from msl.equipment import EquipmentRecord
from msl.equipment.constants import Interface
from .base import equipment
from .dmm import DMM
from .dmm import Settings
from .dmm import Trigger
from ..samples import Samples
# Reduce the number of writes/reads that are needed when calling
# HP34401A.settings(). The order of the queries in the command and in the regex
# must be the same. The multimeter complains if the length of a query is too
# long, so break it up in 2 commands.
_command1 = 'CONFIGURE?;' \
':SENSE:CURRENT:RANGE:AUTO?;' \
':SENSE:VOLTAGE:RANGE:AUTO?;' \
':SENSE:ZERO:AUTO?;' \
':SENSE:CURRENT:NPLC?;' \
':SENSE:VOLTAGE:NPLC?;'
_command1_regex: re.Pattern[str] = re.compile(
r'(?P<FUNCTION>[A-Z]+)\s(?P<RANGE>(\+\d\.\d+E[-+]\d+)),.*;'
r'(?P<DCI_AUTO_RANGE>\d);'
r'(?P<DCV_AUTO_RANGE>\d);'
r'(?P<AUTO_ZERO>\d);'
r'(?P<DCI_NPLC>\+\d\.\d+E[-+]\d+);'
r'(?P<DCV_NPLC>\+\d\.\d+E[-+]\d+)'
)
_command2 = 'SAMPLE:COUNT?;' \
':TRIGGER:SOURCE?;' \
':TRIGGER:COUNT?;' \
':TRIGGER:DELAY?;' \
':TRIGGER:DELAY:AUTO?;'
_command2_regex: re.Pattern[str] = re.compile(
r'(?P<NSAMPLES>\+\d+);'
r'(?P<TRIGGER_SOURCE>[A-Z]+);'
r'(?P<TRIGGER_COUNT>\+\d\.\d+E[-+]\d+);'
r'(?P<TRIGGER_DELAY>\+\d\.\d+E[-+]\d+);'
r'(?P<TRIGGER_AUTO_DELAY>\d)'
)
[docs]
@equipment(manufacturer=r'H.*P.*', model=r'34401A')
class HP34401A(DMM):
def __init__(self, record: EquipmentRecord, **kwargs) -> None:
"""Hewlett Packard 34401A digital multimeter.
Args:
record: The equipment record.
**kwargs: Keyword arguments. Can be specified as attributes
of an XML element in a configuration file (with the tag
of the element equal to the alias of `record`).
"""
super().__init__(record, **kwargs)
self._trigger_cmd: str = 'updated in configure()'
self._zero_once_cmd: str = 'ZERO:AUTO ONCE'
self._prologix: bool = self.record.connection.address.startswith('Prologix')
self._gpib: bool = record.connection.interface == Interface.GPIB
self._rs232: bool = record.connection.interface == Interface.SERIAL
if self._rs232:
self.connection.serial.dtrdsr = True # noqa: connection has serial attribute
self.remote_mode()
[docs]
def abort(self) -> None:
"""Abort a measurement in progress."""
# Only the self._pyvisa case is working reliably with GPIB-USB-HS+ adapter
self.logger.info(f'abort measurement {self.alias!r}')
if self._rs232:
# From the HP 34401A manual "DTR/DSR Handshake Protocol" (Chapter 4, page 152):
# For the <Ctrl-C> character to be recognized reliably by the multimeter
# while it holds DTR FALSE, the controller must first set DSR FALSE.
# PySerial does not allow for the DSR pin to be set, it is a read-only attribute.
self.connection.serial.dtr = True # noqa: connection has serial attribute
self.connection.write(b'\x03') # Ctrl-C ASCII character
elif self._gpib:
self.connection.clear() # noqa: ConnectionGPIB has a clear() method
elif self._prologix:
self.connection.write(b'++clr')
else:
self.raise_exception(f'abort() not handled for {self.alias!r}')
[docs]
def check_errors(self) -> None:
"""Query the error queue.
Raises an exception if there is an error.
"""
message = self.connection.query('SYSTEM:ERROR?')
if not message.startswith('+0,'):
self.raise_exception(message)
[docs]
def disconnect_equipment(self) -> None:
"""Set the digital multimeter to be in LOCAL mode and then close the connection."""
self.local_mode()
super().disconnect_equipment()
[docs]
def fetch(self, initiate: bool = False) -> Samples:
"""Fetch the samples.
Args:
initiate: Whether to call :meth:`.initiate` before fetching the data.
"""
if initiate:
self.initiate()
if self._rs232:
self.logger.info(f'fetch {self.alias!r} | waiting for *OPC? reply...')
assert self.connection.read().startswith('1')
else:
self.logger.info(f'fetch {self.alias!r}')
samples = self.connection.query('FETCH?')
return self._average_and_emit(samples)
[docs]
def local_mode(self) -> None:
"""Set the multimeter to be in LOCAL mode for the RS-232 interface.
All keys on the front panel are fully functional.
"""
self.logger.info(f'set {self.alias!r} to LOCAL mode')
if self._rs232:
self._send_command_with_opc('SYSTEM:LOCAL')
elif self._gpib:
self.connection.local() # noqa: ConnectionGPIB has a local() method
elif self._prologix:
self.connection.write(b'++loc')
else:
self.logger.warning(f'setting {self.alias!r} to LOCAL mode has '
f'not been implemented')
[docs]
def remote_mode(self) -> None:
"""Set the multimeter to be in REMOTE mode for the RS-232 interface.
All keys on the front panel, except the LOCAL key, are disabled.
"""
if not self._rs232:
self.logger.warning(f'setting {self.alias!r} to REMOTE mode is '
f'only valid for the RS-232 interface')
return
self.logger.info(f'set {self.alias!r} to REMOTE mode')
self._send_command_with_opc('SYSTEM:REMOTE')
[docs]
def settings(self) -> Settings:
"""Returns the configuration settings of the digital multimeter."""
match1 = _command1_regex.search(self.connection.query(_command1))
match2 = _command2_regex.search(self.connection.query(_command2))
if not match1 or not match2:
self.raise_exception(f'invalid regex pattern for {self.alias!r}')
function = self.Function(match1['FUNCTION'])
return Settings(
auto_range=match1[f'{function}_AUTO_RANGE'],
auto_zero=match1['AUTO_ZERO'],
function=function,
nplc=match1[f'{function}_NPLC'],
nsamples=match2['NSAMPLES'],
range=match1['RANGE'],
trigger=Trigger(
auto_delay=match2['TRIGGER_AUTO_DELAY'] == '1',
count=match2['TRIGGER_COUNT'],
delay=match2['TRIGGER_DELAY'],
edge=self.Edge.FALLING,
mode=match2['TRIGGER_SOURCE'],
)
)