Using Picotech data loggers with Python

Picotech makes a good range of data loggers for logging pretty much any physical measurements. The commpany publishes the APIs for their devices, which means that they are easy to control from a program written in the Python programming language. This article shows an example temperature logger.

Requirements:

  1. Python 2.5 or greater. Note that programs written for Python 3+ will probably not run on computers that have Python 2.x installed. At the time of writing (April 2009), it is safer to use Python 2.5.
  2. A Picotech TC08 temperature data logger. The author recommends the USB version for ease of connection.

A note on operating systems

Programs written in Python can run on any widely-used operating system. Picotech data loggers are usually used on machines running Microsoft Windows, but Picotech does produce (unsupported) drivers for Linux. Visit the main download page. MacOS is not supported.

A simple Python TC08 module

Save this code in a file, perhaps called usbtc08.py

#!/usr/bin/env python
"""Communication functions for Pico Technology USB TC-08 data logger.

See http://www.picotech.com/document/pdf/usbtc08_en.pdf
"""

import ctypes as ct
import unittest

__author__ = 'Tom Oakley'

# Try loading the dll
try:
    usbtc08 = ct.windll.LoadLibrary('usbtc08')
except WindowsError:
    raise ImportError('usbtc08.dll not found. Check driver is installed.')

# Cold junction is channel 0, plus 8 hot junctions
_CHANNELS_PER_TC08 = 9


class Usbtc08Exception(Exception):
    """Exception generated whilst using USB TC08 unit."""


# Function interface to dll
_usb_tc08_open_unit = usbtc08.usb_tc08_open_unit
_usb_tc08_open_unit.restype = ct.c_short
def usb_tc08_open_unit():
    """Opens unit.

    If you wish to use more than one USB TC-08, call this routine once for
    each unit connected to the PC. The function will return 0 if there are
    no more units found. The driver is thread-safe and will not allow access
    to a single unit from more than one application. If, therefore,
    usb_tc08_open_unit does not find a unit, check that other applications
    are not using the USB TC-08. This includes applications on other user
    accounts on the same computer, where fast user switching is supported.

    Note: The usb_tc08_open_unit function provides a simple way to open
    USB TC-08 units. However, the function call locks up the calling
    thread until the attached USB TC-08 unit has been fully enumerated.
    If a single-threaded application needs to perform concurrent processing,
    such as displaying a progress bar, use usb_tc08_open_unit_async.

    Returns:
    Positive short  - the handle of the unit
    0               - no units were found
    -1              - unit failed to open. Call usb_tc08_get_last_error(0)

    """
    return _usb_tc08_open_unit()


_usb_tc08_close_unit = usbtc08.usb_tc08_close_unit
_usb_tc08_close_unit.restype = ct.c_short
_usb_tc08_close_unit.argtypes = (ct.c_short,)
def usb_tc08_close_unit(handle=1):
    """Closes the TC08 unit.

    Note: If you successfully open any USB TC-08 units, call
    usb_tc08_close_unit for each handle before you exit from your program.
    If you do not, there is a chance that the unit will not reopen until it
    has been disconnected and reconnected.

    Arguments:
    handle - the TC08 unit

    Returns:
    0 - call usb_tc08_get_last_error(0)
    1 - the unit closed successfully

    """
    return _usb_tc08_close_unit(handle)


_usb_tc08_set_channel = usbtc08.usb_tc08_set_channel
_usb_tc08_set_channel.restype = ct.c_short
_usb_tc08_set_channel.argtypes = (ct.c_short, ct.c_short, ct.c_char)
def usb_tc08_set_channel(handle=1, channel=1, tc_type='K'):
    """Enables the given channel.

    Call usb_tc08_open_unit() first.

    Arguments:
    handle  - the USB TC-08 unit.
    channel - which channel to set:  0 - 8 (0 denotes the cold junction.)
    tc_type - type of thermocouple. Set to one of the following characters:
            'B', 'E', 'J', 'K', 'N', 'R', 'S', 'T.' Use a space in quotes
            to disable the channel. Voltage readings can be obtained by
            passing 'X' as the character.

    Returns:
    0 - an error occurred. Call usb_tc08_get_last_error(0)
    1 - channel was successfully set

    """
    return _usb_tc08_set_channel(handle, channel, ct.c_char(tc_type))


