Giter VIP home page Giter VIP logo

qmi's People

Contributors

heevasti avatar qfer avatar rbudhrani avatar snizzleorg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

qmi's Issues

Adding QMI Drivers for Tenma power supplies

Description

Add QMI drivers for the Tenma benchtop power supply models 72-13360 & 72-2550 in QMI. QMI instrument drivers, already working but rough versions can be found here 13360 and 2550. These are expecting for serial transport strings, but should be edited such that also other transport strings, especially (TBD in here) UDP transport can also be used.

There exists already a nice package for controlling Tenma power supplies via serial:
https://github.com/kxtells/tenma-serial/blob/master/tenma/tenmaDcLib.py#L798
From this we could adopt the idea of creating a base class Tenma_72Base for our two drivers. In the base class would be defined some class attributes like the max Volt and Amp. We can probably ignore the NCONFS as at the moment there is no requirement to save/load configurations. We can leave the default baudrate attribute for serial implementations. Probably (but not checked) some rpc_methods could be implemented already in the base class, like open(), close() and get_idn(). For the rest check per method if the implementation can be in the base class. If not, line out the method with inputs, outputs and a docstring, but simply return NotImplementedError. The implementations will then be in the "concrete" classes.

Go through the example drivers' code and the pypi code and implement the necessary functions for classes Tenma_722550 and Tenma_7213360 in qmi.instruments.tenma.psu_72 module. Also remember to include description and imports in the __init__.py so that the QMI driver show nice description in the Sphinx-generated documentation as well (see other QMI drivers for examples).

You can also add nice CLI tools for testing.

Affected components

QMI

Modules to be created

qmi/instruments/tenma/psu_72.py

Modules to be modified

N/A

Tests to be created/updated

tests/instruments/tenma/test_psu_72.py

Documentation to be updated

CHANGELOG.md

Hardware

Tenma 72-2550 & 72-13360 can be used for testing and are both available at Hansonlab

Create UDP transport and class

Description

There is a new request to have UDP transport class for Tenma power supply instruments. As UDP is quite similar to TCP, basically TCP with less checks and stability, we should be able to quickly create one based on the TCP class with stripping some details. Then, we should make a base class that works for both the UDP and TCP transport classes, so to save some double code.

First thing in qmi.core.transport module is the transport descriptor parses definition. This probably can be exactly the same as for TCP, with changing the the names only:

UdpTransportDescriptorParser = TransportDescriptorParser(
    "udp",
    [("host", (str, True)),
     ('port', (int, True))],
    {'connect_timeout': (float, False)}
)

Cees Wolfs has already tested the UDP communication with the following transport class:
class QMI_UdpTransport(QMI_Transport):
    """Bidirectional byte stream via UDP network connection.

    An instance of QMI_UdpTransport represents a client-side UDP connection
    to an instrument. Server-side UDP connections are not supported.
    """

    DEFAULT_CONNECT_TIMEOUT = 10

    def __init__(self, host: str, port: int, connect_timeout: Optional[float] = DEFAULT_CONNECT_TIMEOUT) -> None:
        """Initialize the UDP transport by setting up the connection details.

        Parameters:
            connect_timeout: Maximum time to connect in seconds.

        Raises:
            ~qmi.core.exceptions.QMI_TimeoutException: If connecting takes longer than the specified connection
            timeout.
        """
        super().__init__()

        self._validate_host(host)
        self._validate_udp_port(port)

        self._address = (host, port)
        self._connect_timeout = connect_timeout
        self._socket: Optional[socket.socket] = None
        self._read_buffer = bytearray()

    @staticmethod
    def _validate_udp_port(port):
        if port < 1 or port > 65535:
            raise QMI_TransportDescriptorException("Invalid UDP port number {}".format(port))

    @staticmethod
    def _validate_host(host):
        if (not _is_valid_hostname(host)) and (not _is_valid_ipaddress(host)):
            raise QMI_TransportDescriptorException("Invalid host name {}".format(host))

    def __str__(self) -> str:
        remote_addr = format_address_and_port(self._address)
        return "QMI_UdpTransport(remote={})".format(remote_addr)

    @property
    def _safe_socket(self) -> socket.socket:
        """ The _safe_socket property should be used inside the QMI_Transport code if-and-only-if we are 100% sure that
        the _socket attribute is not None.

        This aids in static typechecking, since whereas the type of _socket is Optional[T], the result of this method
        is guaranteed to be of type T. It is a QMI-internal bug if this property is used in case _socket is None. In
        that case, we raise an AssertionError, and we hope the users will complain to us so we can fix the bug in the
        library.

        Raises: AssertionError: in case the property is used when the underlying value of _socket is None.

        Returns: The value of _socket, if it is not None. """
        assert self._socket is not None
        return self._socket

    def _open_transport(self) -> None:
        _logger.debug("Opening %s", self)
        _logger.debug("Connecting UDP transport to %s", self._address)

        # Create socket.
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        # Bind to be able to listen back
        self._socket.bind(("", self._address[1]))

    def close(self) -> None:
        _logger.debug("Closing UDP transport %s", self)
        super().close()
        self._safe_socket.close()

    def write(self, data: bytes) -> None:
        self._check_is_open()
        # NOTE: We explicitly adjust the socket timeout before each send/recv call.
        self._safe_socket.settimeout(None)
        self._safe_socket.sendto(data, self._address)

    def read(self, nbytes: int, timeout: Optional[float]) -> bytes:
        self._check_is_open()
        self._safe_socket.settimeout(timeout)
        data, _ = self._safe_socket.recvfrom(nbytes)
        return data

    def read_until(self, message_terminator: bytes, timeout: Optional[float]) -> bytes:
        raise NotImplementedError("read_until is not supported for UDP")

    def read_until_timeout(self, nbytes: int, timeout: float) -> bytes:
        self._check_is_open()
        self._safe_socket.settimeout(timeout)
        data, _ = self._safe_socket.recvfrom(nbytes)
        return data

    def discard_read(self) -> None:
        self._check_is_open()
        self._safe_socket.settimeout(0)
        while True:
            try:
                data, _ = self._safe_socket.recvfrom(4096)
            except socket.timeout:
                # no more bytes available
                break
            except BlockingIOError:
                # no more bytes available
                break
            if not data:
                # end of stream
                break

Here the __init__() methods together with the validator methods are exactly the same (excluding "tcp" -> "udp" name changes). Because of the way UDP socket is set-up, though, now the connect_timeout option is not needed and can be removed. There is one other notion on the UDP port number: the QMI_Context uses as default UDP port 35999 as its responder port. It might not be wise to allow also an instrument to communicate (especially large data transfers or high-frequency calls) in this port as it could cause slow-down for communication between QMI contexts. This should be accounted for.

The string definition __str__() needs different versions to give correct description back.

The _safe_socket property is simpler for UDP than for the TCP as no delays are defined and no verification of a connection to the given (remote) address needs to be done. It just associates the local address to the socket e basta. Then the address is ready for listening and to accepting connections. As the socket streaming type is different between UDP and TCP, it is not possible to make a common base for this method.

close() call works exactly the same for both transports, only that the message in the debugger explicitly says which type of protocol is in question. If changing this to grab the parent class name, it could be fully generalized for both.

