qutech-delft / qmi Goto Github PK
View Code? Open in Web Editor NEWQuantum Measurement Infrastructure
License: Other
Quantum Measurement Infrastructure
License: Other
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_method
s 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.
QMI
qmi/instruments/tenma/psu_72.py
N/A
tests/instruments/tenma/test_psu_72.py
CHANGELOG.md
Tenma 72-2550 & 72-13360 can be used for testing and are both available at Hansonlab
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)}
)
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.
...sendto(data, self._address)
instead of ...sendall(data)
as the binding of the socket does not define target (client).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.
QMI
N/A
qmi.core.transport
Add tests into tests/core/test_transport.py
CHANGELOG.md
documentation\sphinx\source\welcome.rst
NEW: Add in the "nice features" a description which transport protocols QMI can handle.To be tested with Tenma power supplies in ticket https://github.com/orgs/QuTech-Delft/projects/2/views/1?pane=issue&itemId=47295046
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.
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.
To Reproduce
move_relative(-0.01)
Expected behavior
Screenshots
Operating System (please complete the following information):
Additional context
Describe the bug
The READTHEDOCS pipeline does not work anymore. It fails with the following message: https://readthedocs.org/projects/qmi/builds/21742071/
To Reproduce
Run pipelines for a pull request.
Expected behavior
The pipeline for creating the READTHEDOCS documentation fails.
Screenshots
N/A
Operating System (please complete the following information):
Additional context
N/A
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)
ScpiProtocol
. The proposed changes do not change the interfaces (only read
and read_until
are in ScpiProtocol
), but perhaps something will function differently.QMI
N/A
qmi.core.transport
CHANGELOG.md
In README.md
there is a spelling error in the word "infrastructure". Correct it.
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...").
qmi.instruments.newport.tlb670x
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
:
time_base
to sampling_interval
(both int
) and time_span
to allow a float
instead of int
.sampling_interval
: Sampling interval in Nanoseconds. Must be 1, 2, 4 or a multiple of 8 ns.time_span
needs correction in the docstring, and the times
return value would be returned also in seconds instead of "us".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")
number_samples = int(time_span / (sampling_interval))
_logger.info(f"Timespan = %i Num samples = %i", time_span, number_samples)
# 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
).
N/A
qmi.instruments.picoscope.picoscope3403
tests.instruments.picoscope.test_picoscope3403
CHANGELOG.md
PicoScope3403 model. Preferably with 4 channels.
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:
*IDN
*RST
*OPC?
*CLS
:WAVelength
:WAVelength:UNIT
:WAVelength:FINe
:WAVelength:FINetuning:DISable
:COHCtrl
:POWer:STATe
:POWer
:POWer:ACTual?
:WAVelength:SWEep:STARt
:WAVelength:SWEep:STOP
:WAVelength:SWEep:MODe
:WAVelength:SWEep:SPEed
:WAVelength:SWEep:CYCLes
:WAVelength:SWEep:STATe
:READout:DATa?
: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.
qmi / QMI
qmi/instruments/santec/__init__.py
qmi/instruments/santec/tsl_570.py
tests/instruments/santec/test_tsl_570.py
Santec laser controller.
CHANGELOG.md
.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.
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.
The more neutral default branch name should be used. Change default branch name to 'main'. See: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-branches-in-your-repository/changing-the-default-branch
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
.
None
qmi.tools.proc
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
CHANGELOG.md
A node using qmi_proc
to start multiple local services.
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".
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 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.
.github\workflows\github-ci.yml
hijljjiljjj
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()
qmi.instruments.newport.tlb_670x.py
tests.instruments.newport.test_tlb_670x.py
CHANGELOG.md
Test on controller if possible.
The Sphinx documentation for readthedocs.io does not create the API reference for all classes automatically, see https://qmi.readthedocs.io/en/latest/api_reference.html. The .readthedocs.yml
is quite standard, so more likely the culprit is some error in the conf.py.
This should be investigated and fixed.
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:
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.
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.
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.
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
GpibTransportDescriptorParser
in the transport.py
GpibTransportDescriptorParser = TransportDescriptorParser(
"gpib",
[],
{'devicenr': (int, True),
'timeout': (int, False)})
transport_gpib_visa.py
module that has main class QMI_VisaGpibTransport
which inherits from QMI_Transport
.
devicenr: int
and optional input timeout: int = 40000
._open_transport(self)
withvisa_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')
try-except
and exception handling as appropriate._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.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.QMI
qmi/core/transport_gpib_visa.py
qmi/core/transport.py
tests/core/test_transport.py
tests/core/test_pyvisa_gpib_transport.py
A Windows GPIB-connected instrument with NI's USB-GPIB device.
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.
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.
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 calldiscard_read()
to reset the buffer.
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()
QMI
All affected drivers in diamondos and qmi - TBD
N/A
qmi.core.transport
tests.core.test_transport_usbtmc
CHANGELOG.md
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
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.
Operating System (please complete the following information):
Additional context
-n/a
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.
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.
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
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.
QMI, Newport SMC100CC QMI driver, base class
qmi.instruments.newport.smc_100pp.py
qmi.instruments.newport.actuators.py
qmi.instruments.newport.smc_100cc.py
tests.instruments.newport.test_smc_100pp.py
tests.instruments.newport.test_smc_100cc.py
CHANGELOG.md
Hardware testing on SMC100CC and SMC100PP instruments.
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).
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.
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."
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.
qmi.instruments.thorlabs._motors.py
qmi.instruments.thorlabs._apt_protocol.py
qmi.instruments.thorlabs.mpc320.py
qmi.instruments.thorlabs.__init__.py
tests.instruments.thorlabs.test_apt_protocol.py
tests.instruments.thorlabs.test_mpc320.py
CHANGELOG.md
Thorlabs MPC320 (this will need to be added to the dialout group if using on Linux)
Any classes that inherit from _RpcObjectMetaClass
cannot be used as mixins. To fix this we need _RpcObjectMetaClass
to inherit from ABCMeta
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 nanometersSome 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 integerforward_motor(self, steps: int) -> None
sfzzz
where the motor is requested to step forward zzz
steps. zzz
will be an integerget_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
).
QMI when prototype is hardware-tested.
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/instruments/wl_photonics/test_wltf_n.py
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.