_usb_tc08_get_single = usbtc08.usb_tc08_get_single
_usb_tc08_get_single.restype = ct.c_short
_usb_tc08_get_single.argtypes = (ct.c_short,
                        ct.POINTER(ct.c_float),
                        ct.POINTER(ct.c_short),
                        ct.c_short)
def usb_tc08_get_single(handle=1, units=0):
    """Reads a single temperature.

    You must set up the channels before calling this function.
    You must not have put the unit into Streaming mode with usb_tc08_run,
    as this will cause usb_tc08_get_single to fail. The function will convert
    all readings on demand. For more details and an example see the Get
    Single mode section.

    Arguments:
    handle      - specifies the USB TC-08 unit.

    units       - specifies the temperature units for returned data:
                0: USBTC08_UNITS_CENTIGRADE
                1: USBTC08_UNITS_FAHRENHEIT
                2: USBTC08_UNITS_KELVIN
                3: USBTC08_UNITS_RANKINE

    Returns a tuple of:
    ok          0 - an error occurred, use usb_tc08_get_last_error to get the code.
                1 - the function succeeded.
    temp        - pointer to an array of length [9]. There are 9 channels
                on the USB TC-08 (8 + cold junction) and the readings are
                always placed in the array subscript corresponding to the
                channel number. Channels which are not enabled are filled
                with QNaN values.

    Raises:
    Usbtc08Exception if an overflow was reported by the TC08.

    """
    # Set up the buffers to be passed as pointers
    buffer = (ct.c_float * _CHANNELS_PER_TC08)()
    overflow = 0

    # Call the function
    ok = _usb_tc08_get_single(handle, buffer, ct.byref(ct.c_short(overflow)), units)

    if overflow:
        raise Usbtc08Exception('Buffer overflow, code:%d' % overflow)

    # Clean the buffer of QNAN values
    buffer = list(buffer)
    for index, temper in enumerate(buffer):
        if 'QNAN' in str(temper):
            buffer[index] = None

    # Return a tuple as described in this function's docstring
    return ok, tuple(buffer)


_usb_tc08_get_last_error = usbtc08.usb_tc08_get_last_error
_usb_tc08_get_last_error.restype = ct.c_short
_usb_tc08_get_last_error.argtypes = (ct.c_short,)
def usb_tc08_get_last_error(handle=1):
    """Returns the last error on the unit specified by the handle.

    If zero is passed instead of a handle, the function returns the error
    associated with the last call to usb_tc08_open_unit or
    usb_tc08_open_unit_async

    Note: If an invalid handle is passed to a function, the function will fail.
    The error code, however, cannot be associated with a unit so
    usb_tc08_get_last_error will not retain an error code in this instance.
    usb_tc08_get_last_error will also fail if the invalid handle is passed to
    it.

    Returns:
    -1  - invalid handle, otherwise...

    0   USBTC08_ERROR_OK                    No error occurred.
    1   USBTC08_ERROR_OS_NOT_SUPPORTED      The driver supports Windows XP SP2 and Vista.
    2   USBTC08_ERROR_NO_CHANNELS_SET       A call to usb_tc08_set_channel is required.
    3   USBTC08_ERROR_INVALID_PARAMETER     One or more of the function arguments were invalid.
    4   USBTC08_ERROR_VARIANT_NOT_SUPPORTED The hardware version is not supported.
                                            Download the latest driver.
    5   USBTC08_ERROR_INCORRECT_MODE        An incompatible mix of legacy and non-legacy
                                            functions was called (or usb_tc08_get_single
                                            was called while in streaming mode.)
    6   USBTC08_ERROR_ENUMERATION_INCOMPLETE usb_tc08_open_unit_async was called
                                            again while a background enumeration
                                            was already in progress.

    """
    return _usb_tc08_get_last_error(handle)