There are some minor, but important differences in the communication with writing and reading data between the two protocols.

  • For UDP we need to use ...sendto(data, self._address) instead of ...sendall(data) as the binding of the socket does not define target (client).
  • UDP could receive data from any client. When reading with recvfrom, we can use the second return value address (of format tuple("ip.add.res.s", port)) to check against self._address to make sure we received data from expected client. In the rough implementation all data buffering into self._read_buffer and timeout and exception handling has been stripped, but for more robust working these should be in place. I think it should be possible to make the two read methods work the same otherwise, except for an extra check for the client address in the UDP case. If also TCP would be changed to use recvfrom instead of recv, we could implement this in a base class with addition of and address check if b returned fine:
    def read(...):
      ...
          b, addr = self._safe_socket.recvfrom(nbytes-nbuf)
      ...
      if not b:
          ...
      if _address_check and addr != self._address:
          del(b)  # Discard this data as it came from wrong client.
          continue  # Keep trying.

and in implementations per protocol this could be used:

    def read(...):
        _address_check = False  # True for UDP
        super().read(...)

Pretty much the same could be done for the read_until function. The read_until_timeout and discard_read would be the same for both protocols, so they could be in the base class.

The last part to edit is the create_transport function at the end of transport.py, where the new transport description and matching needs to be added.

Affected components

QMI

Modules to be created

N/A

Modules to be modified

qmi.core.transport

Tests to be created/updated

Add tests into tests/core/test_transport.py

Documentation to be updated

  • CHANGELOG.md
  • documentation\sphinx\source\welcome.rst NEW: Add in the "nice features" a description which transport protocols QMI can handle.

Hardware

To be tested with Tenma power supplies in ticket https://github.com/orgs/QuTech-Delft/projects/2/views/1?pane=issue&itemId=47295046

Add TeraXion TFN driver

Description

The TeraXion TFN is a narrowband tunable optical filter. It communicates using the I2C protocol. It is delivered with the TFN power unit, which also acts as a converter from ASCII to the I2C signals. Therefore this driver can be written by as a device that accepts ASCII commands. It communicates over serial using the RS-232 standard and has a baudrate of 57600.
The manuals are password protected behind this cloudshare.
A CLI tool for the driver must be created as well.

The following code is an example of how to write the driver.

"""
Instrument driver for TeraXion TFN.
"""
import binascii
from dataclasses import dataclass
import logging
import struct
from typing import Optional
from qmi.core.context import QMI_Context
from qmi.core.instrument import QMI_Instrument
from qmi.core.rpc import rpc_method
from qmi.core.scpi_protocol import ScpiProtocol
from qmi.core.transport import create_transport

# Global variable holding the logger for this module.
_logger = logging.getLogger(__name__)


@dataclass
class Teraxion_TFNStatus:
    """
    Status of TFN.
    """
    busy_error: bool
    overrun_error: bool
    command_error: bool
    tfn_active: bool
    tfn_ready: bool
    invalid_eeprom_error: bool
    tec_4_temp_limit: bool
    tec_3_temp_limit: bool
    tec_2_temp_limit: bool
    tec_1_temp_limit: bool
    tec_4_in_range: bool
    tec_3_in_range: bool
    tec_2_in_range: bool
    tec_1_in_range: bool


@dataclass
class Teraxion_TFNCommand:
    """
    Base class for Teraxion TFN commands.
    """
    command_id: int
    num_received_bytes: int
    num_sent_bytes: Optional[int]
    module_address: int = 0x30


class Teraxion_TFNCommand_GetStatus(Teraxion_TFNCommand):
    """
    Command to get the status of the TFN.
    """
    command_id = 0x00
    num_received_bytes = 4


class Teraxion_TFNCommand_GetFrequency(Teraxion_TFNCommand):
    """
    Command to get the frequency.
    """
    command_id = 0x2F
    num_received_bytes = 8


class Teraxion_TFNCommand_SetFrequency(Teraxion_TFNCommand):
    """
    Command to set the frequency.
    """
    command_id = 0x2E
    num_received_bytes = 8


class Teraxion_TFNCommand_GetManufacturerName(Teraxion_TFNCommand):
    """
    Command to get the manufacturer name.
    """
    command_id = 0x0E
    num_received_bytes = 13


class Teraxion_TFN(QMI_Instrument):
    """
    Instrument driver for TeraXion TFN. It uses serial communication.
    """
    DEFAULT_READ_TIMEOUT = 10  # default read timeout in seconds

    # the start and stop conditions for the ascii commands.
    CMD_START_CONDITION = "S"
    CMD_STOP_CONDTION = "P"

    LEN_STATUS_BYTES = 4  # len of the status bytes

    READ_WRITE_DELAY = 0x000A  # delay value between a write and read command in hex

    def __init__(self,
                 context: QMI_Context,
                 name: str,
                 transport: str) -> None:
        super().__init__(context, name)
        self._transport = create_transport(
            transport, default_attributes={"baudrate": 57600})
        self._scpi_protocol = ScpiProtocol(
            self._transport, command_terminator="")

    def _hex_to_str(self, hex_val: int) -> str:
        """
        Helper method to convert a hex value into string. This is a not a conversion from hex to int to string,
        rather a conversion from hex to string. For example 0x60 is converter to '60' and not '96' which is the
        integer representatin of 0x60.

        Parameters:
            hex_val:    The hexadecimal value to convert to a string.

        Returns:
            The string representation of the hex value.
        """
        return f"{hex_val:02x}"

    def _read(self, cmd: Teraxion_TFNCommand, timeout: float = DEFAULT_READ_TIMEOUT) -> bytes:
        """
        Helper method to create a read command and send it and then retrieve the result.

        Parameters:
            cmd: A Teraxion_TFNCommand.
        """
        # shift module address by one and set the write bit
        module_write_mode = int(cmd.module_address) << 1
        # make write command
        write_command = f"{self.CMD_START_CONDITION}{self._hex_to_str(module_write_mode)}{self._hex_to_str(cmd.command_id)}{self.CMD_STOP_CONDTION}"

        # shift module address by one and set the read bit
        module_read_mode = cmd.module_address << 1 ^ 1
        # make read command
        read_command = f"{self.CMD_START_CONDITION}{self._hex_to_str(module_read_mode)}{cmd.num_received_bytes:02d}{self.CMD_STOP_CONDTION}"

        # send the 2 commands and return the response
        resp = self._scpi_protocol.ask(
            f"{write_command} {read_command}", timeout=timeout)

        # convert to hex reprensation
        return bytes.fromhex(resp)

    def _write(self, cmd: Teraxion_TFNCommand, value: bytes) -> None:
        """
        Helper method to create a write command and send it.

        Parameters:
            cmd:    A Teraxion_TFNCommand.
            value:  The value to write in bytes.
        """
        # convert the value to its hex representation
        hex_val = binascii.hexlify(value).decode()
        # shift module address by one and set the write bit
        module_write_mode = int(cmd.module_address) << 1
        # make write command
        write_command = f"{self.CMD_START_CONDITION}{self._hex_to_str(module_write_mode)}{self._hex_to_str(cmd.command_id)}{hex_val}{self.CMD_STOP_CONDTION}"

        # shift module address by one and set the read bit
        module_read_mode = cmd.module_address << 1 ^ 1
        # make read command
        read_command = f"{self.CMD_START_CONDITION}{self._hex_to_str(module_read_mode)}{cmd.num_received_bytes:02d}{self.CMD_STOP_CONDTION}"

        self._scpi_protocol.write(
            f"{write_command} L{self._hex_to_str(self.READ_WRITE_DELAY)} {read_command}")

    @rpc_method
    def open(self) -> None:
        _logger.info("[%s] Opening connection instrument", self._name)
        self._check_is_closed()
        self._transport.open()
        super().open()

    @rpc_method
    def close(self) -> None:
        _logger.info("[%s] Closing connection to instrument", self._name)
        self._check_is_open()
        self._transport.close()
        super().close()

    @rpc_method
    def get_status(self) -> str:
        """
        Get status of the TFN.

        Returns:
            an instance of Teraxion_TFNStatus.
        """
        _logger.info("[%s] Getting status of instrument", self._name)
        self._check_is_open()
        # get response
        resp = self._read(Teraxion_TFNCommand_GetStatus)
        # TODO: convert to Teraxion_TFNStatus
        return resp

    @rpc_method
    def set_frequency(self, frequency: float) -> None:
        """
        Set frequency setpoint of the TFN.

        Parameters:
            frequency:  The frequency setpoint in GHz.
        """
        _logger.info(
            "[%s] Setting frequency of instrument to [%f]", self._name, frequency)
        self._check_is_open()
        # pack the frequency to a byte array
        freq = struct.pack('>f', frequency)
        # send command
        self._write(Teraxion_TFNCommand_SetFrequency, freq)

    @rpc_method
    def get_frequency(self) -> float:
        """
        Get frequency setpoint of the TFN.

        Returns:
            the frequency setpoint in GHz.
        """
        _logger.info("Getting frequency of instrument [%s]", self._name)
        self._check_is_open()
        # get response
        resp = self._read(Teraxion_TFNCommand_GetFrequency)
        # unpack the frequency and return
        freq = struct.unpack('>f', resp[self.LEN_STATUS_BYTES:])[0]
        return freq

    @rpc_method
    def get_manufacturer_name(self) -> str:
        """
        Get manufacturer name of the TFN.

        Returns:
            the name of the manufacturer.
        """
        _logger.info(
            "[%s] Getting manufacturer name of instrument", self._name)
        self._check_is_open()
        # get response
        resp = self._read(Teraxion_TFNCommand_GetManufacturerName)
        # get the data after the status bytes and ignore the null bytes
        manufacturer_name = resp[self.LEN_STATUS_BYTES:]
        return manufacturer_name.decode('ascii')[:manufacturer_name.find(b'\x00')]

