Source code for msl.equipment.resources.picotech.pt104

"""
Pico Technology PT-104 Platinum Resistance Data Logger.
"""
from __future__ import annotations

from ctypes import POINTER
from ctypes import addressof
from ctypes import byref
from ctypes import c_int16
from ctypes import c_int32
from ctypes import c_int8
from ctypes import c_uint16
from ctypes import c_uint32
from ctypes import string_at
from enum import IntEnum

from msl.loadlib import LoadLibrary

from msl.equipment.connection_sdk import ConnectionSDK
from msl.equipment.constants import IS_WINDOWS
from msl.equipment.exceptions import PicoTechError
from msl.equipment.resources import register
from . import c_enum
from .errors import ERROR_CODES_API
from .errors import PICO_INFO
from .errors import PICO_NOT_FOUND
from .errors import PICO_OK
from .picoscope.enums import PicoScopeInfoApi


[docs] class Pt104DataType(IntEnum): """ The allowed data types for a PT-104 Data Logger. """ OFF = 0 PT100 = 1 PT1000 = 2 RESISTANCE_TO_375R = 3 RESISTANCE_TO_10K = 4 DIFFERENTIAL_TO_115MV = 5 DIFFERENTIAL_TO_2500MV = 6 SINGLE_ENDED_TO_115MV = 7 SINGLE_ENDED_TO_2500MV = 8 MAX_DATA_TYPES = 9
[docs] def enumerate_units(comm_type='all'): """Find PT-104 Platinum Resistance Data Logger's. This routine returns a list of all the attached PT-104 devices of the specified communication type. Note ---- You cannot call this function after you have opened a connection to a Data Logger. Parameters ---------- comm_type : :class:`str`, optional The communication type used by the PT-104. Can be any of the following values: ``'usb'``, ``'ethernet'``, ``'enet'``, ``'all'`` Returns ------- :class:`list` of :class:`str` A list of serial numbers of the PT-104 Data Loggers that were found. """ length = c_uint32(1023) details = (c_int8 * length.value)() t = comm_type.lower() if t == 'usb': t_val = 0x00000001 elif t == 'ethernet' or t == 'enet': t_val = 0x00000002 elif t == 'all': t_val = 0xFFFFFFFF else: raise PicoTechError('Invalid communication type {}'.format(comm_type)) libtype = 'windll' if IS_WINDOWS else 'cdll' sdk = LoadLibrary('usbpt104', libtype) result = sdk.lib.UsbPt104Enumerate(byref(details), byref(length), t_val) if result != PICO_OK: if result == PICO_NOT_FOUND: err_name, err_msg = 'PICO_NOT_FOUND', 'Are you sure that a PT-104 is connected to the computer?' else: err_name, err_msg = ERROR_CODES_API.get(result, ('UnhandledError', 'Error code 0x{:x}'.format(result))) raise PicoTechError('Cannot enumerate units.\n{}: {}'.format(err_name, err_msg)) return string_at(addressof(details)).decode('utf-8').split(',')
[docs] @register(manufacturer=r'Pico\s*Tech', model=r'PT[-]?104') class PT104(ConnectionSDK): MIN_WIRES = 2 MAX_WIRES = 4 DataType = Pt104DataType def __init__(self, record): """Uses the PicoTech SDK to communicate with a PT-104 Platinum Resistance Data Logger. The :attr:`~msl.equipment.record_types.ConnectionRecord.properties` for a PT-104 connection supports the following key-value pairs in the :ref:`connections-database`:: 'ip_address': str, The IP address and port number of the PT-104 (e.g., '192.168.1.201:1234') 'open_via_ip': bool, Whether to connect to the PT-104 by Ethernet. Default is to connect by USB. Do not instantiate this class directly. Use the :meth:`~.EquipmentRecord.connect` method to connect to the equipment. Parameters ---------- record : :class:`~.EquipmentRecord` A record from an :ref:`equipment-database`. """ self._handle = None libtype = 'windll' if IS_WINDOWS else 'cdll' super(PT104, self).__init__(record, libtype) self.set_exception_class(PicoTechError) self.sdk.UsbPt104OpenUnit.argtypes = [POINTER(c_int16), POINTER(c_int8)] self.sdk.UsbPt104OpenUnit.errcheck = self._check self.sdk.UsbPt104OpenUnitViaIp.argtypes = [POINTER(c_int16), POINTER(c_int8), POINTER(c_int8)] self.sdk.UsbPt104OpenUnitViaIp.errcheck = self._check self.sdk.UsbPt104CloseUnit.argtypes = [c_int16] self.sdk.UsbPt104CloseUnit.errcheck = self._check self.sdk.UsbPt104GetUnitInfo.argtypes = [c_int16, POINTER(c_int8), c_int16, POINTER(c_int16), PICO_INFO] self.sdk.UsbPt104GetUnitInfo.errcheck = self._check self.sdk.UsbPt104GetValue.argtypes = [c_int16, c_enum, POINTER(c_int32), c_int16] self.sdk.UsbPt104GetValue.errcheck = self._check self.sdk.UsbPt104IpDetails.argtypes = [c_int16, POINTER(c_int16), POINTER(c_int8), POINTER(c_uint16), POINTER(c_uint16), c_enum] self.sdk.UsbPt104IpDetails.errcheck = self._check self.sdk.UsbPt104SetChannel.argtypes = [c_int16, c_enum, c_enum, c_int16] self.sdk.UsbPt104SetChannel.errcheck = self._check self.sdk.UsbPt104SetMains.argtypes = [c_int16, c_uint16] self.sdk.UsbPt104SetMains.errcheck = self._check self._IP_ADDRESS = record.connection.properties.get('ip_address', None) if self._IP_ADDRESS and record.connection.properties.get('open_via_ip', False): self.open_via_ip(self._IP_ADDRESS) else: self.open() def _check(self, result, func, arguments): self.log_errcheck(result, func, arguments) if result != PICO_OK: conn = self.equipment_record.connection error_name, msg = ERROR_CODES_API.get(result, ('UnhandledError', 'Error code 0x{:x}'.format(result))) error_msg = msg.format(model=conn.model, serial=conn.serial) self.raise_exception('{}: {}'.format(error_name, error_msg))
[docs] def disconnect(self): """Disconnect from the PT-104 Data Logger.""" if self._handle: self.sdk.UsbPt104CloseUnit(self._handle) self.log_debug('Disconnected from %s', self.equipment_record.connection) self._handle = None
[docs] def get_ip_details(self): """Reads the IP details of the PT-104 Data Logger. Returns ------- :class:`dict` The IP details. """ enabled = c_int16() address = c_int8(127) length = c_uint16(address.value) port = c_uint16() self.sdk.UsbPt104IpDetails(self._handle, byref(enabled), byref(address), byref(length), byref(port), 0) return { 'enabled': bool(enabled.value), 'ip_address': string_at(addressof(address)).decode(), 'port': port.value }
[docs] def get_unit_info(self, info=None, include_name=True): """Retrieves information about the PT-104 Data Logger. If the device fails to open, or no device is opened only the driver version is available. Parameters ---------- info : :class:`~.enums.PicoScopeInfoApi`, optional An enum value or member name. If :data:`None` then request all information from the PT-104. include_name : :class:`bool`, optional If :data:`True` then includes the enum member name as a prefix. For example, returns ``'CAL_DATE: 09Aug16'`` if `include_name` is :data:`True` else ``'09Aug16'``. Returns ------- :class:`str` The requested information from the PT-104 Data Logger. """ if info is None: values = [PicoScopeInfoApi(i) for i in range(7)] # only the first 7 items are supported by the SDK else: values = [self.convert_to_enum(info, PicoScopeInfoApi, to_upper=True)] string = c_int8(127) required_size = c_int16() msg = '' for value in values: name = '{}: '.format(value.name) if include_name else '' self.sdk.UsbPt104GetUnitInfo(self._handle, byref(string), string.value, byref(required_size), value) msg += '{}{}\n'.format(name, string_at(addressof(string)).decode()) return msg[:-1]
[docs] def get_value(self, channel, filtered=False): """Get the most recent reading for the specified channel. Once you open the driver and define some channels, the driver begins to take continuous readings from the PT-104 Data Logger. The scaling of measurements is as follows: +-----------------------+----------------------+ | Range | Scaling | +=======================+======================+ | Temperature | value * 1/1000 deg C | +-----------------------+----------------------+ | Voltage (0 to 2.5 V) | value * 10 nV | +-----------------------+----------------------+ | Voltage (0 to 115 mV) | value * 1 nV | +-----------------------+----------------------+ | Resistance | value * 1 mOhm | +-----------------------+----------------------+ Parameters ---------- channel : :class:`int` The number of the channel to read, from 1 to 4 in differential mode or 1 to 8 in single-ended mode. filtered : :class:`bool`, optional If set to :data:`True`, the driver returns a low-pass filtered value of the temperature. The time constant of the filter depends on the channel parameters as set by :meth:`.set_channel`, and on how many channels are active. Returns ------- :class:`float` The latest reading for the specified channel. """ value = c_int32() self.sdk.UsbPt104GetValue(self._handle, channel, byref(value), int(filtered)) return value.value
[docs] def open(self): """Open the connection to the PT-104 via USB.""" if self._handle: self.disconnect() handle = c_int16() s = self.equipment_record.serial serial = (c_int8 * len(s)).from_buffer_copy(s.encode()) self.sdk.UsbPt104OpenUnit(byref(handle), serial) self._handle = handle.value
[docs] def open_via_ip(self, address=None): """Open the connection to the PT-104 via ETHERNET. Parameters ---------- address : :class:`str`, optional The IP address and port number to use to connect to the PT-104. For example, ``'192.168.1.201:1234'``. If :data:`None` then uses the ``'ip_address'`` value of the :attr:`~msl.equipment.record_types.ConnectionRecord.properties` """ if self._handle: self.disconnect() handle = c_int16() ip = self._IP_ADDRESS if address is None else address if ip is None: self.raise_exception('You must either specify the IP address in the ' 'Connection Database or when calling this function') address = (c_int8 * len(ip)).from_buffer_copy(ip.encode()) self.sdk.UsbPt104OpenUnitViaIp(byref(handle), None, address) self._handle = handle.value
[docs] def set_channel(self, channel, data_type, num_wires): """Configure a single channel of the PT-104 Data Logger. The fewer channels selected, the more frequently they will be updated. Measurement takes about 1 second per active channel. If a call to the :meth:`.set_channel` method has a data type of single-ended, then the specified channel's 'sister' channel is also enabled. For example, enabling 3 also enables 7. Parameters ---------- channel : :class:`int` The channel you want to set the details for. It should be between 1 and 4 if using single-ended inputs in voltage mode. data_type : :attr:`.DataType` The type of reading you require. Can be an enum value or member name. num_wires : :class:`int` The number of wires the PT100 or PT1000 sensor has (2, 3 or 4) """ typ = self.convert_to_enum(data_type, Pt104DataType, to_upper=True) if num_wires < self.MIN_WIRES or num_wires > self.MAX_WIRES: self.raise_exception('The num_wires value is {}. It must be 2, 3 or 4.'.format(num_wires)) self.sdk.UsbPt104SetChannel(self._handle, channel, typ, num_wires)
[docs] def set_ip_details(self, enabled, ip_address=None, port=None): """Writes the IP details to the device. Parameters ---------- enabled : :class:`bool` Whether to enable or disable Ethernet communication for this device. ip_address : :class:`str`, optional The new IP address. If :data:`None` then do not change the IP address. port : :class:`int`, optional The new port number. If :data:`None` then do not change the port number. """ if ip_address is None or port is None: details = self.get_ip_details() if ip_address is None: ip_address = details['ip_address'] if port is None: port = details['port'] enabled = c_int16(bool(enabled)) address = (c_int8 * len(ip_address)).from_buffer_copy(ip_address.encode()) length = c_uint16(len(ip_address)) port = c_uint16(port) self.sdk.UsbPt104IpDetails(self._handle, enabled, address, length, port, 1)
[docs] def set_mains(self, hertz): """Inform the driver of the local mains (line) frequency. This helps the driver to filter out electrical noise. Parameters ---------- hertz : :class:`int` Either 50 or 60. """ if hertz not in {50, 60}: self.raise_exception('The mains frequency must be 50 or 60. Got {}'.format(hertz)) self.sdk.UsbPt104SetMains(self._handle, 0 if hertz == 50 else 1)