top of page

De-mystifying hardware interfacing

Have you ever wanted to interface with hardware instruments using Python?


I’ll show you how to make that happen for instruments that come with dynamic link libraries (dlls)!

With this tutorial, you’ll be able to integrate your spectrometer or similar devices into any project, be it for research, industry, or just for fun. We’ll be using the ctypes library to bridge the gap between Python and the spectrometer’s dll, giving you a powerful way to control and interact with your device.

For this tutorial, we will try to interface with a Compact CCD spectrometer from Thorlabs as an example.

Alright, let’s begin!

1. Start by importing ctypes

We will be using the ctypes library to interface with the spectrometer’s DLL.

import ctypes

2. Define data types

Based on the manual and the .h file for the spectrometer, define the necessary data types for the function arguments and return values.

ViStatus = ctypes.c_long
ViRsrc = ctypes.c_char_p
ViBoolean = ctypes.c_int
ViSession = ctypes.c_long
ViPSession = ctypes.POINTER(ctypes.c_long)
ViPUInt32 = ctypes.POINTER(ctypes.c_uint32)
ViUInt32 = ctypes.c_uint32
ViChar = ctypes.c_char
ViReal64 = ctypes.c_double
ViPReal64 = ctypes.POINTER(ctypes.c_double)
ViInt16 = ctypes.c_int16

3. Create a class for your spectrometer and load the DLL

Create a class for your spectrometer that will load the DLL and define the functions we need to interact with the device. Give it a name that makes you smile, like TLCCS!

In the __init__ method of the class, load the dll by using ctypes.CDLL(). ctypes will do its magic and load the DLL for you!

class TLCCS:
    def __init__(self, dll_path:str):
        self._dll = ctypes.CDLL(dll_path)

4. Define function prototypes (it’s like teaching ctypes to speak spectrometer)

For each function in the DLL, specify the argument types and return types. This is important because ctypes doesn’t do any type checking by default. This information can be found in the manual or the .h file. It’s like teaching ctypes to speak the language of your spectrometer!


The description of each function within the dll from the manual

self._dll.tlccs_init.argtypes = [ViRsrc, ViBoolean, ViBoolean, ViPSession]
self._dll.tlccs_init.restype = ViStatus

6. Create methods to talk to your spectrometer

Create methods in the class for each function in the DLL, converting the input arguments and return values to the appropriate ctypes data types.

This will allow you to use Python to communicate with your spectrometer!

def init(self, resource_name:str, id_query:bool, reset_device:bool, instrument_handle:int):
    resource_name_c = ctypes.create_string_buffer(resource_name.encode("utf-8"))
    id_query_c = ViBoolean(id_query)
    reset_device_c = ViBoolean(reset_device)
    self.instrument_handle_c = ViSession(instrument_handle)
    return self._dll.tlccs_init(resource_name_c, id_query_c, reset_device_c, ctypes.byref(self.instrument_handle_c))

7. Let the fun begin!

Now it’s time to play with your spectrometer. Instantiate the class and use the methods to interact with the spectrometer.

# Path to the dll
libname = ".\base\TLCCS_64.dll" 

# Define the instrument ID
# Change this instrument ID to the ID of your device
resource_name = "USB0::0x1313::0x8087::M00234008::0::RAW"

# Create an instance of the class
ccs = TLCCS(libname)

# Initialize the device
result = ccs.init(resource_name, 0, 0, 0)
print(f"Initialize device (0 = correct, rest = error): {result}")

# Set the integration time
result = ccs.set_integration_time(0.1)

# Start the scan
ccs.start_scan()
ccs.get_scan_data()
ccs.get_wavelengths()

# Plot the data
print(ccs.wavelengths)
print(ccs.specdata_c)

# A short pause to make sure the data acquisition is finished
import time
time.sleep(0.01)

# Close the spectrometer connection
ccs.close()

Need help interfacing with your hardware, do reach out to us!


Python ‘wrapper’ for CCS spectrometer:

import ctypes

#: Define the data types based on the spectrometer manual and the .h file
ViStatus = ctypes.c_long
ViRsrc = ctypes.c_char_p
ViBoolean = ctypes.c_int
ViSession = ctypes.c_long
ViPSession = ctypes.POINTER(ctypes.c_long)
ViPUInt32 = ctypes.POINTER(ctypes.c_uint32)
ViUInt32 = ctypes.c_uint32
ViChar = ctypes.c_char
ViReal64 = ctypes.c_double
ViPReal64 = ctypes.POINTER(ctypes.c_double)
ViInt16 = ctypes.c_int16

#: Define the number of pixels for the CSS series spectrometer
CSS_SERIES_NUM_PIXELS = 3648