Using this the other calls and other improvements can be made.

Modules to be created

  • qmi.instruments.teraxion.tfn

Modules to be modified

  • n/a

Tests to be created/updated

  • unittest

Documentation to be updated

  • CHANGELOG

Hardware

  • TeraXion TFN

Newport single axis motion controller relative move with negative values

Describe the bug
The Newport single axis motion controller has a bug in its relative move command. it does not allow for negative displacement. It checks if the given displacement is less than the allowed minimum incremental motion of the actuator and fails because a negative displacement will always be lower than that value. Instead it must check against the absolute value of the provided displacement.

if displacement < self._actuators[self.controller_address].MIN_INCREMENTAL_MOTION:

To Reproduce

  • Make/Get an instance of a Newport Conex CC controller.
  • Run move_relative(-0.01)
  • Instrument will raise an exception that -0.01 is less than the minimum incremental motion

Expected behavior

  • The instrument should not raise an exception and should move the actuator by a negative value.

Screenshots

  • to be added

Operating System (please complete the following information):

  • OS: Linux
  • Version: Bookworm
  • Distribution: Debian

Additional context

  • n/a

Change implementation for `read` and `read_until[_timeout]` methods for `QMI_Vxi11Transport` class

Description

The implementation of the Vxi11 protocol in the QMI_Vxi11Transport class has a couple of shortcomings. The read method tries to read nbytes data, but if the protocol does not return enough data, we get Vxi11Exception. The read data gets in that case discarded. For read_until method, the situation is quite the same, with the addition that by successful read, but the last character being some other than the expected message_terminator, exception is raised and data discarded. There is no implementation of the read_until_timeout.

Meanwhile in discard_read method data is read one byte at a time from the device, and then eventually any data read is discarded.

We could rather make use of the similar scheme as for other "Transports", where data is read into self._read_buffer. We can do this with the approach of discard_read but now adding each read character into the self._read_buffer. For read case until nbytes bytes are in the buffer, and for read_until until message_terminator is reached:

    def read(...):
        nbuf = len(self._read_buffer)
        if nbuf >= nbytes:
            # The requested number of bytes are already in the buffer.
            # Return them immediately.
            ret = bytes(self._read_buffer[:nbytes])
            self._read_buffer = self._read_buffer[nbytes:]
            return ret

        # timeout handling here, and then
        while len(self._read_buffer) < nbytes:
            try:
                self._read_buffer.extend(self._safe_serial.read_raw(1))
            ...

        return self._read_buffer
    def read_until(...):
        nbuf = len(self._read_buffer)
        if nbuf > 0 and message_terminator in self._read_buffer:
            # The requested response is already in the buffer.
            # Return it immediately.
            terminator_index = self._read_buffer.index(message_terminator)
            ret = bytes(self._read_buffer[:terminator_index])
            self._read_buffer = self._read_buffer[terminator_index:]
            return ret

        try:
            while True:
                self._read_buffer.extend(self._safe_serial.read_raw())
                # Validate terminator.
                data_term_char = self._read_buffer[-1:]  # use slice rather than index to get back bytes()
                if data_term_char == message_terminator:
                    break

        # terminator and timeout handling here as before, and then

        ret = self._read_buffer
        self._read_buffer = bytearray()
        return ret

and finally we could add implementation

    def read_until_timeout(...):
        return read(nbytes, timeout)
  • So far only Siglent SSA3000x and Tektronix AWG5014 use VXI11 protocol and both refer to ScpiProtocol. The proposed changes do not change the interfaces (only read and read_until are in ScpiProtocol), but perhaps something will function differently.

Affected components

QMI

Modules to be created

N/A

Modules to be modified

qmi.core.transport

Tests to be created/updated

  • unittests
  • test on hardware devices that use it

Documentation to be updated

CHANGELOG.md

Fix 'regexp' strings in TLB-670x driver to avoid future errors

Description

The qmi driver of the TLB-760x has some old regexp string formatting: The \d will be considered as an escape character in the future and not as a regular expression key. The fix will be either using double backslashes to "escape" the escape character (\d) or set the search string as raw (r"\d...").

Image

Modules to be changed

qmi.instruments.newport.tlb670x

Make PicoScope3403 also to accept sample times of 1 and 2 ns, and correct docstrings.

Description

The PicoScope3403 driver does not currently accept time-base values 0 and 1, respectively representing sampling intervals of 1ns and 2ns. The minimum accepted currently is time-base 2 with sampling interval of 4ns. Also, the number of samples is calculated as number_samples = int(time_span / time_base) which makes it impossible for time-base 0. The scientists would find it more intuitive to be able to give the sampling interval as an input, instead of time-base, anyhow. So, we should change the method definition for acquire_by_trigger_and_get_data to use sampling interval instead of time-base as input.

Further remark is that the method descriptions for time_base in the other methods show transformation of "(time_base+1) * 12.5 ns", which is not correct for this model (but for model 4824 instead). These docstring errors need to be fixed as well.

Changes needed:

  • The time_base docstrings should read:
    "The effective time base is (timebase - 2) * 8 ns for timebase > 2 else 2^timebase.
    Depending on the enabled channels, a minimum value of up to 2 may be required
    (minimum time base 4 ns)."

  • Introduce a new method get_time_base that calculates the time-base from sampling interval:

    def get_time_base(self, sampling_interval: int) -> int:
        """Returns the time base selector for a given sampling interval. For invalid sampling intervals, an exception is raised.
        
        Parameters:
            sampling_interval:    Sampling interval in nanoseconds. Must be 1, 2, 4 or a multiple of 8 ns.

        Returns:
            time_base: The respective time-base value for the sampling interval.
        """
        if sampling_interval < 1 or sampling_interval % 8 != 0:
            raise QMI_UsageException("Sampling interval must be 1, 2, 4 or a multiple of 8 ns")
        
        if sampling_interval > 4:
            return int(round((sampling_interval / 8) + 2, 0))

        return int(2**sampling_interval)