_usb_tc08_set_mains = usbtc08.usb_tc08_set_mains
_usb_tc08_set_mains.restype = ct.c_short
_usb_tc08_set_mains.argtypes = (ct.c_short, ct.c_short)
def usb_tc08_set_mains(handle=1, sixty_hertz=1):
    """Sets the USB TC-08 to reject either 50 or 60 Hz [noise].

    handle - the USB TC08 unit.
    sixty_hertz - Specifies whether to reject 50 Hz or 60 Hz.
                If set to 1, the unit will reject 60 Hz,
                If set to 0, the unit will reject 50 Hz.

    Returns:
    0 if an error occurred. Use usb_tc08_get_last_error
    1 if mains rejection was set

    """
    return _usb_tc08_set_mains(handle, sixty_hertz)


_usb_tc08_get_minimum_interval_ms = usbtc08.usb_tc08_get_minimum_interval_ms
_usb_tc08_get_minimum_interval_ms.restype = ct.c_long
_usb_tc08_get_minimum_interval_ms.argtypes = (ct.c_short,)
def usb_tc08_get_minimum_interval_ms(handle=1):
    """Returns the minimum sampling interval (or fastest millisecond
    interval) that the unit can achieve in its current configuration.

    The configuration is defined by calling usb_tc08_set_channel().

    Note: The USB TC-08 can sample, from a single channel, at a rate of 10
    samples per second. The absolute minimum sampling interval,
    with all 8 channels and the cold junction enabled, is 900 ms.
    You must set up all the channels that you wish to use before
    calling this routine.

    handle - the USB TC08 unit.

    Returns:
    0 if an error occurred. Use usb_tc08_get_last_error
    minimum sampling interval otherwise

    """
    return _usb_tc08_get_minimum_interval_ms(handle)


class Testusbtc08(unittest.TestCase):
    """Unit tests that require USB TC08 hardware to be plugged in."""

    def setUp(self):
        self.openunits = []

    def tearDown(self):
        for handle in self.openunits:
            usb_tc08_close_unit(handle)

    def testopenunit(self):
        while True:
            handle = usb_tc08_open_unit()

            if handle > 0:
                self.openunits.append(handle)
            else:
                break

        # Assert no errors
        self.assertNotEqual(-1, handle, str(usb_tc08_get_last_error(0)))

        # Assert that at least 1 TC08 was opened
        self.assertTrue(self.openunits, 'No units opened. Check TC08 is plugged in.')

    def testgettemp(self):
        """Tests single temperature retrieval."""
        handle = usb_tc08_open_unit()
        self.assertTrue(handle)
        self.openunits.append(handle)

        # Set channel 1
        channelok = usb_tc08_set_channel(handle)
        self.assertTrue(channelok)

        # Read channel 1
        ok, temps = usb_tc08_get_single(handle)
        self.assertTrue(ok)
        # Assert that the temperature is within expected limits in degrees C
        MIN_TEMP = 17
        MAX_TEMP = 25
        self.assertTrue(MIN_TEMP < temps[1] < MAX_TEMP,
            'Temperature should be %f to %f but is %f' % (MIN_TEMP, MAX_TEMP, temps[1]))

if __name__ == "__main__":
    """Runs the given unit tests.

    Command line examples:
    %python usbtc08.py Testusbtc08   # Runs Testusbtc08 test fixture
    %python usbtc08.py               # Runs all unit tests

    """
    unittest.main()

A simple program to read the USB TC08

Run the following program to use the library above. Perhaps save this code as tc08simpleread.py

#!/usr/bin/env python
"""Print one temperature reading from channel 1 of the first USBTC08 found."""

import usbtc08

def main():
    # Open the first TC08 unit found
    handle = usbtc08.usb_tc08_open_unit()
    print 'Opened unit %d' % handle

    # Set channel 1
    usbtc08.usb_tc08_set_channel(handle, 1)

    # Read all channels
    ok, temps = usbtc08.usb_tc08_get_single(handle)

    # Print results
    for channel, temp in enumerate(temps):
        if temp:
            print 'Unit %d channel %d temp is %s degC' % (handle, channel, temp)

    # Close the unit
    usbtc08.usb_tc08_close_unit(handle)
    print 'Closed unit %d' % handle

if __name__ == "__main__":
    main()

Notes

Please comment on this page:


Message:

Question to reduce spam:
Where did Tom get his engineering degree?

Privacy policy