class TLCCS:
    """
    TLCCS class provides a Python interface to control and interact with the
    Thorlabs CCS spectrometer using the ctypes library.
    """

    def __init__(self, dll_path: str):
        """
        Initialize the TLCCS class.

        :param dll_path: The path to the spectrometer's DLL.
        """
        # Load the DLL into Python
        self._dll = ctypes.CDLL(dll_path)

        # Set the argument types and return types for each function in the DLL
        self._dll.tlccs_init.argtypes = [ViRsrc, ViBoolean, ViBoolean, ViPSession]
        self._dll.tlccs_init.restype = ViStatus
        
        self._dll.tlccs_setIntegrationTime.argtypes = [ViSession, ViReal64]
        self._dll.tlccs_setIntegrationTime.restype = ViStatus

        self._dll.tlccs_startScan.argtypes = [ViSession]
        self._dll.tlccs_startScan.restype = ViStatus

        self._dll.tlccs_getScanData.argtypes = [ViSession, ViReal64]
        self._dll.tlccs_getScanData.restype = ViStatus

        self._dll.tlccs_getWavelengthData.argtypes = [ViSession, ViInt16, ViReal64, ViPReal64, ViPReal64]
        self._dll.tlccs_getWavelengthData.restype = ViStatus

        self._dll.tlccs_close.argtypes = [ViSession]
        self._dll.tlccs_close.restype = ViStatus


    
    def init(self, resource_name: str, id_query: bool, reset_device: bool, instrument_handle: int) -> ViStatus:
        """
        Initialize the instrument driver session.

        :param resource_name: The device's resource name.
        :param id_query: Specifies whether an identification query is performed.
        :param reset_device: Resets the instrument to a known state
        :param instrument_handle: An integer representing the instrument handle.
        :return: The status of the initialization process.
        """
        # Convert the input arguments to ctypes data types
        resource_name_c = ctypes.create_string_buffer(resource_name.encode("utf-8"))
        id_query_c = ViBoolean(id_query)
        reset_device_c = ViBoolean(reset_device)
        self.instrument_handle_c = ViSession(instrument_handle)

        # Call the DLL function and return the status
        return self._dll.tlccs_init(resource_name_c, id_query_c, reset_device_c, ctypes.byref(self.instrument_handle_c))

    def set_integration_time(self, integration_time: float) -> ViStatus:
        """
        This function sets the optical integration time in seconds [s].

        :param integration_time: The integration time in seconds. Valid range is 1E-5 to 6.0E+1.
        :return: The status of the operation.
        """
        integration_time_c = ViReal64(integration_time)
        return self._dll.tlccs_setIntegrationTime(self.instrument_handle_c, integration_time_c)
    
    def start_scan(self) -> ViStatus:
        """
        Start a scan with the spectrometer.

        :return: The status of the operation.
        """
        return self._dll.tlccs_startScan(self.instrument_handle_c)

    def get_scan_data(self) -> ViStatus:
        """
        Get the scan data from the spectrometer.

        :return: The status of the operation.
        """
        self.specdata_c = (ctypes.c_double * CSS_SERIES_NUM_PIXELS)()
        return self._dll.tlccs_getScanData(self.instrument_handle_c, self.specdat_c)

    def get_wavelengths(self, data_set=0, minimum_wavelength=0, maximum_wavelength=0) -> ViStatus:
        """
        This function returns data for the pixel-wavelength correlation.
        The maximum and the minimum wavelength are additionally provided in two separate return values.
        Note:
        If you do not need either of these values you may pass NULL.
        The value returned in Wavelength_Data_Array[0] is the wavelength at pixel 1, this is also the minimum wavelength, the value returned in Wavelength_Data_Array[1] is the wavelength at pixel 2 and so on until Wavelength_Data_Array[CCS_SERIES_NUM_PIXELS-1] which provides the wavelength at pixel CCS_SERIES_NUM_PIXELS (3648). This is the maximum wavelength.

        :param data_set: Calibration data set. 0 = factory calibration, 1 = user calibration.
        :param minimum_wavelength: The minimum wavelength in nm to retrieve.
        :param maximum_wavelength: The maximum wavelength in nm to retrieve.
        :return: The status of the operation.
        """
        self.wavelengths = (ctypes.c_double * 3648)()
        data_set_c = ctypes.c_int16(data_set)
        mininum_wavelength_c = ctypes.byref(ViReal64(minimum_wavelength))
        maximum_wavelength_c = ctypes.byref(ViReal64(maximum_wavelength))

        return self._dll.tlccs_getWavelengthData(self.instrument_handle_c, data_set_c, self.wavelengths, minimum_wavelength_c, maximum_wavelength_c)

    def close(self) -> ViStatus:
        """
        Close the session with the spectrometer.

        :return: The status of the operation.
        """
        return self._dll.tlccs_close(self.instrument_handle_c)

Copyright 2023 Prashanta Kharel

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.

45 views0 comments

Recent Posts

See All

Comments


bottom of page