In acquire_by_trigger_and_get_data:

  • change input time_base to sampling_interval (both int) and time_span to allow a float instead of int.
  • The parameter description for sampling_interval: Sampling interval in Nanoseconds. Must be 1, 2, 4 or a multiple of 8 ns.
  • Also the time_span needs correction in the docstring, and the times return value would be returned also in seconds instead of "us".
  • calculate the time-base from sampling_interval and raise exception if invalid value w.r.t. channels enabled is used.
        time_base = self.get_time_base(sampling_interval)

        if time_base == 0 and len(channels) > 1:
            raise QMI_UsageException("Sampling interval of 1 ns is not supported for multiple channels")

        if time_base == 1 and len(channels) > 2:
            raise QMI_UsageException("Sampling interval of 2 ns is not supported for more than two channels")
  • calculate the number of samples based on desired time span and sampling interval.
        number_samples = int(time_span / (sampling_interval))
        _logger.info(f"Timespan = %i Num samples = %i", time_span, number_samples)
  • Add disabling of unused channels after channel voltage range check:
        # Disable all channels that are not used
        for chan in range(self.NUM_CHANNELS):
            if chan not in channels:
                self.set_channel(chan, False, ChannelCoupling.DC, 0, 0)
  • The input parameters for self.run_block here should now use number_samples // 2 for the number of samples before and after trigger, or number_samples when all samples are taken before or after the trigger, and simply time_base for time-base, not with + 2.

  • The return value for timetrace array should get a multiplier of * sampling_interval to make it in nanoseconds.

  • For get_time_resolution extend the time resolution solving also for time-base 0 & 1 (sampling_interval = 2**time_base).

Modules to be created

N/A

Modules to be modified

qmi.instruments.picoscope.picoscope3403

Tests to be created/updated

tests.instruments.picoscope.test_picoscope3403

Documentation to be updated

  • CHANGELOG.md

Hardware

PicoScope3403 model. Preferably with 4 channels.

Add Santec TSL-570 laser driver

Description

A new driver needs to be created for Santec tunable laser model TSL-570. The controller can be connected through GPIB, USB (on Windows with a USB-to-serial driver installation) and LAN interfaces (TCP-IP). The command protocol is SCPI with carriage return "CR" as delimiter. With QMI and Linux, all connection options should in principle be viable through TCP and USBTMC transport interfaces. With Windows, GPIB is not (yet) developed, but with USB-to-serial driver installation serial transport works, and TCP of course as well.

As the hardware is using SCPI protocol, there is no need to refer to any manufacturer-provided software or libraries, thus there will be no license issues. The driver can be implemented also in the open-source QMI in Github.

Functionality / commands to be implemented:

  • basic functions:
    • *IDN
    • *RST
    • *OPC?
    • *CLS
  • control functions (get AND set where possible):
    • :WAVelength
    • :WAVelength:UNIT
    • :WAVelength:FINe
    • :WAVelength:FINetuning:DISable
    • :COHCtrl
    • :POWer:STATe
    • :POWer
    • :POWer:ACTual?
  • From the sweep functions we will need the following:
    • :WAVelength:SWEep:STARt
    • :WAVelength:SWEep:STOP
    • :WAVelength:SWEep:MODe
    • :WAVelength:SWEep:SPEed
    • :WAVelength:SWEep:CYCLes
    • :WAVelength:SWEep:STATe
    • :READout:DATa?
  • system functions:
    • :SYSTem:ERRor?
    • :SYSTem:ALERt?
    • :SPECial:SHUTdown
    • :SPECial:REBoot

These commands should allow the user to create sweeps in a script with some for loops. Optionally, if the sweeps do not seem to be satisfactorily implemented through a script, the controller has several SCPI commands for wavelength and frequency-based sweeps.

Other possibly interesting commands might be setting an offset to the wavelength and trigger commands. The modulation enable/disable and modulation source might also be of interest.

Affected components

qmi / QMI

Modules to be created

  • qmi/instruments/santec/__init__.py
  • qmi/instruments/santec/tsl_570.py

Tests to be created/updated

  • tests/instruments/santec/test_tsl_570.py
  • hardware test for frequency sweep, power sweep?

Hardware

Santec laser controller.

Documentation to be updated

  • CHANGELOG.md.
  • in-code documentation and comments

Modify workflows to do only python-latest on push branch or pull request, and full CI when doing a merge

Description

The workflows do not yet seem to push the badges correctly to the repo. A further issue is that doing the full, PYthon 3.8, 3.9, 3.10 and 3.11 workflows at each push to a pull request takes a lot of time. Up to 40 minutes / push. This is too much, and it would be better if this could be done only when the pull request is ready to be merged.

Actions

As part of this ticket try to figure out how to get the badges pushed into the repo (now, plan is to push them in .gitlab/badges). Figure out how this would be done only during when the actual pull request is to be merged. Probably, the currently trialled doing commits in steps / check, and then one push at the end, doesn't work. Most likely each step needs its own push, which creates the requirement for the next step to "pull" to be on the correct HEAD. Try to get it working.

qmi_proc should close services in reverse order to starting them

Description

One small request about the new (and super useful!) qmi_proc restart --locals: It would be good if it stopped the processes in reverse order before starting them again. Right now, it does it in the same order and hence services that depend on others, always crash while closing down. For now it's not a big deal, but there might be cases when hosted instruments don't like this.

The processes are stopped with proc_stop function. If the parameter local is True in the call, the context names to be stopped will be obtained with select_local_contexts(cfg) call, with current CfgQmi configuration as input. This returns a list of context name string which is compiled in the order they appear in the input CfgQmi object (contexts use OrderedDict as default_factory so should be ordered in the input order). So, simply reversing the list order of context names in L954 of proc.py:
for context_name in context_names:
to
for context_name in context_names[::-1]:
should address this request.

Note that this change applies then also to context names obtained with select_contexts(cfg) so also those would be also stopped in reverse order. The select_context_by_name returns only one context, so it does not change the result for this. We cannot edit the order reverse in select_local_contexts directly, as this is also used for starting the contexts. We could optionally include second input parameter reverse=False for this and if it is set to True, then return context_names[::-1] instead of context_names.

Modules to be created

None

Modules to be modified

  • qmi.tools.proc

Tests to be created/updated

Currently, the unit-tests do not test for closing more than one service at a time. So, the change would not affect current unit-testing scheme. But, if we want to prove with unit-tests that the inverse order is used for stopping w.r.t. start, we would need to add test(s) for it

  • tests.tools.test_proc

Documentation to be updated

CHANGELOG.md

Hardware

A node using qmi_proc to start multiple local services.

#QMI Add/make to work Sphinx documentation runner

In the qmi project we have already the documentation ready for Sphinx. We should now try to do it the "Github" way and push the documentation into readthedocs. There are some possible actions in the marketplace to do this, see https://github.com/marketplace?type=actions&query=sphinx+.

The pennylane-quantuminspire project uses the following: https://github.com/QuTech-Delft/pennylane-quantuminspire/blob/master/.github/workflows/docs.yml.

Figure out how we can do the publishing, with the target of publishing in the "readthedocs".

Add MultiHarp and PicoHarp files into the codebase

WE should add the MultiHarp and PicoHarp QMI driver files into the qmi/instruments/picoquant module. These should include the reference to the original PicoQuant's software drivers. Also add the referencing to the HydraHarp instrument as well.

Also add the respective unit-tests into tests/instruments/picoquant.

Update CHANGELOG.md

Add CI workflows into the .github directory

Description

