# ----------------------------------------------------------------------------
# Description : IEEE488.2 interface
# Git repository : https://gitlab.com/qblox/packages/software/qblox_instruments.git
# Copyright (C) Qblox BV (2020)
# ----------------------------------------------------------------------------
# -- include -----------------------------------------------------------------
from __future__ import annotations
from asyncio import Lock
from contextlib import asynccontextmanager
from functools import wraps
from typing import TYPE_CHECKING, TypeVar
from qblox_instruments.ieee488_2.helpers import make_sync_wrapper
from qblox_instruments.types import DebugLevel
if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Awaitable
from typing import Any, Callable
from qblox_instruments.ieee488_2 import Transport
T = TypeVar("T")
# -- function ----------------------------------------------------------------
def ieee488_2_error_check( # noqa: ANN201
minimal_check: bool | Callable[..., Any | None] = False,
):
"""
Factory function for a decorator that catches and checks for errors on an IEEE488.2 call.
Parameters
----------
minimal_check
If True, this decorator will always check for errors unless the debug
level is ``DebugLevel.NO_CHECK``. By default False.
Returns
-------
Callable
The decorator.
Raises
------
RuntimeError
An error was found in system error.
"""
def decorator(func: Callable[..., Any | None]) -> Callable[..., Any | None]:
@wraps(func)
def wrapper(self: Ieee488_2, *args: Any, **kwargs: Any) -> Any | None:
if self._debug in (DebugLevel.ERROR_CHECK, DebugLevel.VERSION_AND_ERROR_CHECK) or (
self._debug == DebugLevel.MINIMAL_CHECK and minimal_check is True
):
error = None
try:
return func(self, *args, **kwargs)
except OSError:
raise
except Exception as err:
error = err
finally:
self._check_error(error)
else:
return func(self, *args, **kwargs)
return wrapper
# if used without parentheses
if callable(minimal_check):
return decorator(minimal_check)
# if used with parentheses
return decorator
def ieee488_2_error_check_async( # noqa: ANN201
minimal_check: bool | Callable[..., Any | None] = False,
):
"""
Factory function for a decorator that catches and checks for errors on an IEEE488.2 call.
Parameters
----------
minimal_check
If True, this decorator will always check for errors unless the debug
level is ``DebugLevel.NO_CHECK``. By default False.
Returns
-------
Callable
The decorator.
Raises
------
RuntimeError
An error was found in system error.
"""
def decorator(func: Callable[..., Any | None]) -> Callable[..., Any | None]:
@wraps(func)
async def wrapper(self: Ieee488_2, *args: Any, **kwargs: Any) -> Any | None:
if self._debug in (DebugLevel.ERROR_CHECK, DebugLevel.VERSION_AND_ERROR_CHECK) or (
self._debug == DebugLevel.MINIMAL_CHECK and minimal_check is True
):
error = None
try:
return await func(self, *args, **kwargs)
except OSError:
raise
except Exception as err:
error = err
finally:
await self._check_error_async(error)
else:
return await func(self, *args, **kwargs)
return wrapper
# if used without parentheses
if callable(minimal_check):
return decorator(minimal_check)
# if used with parentheses
return decorator
# -- class -------------------------------------------------------------------
[docs]
class Ieee488_2: # noqa: N801
"""
Class that implements the IEEE488.2 interface.
"""
__slots__ = ("_debug", "_lock", "_transport")
# ------------------------------------------------------------------------
[docs]
def __init__(self, transport: Transport, debug: DebugLevel = DebugLevel.MINIMAL_CHECK) -> None:
"""
Creates IEEE488.2 interface object.
Parameters
----------
transport : Transport
Transport class responsible for the lowest level of communication
(e.g. ethernet).
debug : DebugLevel
Debug level. See :class:`~qblox_instruments.types.DebugLevel` for more
information. By default None, which means that for a connection to a dummy
cluster, `DebugLevel.ERROR_CHECK` will be used, and for a real cluster,
`DebugLevel.MINIMAL_CHECK`.
"""
self._transport = transport
self._lock = Lock()
self._debug = debug
# ------------------------------------------------------------------------
def _run_in_loop(self, fn: Awaitable[T]) -> T:
return self._transport._run_in_loop(fn)
# ------------------------------------------------------------------------
[docs]
def reconnect(self) -> None:
"""Close and re-open the connection to instrument."""
self._transport.reconnect()
# ------------------------------------------------------------------------
[docs]
async def close(self) -> None:
"""Close the connection to the instrument. It can not be used afterwards."""
if self._transport:
await self._transport.close()
self._transport = None
# ------------------------------------------------------------------------
@asynccontextmanager
async def _connection(self) -> AsyncGenerator[Ieee488_2]:
async with self._lock:
yield self
# ------------------------------------------------------------------------
async def _write_async(self, cmd_str: str) -> None:
"""
Write command to instrument.
Parameters
----------
cmd_str : str
Command string.
"""
await self._transport.write(cmd_str)
_write = make_sync_wrapper(_write_async)
# ------------------------------------------------------------------------
async def _write_bin_async(self, cmd_str: str, bin_block: bytes) -> None:
"""
Write command and binary data block to instrument.
Parameters
----------
cmd_str : str
Command string.
bin_block : bytes
Binary data array to send.
"""
await self._bin_block_write(cmd_str + " ", bin_block)
_write_bin = make_sync_wrapper(_write_bin_async)
# ------------------------------------------------------------------------
async def _read_async(self, cmd_str: str) -> str:
"""
Write command to instrument and read response. Using an empty command
string will skip the write and only read.
Parameters
----------
cmd_str : str
Command string.
Returns
-------
str
Command response string.
"""
await self._transport.write(cmd_str)
data = await self._transport.readline()
return data.rstrip() # Remove trailing white space, CR, LF
_read = make_sync_wrapper(_read_async)
# ------------------------------------------------------------------------
async def _read_bin_async(self, cmd_str: str, flush_line_end: bool = True) -> bytes:
"""
Write command to instrument and read binary data block. Using an empty
command string will skip the write and only read to allow reading
concatenated binary blocks.
Parameters
----------
cmd_str : str
Command string.
flush_line_end : bool
Flush end of line characters.
Returns
-------
bytes
Binary data array received.
"""
if cmd_str != "":
await self._transport.write(cmd_str)
return await self._bin_block_read(flush_line_end)
_read_bin = make_sync_wrapper(_read_bin_async)
# ------------------------------------------------------------------------
async def _write_read_bin_async(
self, cmd_str: str, bin_block: bytes, flush_line_end: bool = True
) -> bytes:
"""
Write command and binary data block to instrument and read binary data
block. Using an empty command string will skip the write and only read
to allow reading concatenated binary blocks.
Parameters
----------
cmd_str : str
Command string.
bin_block : bytes
Binary data array to send.
flush_line_end : bool
Flush end of line characters.
Returns
-------
bytes
Binary data array received.
"""
if cmd_str != "":
await self._write_bin_async(cmd_str, bin_block)
return await self._bin_block_read(flush_line_end)
_write_read_bin = make_sync_wrapper(_write_read_bin_async)
# ------------------------------------------------------------------------
async def _bin_block_write(self, cmd_str: str, bin_block: bytes) -> None:
"""
Write IEEE488.2 binary data block to instrument.
Parameters
----------
cmd_str : str
Command string.
bin_block : bytes
Binary data array to send.
"""
header = cmd_str + Ieee488_2._build_header_string(len(bin_block))
await self._transport.write_binary(header.encode(), bin_block)
await self._transport.write("") # Add a Line Terminator
# ------------------------------------------------------------------------
async def _bin_block_read(self, flush_line_end: bool = True) -> bytes:
"""
Read IEEE488.2 binary data block from instrument.
Parameters
----------
flush_line_end : bool
Flush end of line characters.
Returns
-------
bytes
Binary data array received.
Raises
------
RunTimeError
Header error.
"""
header_a = await self._transport.read_binary(2) # Read '#N'
header_a_str = header_a.decode()
# character ',' is a valid delimiter and we need to discard it
if header_a_str[0] == ",":
# read again to get the digit part of '#N'
digit = await self._transport.read_binary(1)
digit_str = digit.decode()
header_a_str = header_a_str[1] + digit_str # continue as before
if header_a_str[0] != "#":
s = f"Header error: received {header_a}"
raise RuntimeError(s)
digit_cnt = int(header_a_str[1])
header_b = await self._transport.read_binary(digit_cnt)
byte_cnt = int(header_b.decode())
bin_block = await self._transport.read_binary(byte_cnt)
if flush_line_end:
await self._flush_line_end_async()
return bin_block
# ------------------------------------------------------------------------
@staticmethod
def _build_header_string(byte_cnt: int) -> str:
"""
Generate IEEE488.2 binary data block header.
Parameters
----------
byte_cnt : int
Size of the binary data block in bytes.
Returns
-------
str
Header string.
"""
byte_cnt_str = str(byte_cnt)
digit_cnt_str = str(len(byte_cnt_str))
bin_header_str = "#" + digit_cnt_str + byte_cnt_str
return bin_header_str
# ------------------------------------------------------------------------
async def _flush_line_end_async(self) -> None:
"""
Flush end of line <CR><LF> characters.
"""
await self._transport.read_binary(2) # Consume <CR><LF>
_flush_line_end = make_sync_wrapper(_flush_line_end_async)
# -------------------------------------------------------------------------
async def _check_error_async(self, err: Exception | None = None) -> None:
"""
Check system error for errors. Empties and prints the complete error
queue.
Parameters
----------
err : Optional[Exception]
Exception to reraise.
Raises
------
Exception
An exception was passed as input argument.
"""
if err:
raise err from err
_check_error = make_sync_wrapper(_check_error_async)
# -------------------------------------------------------------------------
async def _fast_error_check_async(self, *raw_data: Any) -> None:
"""
Inspects raw SCPI responses for firmware error sentinels.
Triggers a full error queue check if a sentinel is detected.
Parameters
----------
*raw_data : Any
The raw response data returned from the SCPI read operation.
Accepts multiple arguments if a command returns multiple variables.
Raises
------
RuntimeError
An error was found in the system error queue after a
sentinel was detected.
"""
is_error = False
for data in raw_data:
if isinstance(data, (str, bytes)) and len(data) == 0:
is_error = True
break
if is_error:
await self._check_error_queue_async()
fast_error_check = make_sync_wrapper(_fast_error_check_async)
# ------------------------------------------------------------------------
# IEEE488.2 constants
# ------------------------------------------------------------------------
# fmt: off
# *ESR and *ESE bits
_ESR_OPERATION_COMPLETE = 0x01
_ESR_REQUEST_CONTROL = 0x02
_ESR_QUERY_ERROR = 0x04
_ESR_DEVICE_DEPENDENT_ERROR = 0x08
_ESR_EXECUTION_ERROR = 0x10
_ESR_COMMAND_ERROR = 0x20
_ESR_USER_REQUEST = 0x40
_ESR_POWER_ON = 0x80
# fmt: on
# ------------------------------------------------------------------------
# IEEE488.2 commands
# ------------------------------------------------------------------------
# -------------------------------------------------------------------------
async def _get_idn_async(self) -> str:
"""
Get device identity.
Parameters
----------
None
Returns
-------
str
Concatenated list of strings separated by the semicolon character.
The IDN consists of four strings.
Raises
------
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
result = await conn._read_async("*IDN?")
await self._fast_error_check_async(result)
return result
_get_idn = make_sync_wrapper(_get_idn_async)
# -------------------------------------------------------------------------
@ieee488_2_error_check_async
async def _reset_async(self) -> None:
"""
Reset device and clear all status and event registers.
Parameters
----------
None
Returns
-------
None
Raises
------
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
return await conn._write_async("*RST")
_reset = make_sync_wrapper(_reset_async)
# -------------------------------------------------------------------------
@ieee488_2_error_check_async
async def _clear_async(self) -> None:
"""
Clear all status and event registers (see `SCPI <https://www.ivifoundation.org/downloads/SCPI/scpi-99.pdf>`_).
Parameters
----------
None
Returns
-------
None
Raises
------
Exception
Invalid input parameter type.
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
return await conn._write_async("*CLS")
clear = make_sync_wrapper(_clear_async)
# -------------------------------------------------------------------------
async def _get_status_byte_async(self) -> int:
"""
Get status byte register. Register is only cleared when feeding registers are cleared.
Parameters
----------
None
Returns
-------
int
Status byte register.
Raises
------
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
result = await conn._read_async("*STB?")
await self._fast_error_check_async(result)
return int(result)
get_status_byte = make_sync_wrapper(_get_status_byte_async)
# -------------------------------------------------------------------------
@ieee488_2_error_check_async
async def _set_service_request_enable_async(self, reg: int) -> None:
"""
Set service request enable register.
Parameters
----------
reg : int
Service request enable register.
Returns
-------
None
Raises
------
Exception
Invalid input parameter type.
Exception
An error is reported in system error and debug <= 1.
"""
if not isinstance(reg, int):
raise TypeError(
f"Unexpected type for input argument reg, expected int but got {type(reg).__qualname__}." # noqa: E501
)
async with self._connection() as conn:
await conn._write_async(f"*SRE {reg}")
set_service_request_enable = make_sync_wrapper(_set_service_request_enable_async)
# -------------------------------------------------------------------------
async def _get_service_request_enable_async(self) -> int:
"""
Get service request enable register. The register is cleared after reading it.
Parameters
----------
None
Returns
-------
int
Service request enable register.
Raises
------
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
result = await conn._read_async("*SRE?")
await self._fast_error_check_async(result)
return int(result)
get_service_request_enable = make_sync_wrapper(_get_service_request_enable_async)
# -------------------------------------------------------------------------
@ieee488_2_error_check_async
async def _set_standard_event_status_enable_async(self, reg: int) -> None:
"""
Set standard event status enable register.
Parameters
----------
reg : int
Standard event status enable register.
Returns
-------
None
Raises
------
Exception
Invalid input parameter type.
Exception
An error is reported in system error and debug <= 1.
"""
if not isinstance(reg, int):
raise TypeError(
f"Unexpected type for input argument reg, expected int but got {type(reg).__qualname__}." # noqa: E501
)
async with self._connection() as conn:
await conn._write_async(f"*ESE {reg}")
set_standard_event_status_enable = make_sync_wrapper(_set_standard_event_status_enable_async)
# -------------------------------------------------------------------------
async def _get_standard_event_status_enable_async(self) -> int:
"""
Get standard event status enable register. The register is cleared after reading it.
Parameters
----------
None
Returns
-------
int
Standard event status enable register.
Raises
------
Exception
An error is reported in system error and debug <= 1.
All errors are read from system error and listed in the exception.
"""
async with self._connection() as conn:
result = await conn._read_async("*ESE?")
await self._fast_error_check_async(result)
return int(result)
get_standard_event_status_enable = make_sync_wrapper(_get_standard_event_status_enable_async)
# -------------------------------------------------------------------------
async def _get_standard_event_status_async(self) -> int:
"""
Get standard event status register. The register is cleared after reading it.
Parameters
----------
None
Returns
-------
int
Standard event status register.
Raises
------
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
result = await conn._read_async("*ESR?")
await self._fast_error_check_async(result)
return int(result)
get_standard_event_status = make_sync_wrapper(_get_standard_event_status_async)
# -------------------------------------------------------------------------
@ieee488_2_error_check_async
async def _set_operation_complete_async(self) -> None:
"""
Set device in operation complete query active state.
Parameters
----------
None
Returns
-------
None
Raises
------
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
await conn._write_async("*OPC")
set_operation_complete = make_sync_wrapper(_set_operation_complete_async)
# -------------------------------------------------------------------------
async def _get_operation_complete_async(self) -> bool:
"""
Get operation complete state.
Parameters
----------
None
Returns
-------
bool
Operation complete state (False = running, True = completed).
Raises
------
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
result = await conn._read_async("*OPC?")
await self._fast_error_check_async(result)
return bool(int(result))
get_operation_complete = make_sync_wrapper(_get_operation_complete_async)
# -------------------------------------------------------------------------
async def _test_async(self) -> bool:
"""
Run self-test.
Parameters
----------
None
Returns
-------
bool
Test result (False = failed, True = success).
Raises
------
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
result = await conn._read_async("*TST?")
await self._fast_error_check_async(result)
return bool(int(result))
test = make_sync_wrapper(_test_async)
# -------------------------------------------------------------------------
@ieee488_2_error_check_async
async def _wait_async(self) -> None:
"""
Wait until operations completed before continuing.
Parameters
----------
None
Returns
-------
None
Raises
------
Exception
An error is reported in system error and debug <= 1.
"""
async with self._connection() as conn:
await conn._write_async("*WAI")
wait = make_sync_wrapper(_wait_async)