Add the CI pipeline into the repo. This should go under a new folder in .github directory called workflows with the name github-ci.yml. This could be copied over from our GitLab QMI project with all the GitLab-specific parts either adjusted for GitHub or removed. For example, the twine username and password for installing wheel and creating the package should be figured out. Consult @fer to figure out how this should be done as it also reflects to creating the package in the TBD PyPi project page.

Make sure that the scripts directory with the relevant run scripts is present before running the pipeline for the first time. Check and upload those with this ticket if applicable.

Files to be added

  • .github\workflows\github-ci.yml

NewFocus TLB-670x driver erroneous response handling

Description

For some reason with certain TLB-6700 controllers, the QMI driver gets erroneous responses to queries. It was reported that the get_piezo_voltages returns perhaps one time out of five partial controller IDN string without any clear reason. With some closer look at the driver and logging in debug mode, there was no reason to suspect that the driver would have encountered an error and called *IDN? at re-initialization. Thus this likely is a fluke of the controller or its firmware or the hardware driver. We cannot fully close out the possibility that multiple QMI connections to the controller were open, which could also cause issues like this, as the testing was done on otherwise active setup and not all other processes could not be closed.

On another setup with the same model controller, QMI driver, firmware version and hardware drivers, the issue did not appear. On superficial level no differences should be present in the setups regarding the laser controller.

Earlier, a similar issue with response "OK\r\n" to the query, instead of the query value, was fixed by checking the response before trying to parse or cast it. A similar check should be done also now for the sudden *IDN? response. The response, according to the user manual, is of format 'NEW_FOCUS XXXX vYYY mm/dd/yy, SNZZZZ'. We see this, although the response size seems to be capped and the first letters were cut, leaving something like cus TLB-6700 v2.4 31/09/23 SN12345. As the exact model, version, date and serial number can all change, we should make a generic check with f.ex. checking only the last part, starting from the version, but by recognizing only that numbers are present at expected places around the spaces and characters.

Circumvent the issue by checking the response length in _send():

            if len(response) > 2 and len(response[-2]) > 0 and response[-2] != "OK":
                # The response probably is this. This presumes the last value in list is [''] due to "...\r\n" split.
                # But check for sporadic "*IDN?" query response first.
                idn_match = # TODO: some kind of regular expression check on the string of response[-2] (or alternative)
                if idn_match:  # The entry is the invalid response, delete it
                    del response[-2]

                # OK check. Otherwise returns response[0]
                if response[-2] != "OK":
                    return response[-2]

A further improvement could also be made: As the device IDs are "fixed" once the hardware is on the setup, re-initialization and un-initialization should not have ANY effect on the device ID. Now, once the driver is started and the instrument opened, we have the device ID stored in self._device_id. The valid ID range is 0-31. So, instead of setting this in class __init__ as : int = 0, we could do : int = -1. This will be set to the current device's Device ID number in open() which in turns checks this value in _init_device(). Currently, if there is a need to re-initialize the device, this value is obtained again parsing the _get_device_info()
call result, but as the number should be the same as before, this is not necessary.

The proposal is to add an extra check after the self._handle.newp_usb_init_product(self.PRODUCT_ID) call to see if the self._device_id is/has been changed to be in the range 0-31, then all the following steps are not necessary and the function should return directly. Also, the function should set itself the self._device_id and not return it to open() in that case, to make things a bit simpler.

    def _init_device(self) -> None:
        """Find and initialize the device ID for the device with the given serial number."""
        # Open all USB devices that match the USB product ID.
        self._handle.newp_usb_init_product(self.PRODUCT_ID)
        if self._device_id in range(32):
            return
        
        ....
        self._device_id = our_device_id

    @rpc_method
    def open(self) -> None:
        """Open connection to the device controller."""
        super().open()
        _logger.info(f"Opening connection to {self._name}")
        self._init_device()

Modules to be modified

  • qmi.instruments.newport.tlb_670x.py

Tests to be created/updated

  • tests.instruments.newport.test_tlb_670x.py

Documentation to be updated

  • CHANGELOG.md

Hardware

Test on controller if possible.

Add generic QMI power meter

Description

Having a base class for the power meters in QMI, will generalise power meters so they can be used in the software layers above QMI. A QMI_Powermeter will implement one method called get_power.

For example the existing Thorlabs PM100D power meter will be defined as:

class Thorlabs_PM100D(QMI_Powermeter):
...

QMI_Powermeter will inherit from QMI_Instrument. The following power meters will be redefined to follow this structure:

  • Thorlabs PM10x
  • Newport 843R

These power meters will keep their existing definition/API but will also implement the new methods given by QMI_Powermeter

Another solution is to have QMI_Powermeter be a mixin class. This way a concrete QMI instrument can implement multiple mixins.
Then the existing Thorlabs PM100D power meter will be defined as:

class Thorlabs_PM100D(QMI_Instrument, QMI_Powermeter):
...

A caveat for this solution is that the metaclass clash between and ABC and QMI_RpcObject must be fixed first.

A third solution would be to Protocols. With this the existing class definitions of the instruments stay the same, but they will need to add the method(s) defined by the protocol. The protocol would be defined as:

class QMI_Powermeter(Protocol):
    def get_power(self) -> float:
        ...

Each QMI_Instrument that would like to adhere to this protocol need only implement the get_power method with the same signature.

The final solution would be to create a concrete version of each power meter based on a base class QMI_Powermeter. Then every power meter would implement a concrete version of QMI_Powermeter. This would decouple the QMI_Instrument from the powermeter of itself.

Modules to be created

  • qmi.instruments.types.power_meter

Modules to be modified

  • n/a

Tests to be created/updated

  • unittests

Documentation to be updated

  • Spinx documentation

Hardware

  • n/a

Use reusable workflows

Description

Our pipelines have several steps that are the same. We can refactor our pipelines to use reusable workflows. For example we can define a workflow mypy.yml and use that in our pipelines, instead of rewriting the same mypy step in each pipeline. This also allows us to easily incorporate existing steps/jobs in each pipeline.

Modules to be created

  • n/a

Modules to be modified

  • n/a

Tests to be created/updated

  • n/a

Documentation to be updated

  • n/a

Hardware

  • n/a

Add GPIB transport into QMI [Windows only]

Description

Currently, the QMI transport layer does not have specific handling of a GPIB interface. Even though, not anymore commonly used, it is still present in some instrumentation like Tektronix's AWG 5014 model. And currently this interface is used for the AWG at the RT2 setup.

The GPIB device is usually a device that, when controlled through a PC, has an Ethernet, USB or similar interface to connect to the control PC. On some setups, USB/ethernet Prologix GPIB devices are used. The USB connector works on Linux, Mac and Windows and can use a VCP driver on Windows to 'serialize' the connection. Therefore, we can utilize these instruments with the regular serial/USB/tcp transport interfaces.
On the contrary, the National Instrument's GPIB connector although having these and several more interfacing options, the USB connection cannot be used with the present transport implementations. The 'drawback' of the NI GPIB USB connector is that it needs the pyvisa package to be used with SCPI-protocol instruments and works only on Windows. The construction of the string to give to the pyvisa manager is not the same as for USBTMC instruments, which creates the incompatibility.

Proposed addition

When using NI GPIB USB connector on Windows, we need to be able to connect to it using pyvisa. For pyvisa we need to input for target device, which is a string. For this partuclar case, it is in the format "GPIB::X::INSTR" where X is a device number, e.g. 1. We need

  • a transport parser class GpibTransportDescriptorParser in the transport.py
GpibTransportDescriptorParser = TransportDescriptorParser(
    "gpib",
    [],
    {'devicenr': (int, True),
     'timeout': (int, False)})
  • a transport_gpib_visa.py module that has main class QMI_VisaGpibTransport which inherits from QMI_Transport.
    • At constructor we want to have input devicenr: int and optional input timeout: int = 40000.
    • We need to override method _open_transport(self) with
visa_resource = f"GPIB::{self.devicenr}::INSTR"
rm = pyvisa.ResourceManager()
self._device = rm.open_resource(visa_resource, timeout=self.timeout, write_termination='\n',
                                read_termination='\n')
  • On this will be added also a try-except and exception handling as appropriate.
  • property _safe_device and methods write and _read_message can most likely be included the same way they are included in transport_usbtmc_visa.py. It is not clear if the static method list_resources() is needed - evaluate while implementing the class.
  • close(self) method should also be in like in QMI_UsbTmcTransport class.
  • On create_transport method in transport.py needs to be added handling for string format "gpib:devicenr[:timeout]". A new elif GpibTransportDescriptorParser.match_interface(...) is needed to return the correct transport class.

Affected components

QMI

Modules to be created

  • qmi/core/transport_gpib_visa.py

Modules to be modified

  • qmi/core/transport.py

Tests to be created/updated

  • tests/core/test_transport.py
  • tests/core/test_pyvisa_gpib_transport.py

Documentation to be updated

  • CHANGELOG.md
  • Sphinx documentation
  • QMI Confluence page

Hardware

A Windows GPIB-connected instrument with NI's USB-GPIB device.

Implement `read_until_timeout` and `discard_read` in QMI_UsbTmcTransport class

Description

For the UsbTmc, the current read and/or read_until and/or read_until_timeout should (try to) return the whole _read_buffer AND whatever the _read_message(timeout) returns, up to their nbytes or message_terminator limits. This is not happening now. Also discard_read should get implemented.

  1. The read method should be modified to work the same way as the QMI_Transport.read base class. It should also start with the existing buffer check
        nbuf = len(self._read_buffer)
        if nbuf >= nbytes:
            # The requested number of bytes are already in the buffer.
            # Return them immediately.
            ret = bytes(self._read_buffer[:nbytes])
            self._read_buffer = self._read_buffer[nbytes:]
            return ret

And if no buffer is present, then check the timeout value as now and read-in the buffer.

        # USB requires a timeout
        if timeout is None:
            timeout = self.DEFAULT_READ_TIMEOUT

        # Read buffer was too short or is empty - read a new message from the instrument.
        self._read_buffer.extend(self._read_message(timeout))

Then re-check the buffer length. By too short buffer length discard buffer and raise exception. By too long data discard buffer after nbytes and return nbytes data like now. And the with nbytes of data return and clear whole data buffer.

  1. The read_until (of the QMI_UsbTmcTransport) command says in the description that the message_terminator is ignored as it is not somehow valid for USBTMC protocol. This means that actually this command should not be implemented and be set with NotImplementedError and the read_until_timeout be implemented instead. The problem here is that instruments using SCPI protocol use ScpiProtocol class where in ask the read_until is used by default. We cannot change this to read_until_timeout there as also other transports can use this and that would mess things up. So, we would need to keep read_until but change the implementation such that it forwards then to read_until_timeout.

NOTE: If there remains partial messages in the self._read_buffer and this keeps messing up the parsing of responses or similar, currently it is up to the user or e.g. implemented QMI driver commands to call discard_read() to reset the buffer.

  1. Set the discard_read method of the QMI_UsbTmcTransport class to clear the current buffer (self._read_buffer = bytes<array>()).
def discard_read():
    # We should empty the buffer, or if it is empty, discard the next message from the source
    if not len(self._read_buffer):
        # Read buffer is empty - read a new message from the instrument.
        self._read_message(self.DEFAULT_READ_TIMEOUT)

    else:
        self._read_buffer = bytes()     

Affected components

QMI
All affected drivers in diamondos and qmi - TBD

Modules to be created

N/A

Modules to be modified

qmi.core.transport

Tests to be created/updated

tests.core.test_transport_usbtmc

Documentation to be updated

CHANGELOG.md

Enhancement: Device Driver Rhode Schwarz ZNBx

Is your feature request related to a problem? Please describe.
I am wondering if you are planning or working on introducing device drivers for Rhode Schwarz instruments like Oscilloscope [1] / VNAs [2] etc.

Describe the solution you'd like
Device driver for ZNB4,6,8,20.

Describe alternatives you've considered
I guess using Qcodes is an alternative.

[1] https://www.rohde-schwarz.com/us/products/test-and-measurement/oscilloscopes_63663.html
[2] https://www.rohde-schwarz.com/us/products/test-and-measurement/network-analyzers/rs-znb-vector-network-analyzer_63493-11648.html

Mypy stage of pipeline fails but pipeline passes

Describe the bug
If the mypy stage of the pipelines fail, then it is still reported as a success and the whole pipeline can pass. This results in PRs that can be merged but should not.

With this bugfix, the radon stages will also be removed. They are not important for us and the complexity analysis can be taken over by pylint.

To Reproduce
Make a mypy error in the code.
Push it
Pipeline reports mypy errors, but passes

Expected behavior
Pipeline should fail if mypy fails.

Screenshots
image

Operating System (please complete the following information):

  • n/a

Additional context
-n/a

Remove unittests stage

Description

The current workflows run the unittests twice-once while running the unittests stage and once while running the coverage stage. The coverage stage should suffice and the unittests stage can be removed.

Acceptance criteria

  • Unittests stage removed
  • New badge showing tests passing
  • Remove shell script for coverage

License and acknowledgements

Description

Modify in the LICENCE the first line of text to be "This work is licensed under a MIT OSS licence".

Add acknowledgements to TUD in a ACKNOWLEDGEMENTS text file (not strictly necessary).

Figure out where the waiver text
"Technische Universiteit Delft hereby disclaims all copyright interest in the program “QMI” (Quantum Measurement Infrastructure, a lab instrument remote procedure call control framework) written by the QuTech.
Prof.dr.ir. Paulien Herder, Dean of Applied Physics"
should go.

These changes due to the requirements by the "TU Delft Research Software Guide" document for OSS.

Create a QMI driver for SMC100PP and separate methods correctly with SMC100CC

Description

From Newport SMC100CC/PP Firmware manual, it can be seen that even if the two hardware models share the same protocol to communicate, not all commands can be executed on both models. See table

Image

The current implementation is valid for SMC100CC only. We can add also a SCM100PP QMI driver by also inheriting from the Newport_Single_Axis_Motion_Controller and adding SMC100PP-specific calls as RPC method in that driver (namely, commands "FR" and "VB"). For SMC100CC also several CC-model specific calls can be added under the Newport_SMC100CC class. One CC-specific command has already been implemented in Newport_Single_Axis_Motion_Controller, "SU", which get/set methods need to be moved into the inheriting class.

There are also several not-implemented calls in the table that could be added as new methods under the Newport_Single_Axis_Motion_Controller class.

Also add new actuator in actuators.py:

UTS100PP: LinearActuator = LinearActuator(
    100, 20, 0.05, 300e-6, 200, 0.0)

Read well the documentation about those calls from the firmware and command interface manuals. Implement the non-implemented methods in respective classes.

Affected components

QMI, Newport SMC100CC QMI driver, base class

Modules to be created

qmi.instruments.newport.smc_100pp.py

Modules to be modified

qmi.instruments.newport.actuators.py
qmi.instruments.newport.smc_100cc.py

Tests to be created/updated

  • tests.instruments.newport.test_smc_100pp.py
  • tests.instruments.newport.test_smc_100cc.py

Documentation to be updated

CHANGELOG.md

Hardware

Hardware testing on SMC100CC and SMC100PP instruments.

New QMI driver for Thorlabs MPC320

Description

The Thorlabs USB device(s) use(s) FTDI peripehral chips for PC communication. For these, there are (USB-to-serial) drivers available for Windows and Linux from FTDI. "Before any PC USB communication can be established with an
Thorlabs controller, the client program is required to set up the necessary FTDI chip serial
port settings used to communicate to the Thorlabs controller embedded system. Within the
Thorlabs software itself the following FTDI library calls are made to set up the USB chip serial
port for each Thorlabs USB device enumerated on the bus." Example:

// Set baud rate to 115200.
ftStatus = FT_SetBaudRate(m_hFTDevice, (ULONG)uBaudRate);
// 8 data bits, 1 stop bit, no parity
ftStatus = FT_SetDataCharacteristics(m_hFTDevice, FT_BITS_8, FT_STOP_BITS_1, 
FT_PARITY_NONE);
// Pre purge dwell 50ms.
Sleep(uPrePurgeDwell);
// Purge the device.
ftStatus = FT_Purge(m_hFTDevice, FT_PURGE_RX | FT_PURGE_TX);
// Post purge dwell 50ms.
Sleep(uPostPurgeDwell);
// Reset device.
ftStatus = FT_ResetDevice(m_hFTDevice);
// Set flow control to RTS/CTS.
ftStatus = FT_SetFlowControl(m_hFTDevice, FT_FLOW_RTS_CTS, 0, 0);
// Set RTS.
ftStatus = FT_SetRts(m_hFTDevice);

So with a USB-to-serial driver, we set up the comms with baud rate of 115200 and the rest are default (also RTS/CTS Handshake).

USB Device Enumeration

The USB devices connected to the PC are enumerated, and serial numbers are read from the devices. For MPC320 the
serial number appears to start with "38", as seen on hardware manual p5. The number "38" is followed by six digits, which are the actual serial number. "38" is the harwadre type indicator.

APT Communication protocol

The protocol communicates through messages, which have a structure that always starts with a fixed length, 6-byte message header which, in some cases, is followed by a variable length data packet.
The header part of the message always contains information that indicates whether a data
packet follows the header and if so, the number of bytes that the data packet contains.
Values that are longer than a byte follow the Intel little-endian format.

Bytes 0 & 1 are the message ID, describing the action the message requests.
Bytes 2 & 3 give the data packet length if data packet follows or
Byte 2 gives param1 and Byte 3 gives param2. If no parameters are required, these are 0.
Byte 4 gives the destination module, or destination module | 0x80 for data packet. This
gives indication if a data packet follows or not. If the MSB of byte 4 is set, then the message will be followed by a data packet.
Byte 5 gives the source of the message.

As we have a system with 3 sub-modules (paddles), we probably have a communication system that goes through the devices motherboard to these sub-modules. The source bytes therefore likely will be

0x01 Host controller (i.e., control PC)
0x11 Rack controller, motherboard in a card slot system or comms router board
0x21 Bay 0 in a card slot system (paddle 1)
0x22 Bay 1 in a card slot system (paddle 2)
0x23 etc (paddle 3)

"The host sends a message to the motherboard that the sub-modules are plugged into, with the destination field of each message indicating which slot the message must be routed to... In slot-type systems the host can also send messages to the motherboard that the sub-modules are plugged into (destination byte = 0x11). In fact, as a very first step in the communications process, the host must send a message to the motherboard to find out which slots are used in the system... Sub-modules never send SET and REQUEST messages to the host and GET messages are always sent to the host as a destination."

Format Specifiers

  • word: Unsigned 16- bit integer (2 bytes) in the Intel (little-endian) format.
    For example, decimal 12345 (3039H) is encoded as the byte sequence 39, 30.
  • short: Signed 16-bit integer (2 bytes) in 2’s compliment format.
    For example, decimal -1 is encoded as the byte sequence FF, FF.
  • dword: Unsigned 32-bit integer (4 bytes) in the Intel (little-endian) format.
    For example, decimal 123456789 (75BCD15H) is encoded as the byte sequence 15, CD, 5B, 07
  • long:
    • Signed 32-bit integer (4 bytes) in 2’s compliment format.
      For example, decimal -1 is encoded as the byte sequence FF, FF.
    • 4 bytes in the Intel (little-endian) format.
      For example, decimal -123456789 (FFFFFFFFF8A432EBH) is encoded as the byte sequence EB, 32, A4, F8,.
  • char: 1 byte (2 digits).
  • char[N]: string of N characters.

Messages Applicable to MPC220 and MPC320

Inputs:
chanID should be possible to give two channels as input 0x01 or 0x02. But MPC320 probably has only one "channel", 0x01. Not fully clear.
destID is 0x11 for controller and 0x21-0x23 for paddles 1, 2, 3, respectively. Also called as "bays" in documentation.
hostID byte 5 is "always" 0x01, the Host Controller.
de0x80 like destID but inputted with the logical OR 0x80.
ho0x80 like hostID but inputted with the logical OR 0x80.
enabSt Enable states, 0x01 for enable, 0x02 for disable
dir The direction to Jog. 0x01 to jog forward, or 0x02 to jog in the reverse direction.
StopMd 0x01 to stop immediately, or 0x02 to stop in a controller (profiled) manner.
reqbuf A 'ctypes.int' (or so) modifiable buffer value that will be changed during command. Usually the single-byte "get" response.
data bits if the inputted command expects input value(s) more than 1 byte. This will be appended to the command header as data bits (no buffer!).

Returns:
data bits if the inputted command expects a response. This will be appended to the command (6 bits) as a buffer that the device response will fill.
The format on the table is ": <start_byte-end_byte description>, <start_byte2-end_byte2 description2>, ..."
returns is what the Python implementation should return

Command name hex ID byte 0 byte 1 byte 2 byte 3 byte 4 byte 5 data bits returns
MGMSG_MOD_IDENTIFY 0x0223 0x23 0x02 chanID 0x00 destID hostID No None (flashes LED of controller)
MGMSG_MOD_SET_CHANENABLESTATE 0x0210 0x10 0x02 chanID EnabSt destID hostID No None
MGMSG_MOD_REQ_CHANENABLESTATE 0x0211 0x10 0x02 chanID 0x00 destID hostID No N/A
MGMSG_MOD_GET_CHANENABLESTATE 0x0212 0x10 0x02 chanID reqbuf destID hostID No int (bool)
MGMSG_HW_DISCONNECT 0x0002 0x02 0x00 0x00 0x00 destID hostID No None
MGMSG_HW_START_UPDATEMSGS 0x0011 0x11 0x00 0x00 0x00 destID hostID No None
MGMSG_HW_STOP_UPDATEMSGS 0x0012 0x12 0x00 0x00 0x00 destID hostID No None
MGMSG_HW_REQ_INFO 0x0005 0x05 0x00 0x00 0x00 hostID destID No N/A
MGMSG_HW_GET_INFO 0x0006 0x06 0x00 0x54 0x00 ho0x80 hostID 84: 6-9 SN#, 10-17 model#, 18-19 type, 20-23 FW ver, 24-83 N/A, 84-85 HW ver, 86-87 Mod state, 88-89 end chars QMI_InstrumentIdentification
MGMSG_RESTOREFACTORYSETTINGS 0x0686 0x86 0x06 chanID 0x00 destID hostID No None
MGMSG_MOT_SET_POSCOUNTER 0x0410 0x10 0x04 0x06 0x00 de0x80 hostID 6: 6-7 chanID, 8-11 position None
MGMSG_MOT_REQ_POSCOUNTER 0x0411 0x11 0x04 chanID 0x00 destID hostID No N/A
MGMSG_MOT_GET_POSCOUNTER 0x0412 0x12 0x04 0x06 0x00 de0x80 hostID 6: 6-7 chanID, 8-11 position float
MGMSG_MOT_MOVE_HOME 0x0443 0x43 0x04 chanID 0x00 destID hostID No None
MGMSG_MOT_MOVE_HOMED 0x0444 0x44 0x04 chanID 0x00 hostID destID No N/A (used in combination with 0x0443 only)
MGMSG_MOT_MOVE_COMPLETED 0x0464 0x64 0x04 0x0E 0x00 ho0x80 destID 14: ??? N/A (used in combination with move commands)
MGMSG_MOT_MOVE_ABSOLUTE 0x0453 0x53 0x04 0x06 0x00 destID hostID 6: 6-7 chanID, 8-11 abs_dist None
MGMSG_MOT_MOVE_JOG 0x046A 0x6A 0x04 chanID dir destID hostID No None
MGMSG_MOT_MOVE_STOP 0x0465 0x65 0x04 chanID StopMd destID hostID No None
MGMSG_MOT_MOVE_STOPPED 0x0466 0x66 0x04 0x0E 0x00 ho0x80 destID 14: ??? N/A (used in combination with 0x0465 only)
MGMSG_MOT_SET_EEPROMPARAMS 0x04B9 0xB9 0x04 0x04 0x00 de0x80 hostID 4: 6-7 chanID, 8-9 message ID None
MGMSG_MOT_GET_DCSTATUSUPDATE 0x0491 0x91 0x04 0x0E 0x00 de0x80 hostID 14: 6-7 chanID, 8-11 position, 12-13 velocity, 14-15 motor curr, 16-19 status bits Tuple[float, float, float, int]
MGMSG_MOT_REQ_DCSTATUSUPDATE 0x0490 0x90 0x04 chanID 0x00 destID hostID No N/A
MGMSG_POL_SET_PARAMS 0x0530 0x30 0x05 0x0A 0x00 de0x80 hostID 12: 6-7 N/A, 8-9 velocity, 10-11 homepos, 12-13 jogstep1, 14-15 jogstep2, 16-17 jogstep3 None
MGMSG_POL_REQ_PARAMS 0x0531 0x31 0x05 0x00 0x00 destID hostID No N/A
MGMSG_POL_GET_PARAMS 0x0532 0x32 0x05 0x0A 0x00 ho0x80 destID 12: 6-7 N/A, 8-9 velocity, 10-11 homepos, 12-13 jogstep1, 14-15 jogstep2, 16-17 jogstep3 Tuple[float, float, List[int]]

Note that we do not suggest yet specific names for the Python functions. Also note that query functions can be combinations of two or more commands. E.g.
get_idn() -> QMI_InstrumentIdentification needs to first send the request (0x0005) and the obtain the respose with another command (0x0006).
Note also that the data bits can have specific format specifiers per byte range. Check the manual for those specifiers.

Figure out motor type and which conversions might be needed. As Thorlabs parameters are integer values, the Thorlabs values calculated from the conversion equations need to be rounded to the nearest integer. This is mostly done already in a scientist-made driver for the device, see instruments.thorlabs.mpc320 in 169-ltfiber-experiments-towards-transmission-dip branch in Diamondos.

As the motor types are different and the driver could be applied also to other hardware of Thorlabs, it could be beneficial to directly make the driver the same way as the Newport single_axis_motion_controller.py where the actuators (motors) are defined in actuators.py,
where we would now define the conversion constants as part of the actuator (motor) class. Also we can directly implement two classes, the linear and the rotational one.

Modules to be created

  • qmi.instruments.thorlabs._motors.py
  • qmi.instruments.thorlabs._apt_protocol.py
  • qmi.instruments.thorlabs.mpc320.py

Modules to be modified

  • qmi.instruments.thorlabs.__init__.py

Unit-tests to be created or modified

  • tests.instruments.thorlabs.test_apt_protocol.py
  • tests.instruments.thorlabs.test_mpc320.py

Documentation to be updated

  • CHANGELOG.md
  • Confluence

Hardware

Thorlabs MPC320 (this will need to be added to the dialout group if using on Linux)

Change _RpcObjectMetaClass to inherit from ABCMeta

Description

Any classes that inherit from _RpcObjectMetaClass cannot be used as mixins. To fix this we need _RpcObjectMetaClass to inherit from ABCMeta

Modules to be created

  • n/a

Modules to be modified

  • qmi.core.rpc

Tests to be created/updated

  • unittests

Documentation to be updated

  • n/a

Hardware

  • n/a

QMI Driver for WL Photonics Wavelength Tunable Filter

Description

We would like to have a driver for a wavelength-tunable narrowband filter from WL Photonics. The required function is the ability to get and set the center wavelength. The type of filter is WLTF-NE-S-1310-95/0.1-SM-3.0/1.0-FC/APC-USB. On windows, for that a common USB-to-serial driver is needed. For lots of other instruments https://ftdichip.com/drivers/ has worked so hopefully it can be used also for this instrument (no licensing issues using this). For Linux the situation is not as straightforward, but we could try to see if a generic serial driver works. If not, we might need to expand the driver a bit to work correctly also with QMI's USBTMC driver. In second case we will probably also need to figure out the instrument's vendor and product IDs.

As there is no mention in the documentation about serial communication settings, we must presume all default values are used. From documentation found on an internet forum, though, we see that the communication speed is 115200Baud/s. As we are dealing with USB2.0+ so more speed should be fine. But for the sake of possible future use of pure serial instrument this should be defined in the transport string and not in the driver. lsusb recognized the device as ID 10c4:ea60 Cygnal Integrated Products.

Usually the filter is operated as described in https://www.wlphotonics.com/product/Narrowband_Tunable_Filter.pdf

From the HyperTerminal example in the document we can extract commands that we need to implement:

  • dev? For the use of get_idn(), and it also returns the wavelength range of the instrument, so it can also be used for setting software limits for the inputs. It could be incorporated to be part of the open() (with internal function(s)). If extra commands with steps are included, we can extract the step range from this response as well.
  • set_center_wavelength(self, wavelength: float) -> None
    • wlxxxx to set the center wavelength, where xxxx is the wavelength (in nanometers). The example shows only an integer number as xxxx, so it was unclear if and how we could set a fractional nanometer. From tests we found out it is also possible to use floating point values, but we limit it to 3 decimals.
  • get_center_wavelength(self) -> float
    • wl? Returns the current wavelength value in nanometers

Some possible extra commands are

  • reverse_motor(self, steps: int) -> None
    • sbyyy where the motor is requested to step back yyy steps. yyy will be an integer
  • forward_motor(self, steps: int) -> None
    • sfzzz where the motor is requested to step forward zzz steps. zzz will be an integer
  • get_steps(self) -> int
    • s? Returns current step number, error code and status code.
  • go_to_zero(self) -> None
    • z is "Go to Zero". So some kind of motor homing command, setting the motor to 0 and wavelength beyond max value. But this probably does also calibration of the motor.

The command interface excepts and returns carriage return and newline at the end of each line (/r/n).

Affected components

QMI when prototype is hardware-tested.

Modules to be created

  • qmi/instruments/wl_photonics/__init__.py
  • qmi/instruments/wl_photonics/wltf_n.py (_n for "Narrowband", future implementations are possible: _w for "Wideband", _ba for "Bandwith-adjustable". _b for "Bandpass", _x00 for electric versions). Class name WlPhotonics_WltfN

Tests to be created/updated

  • tests/instruments/wl_photonics/test_wltf_n.py

Hardware

  • Santec TSL570 laser
  • WL Photonics WLTF
  • IR Spectrometer

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.