Source code for qblox_instruments.ieee488_2.transport
# ----------------------------------------------------------------------------
# Description : Transport layer (abstract, IP, file, dummy)
# Git repository : https://gitlab.com/qblox/packages/software/qblox_instruments.git
# Copyright (C) Qblox BV (2020)
# ----------------------------------------------------------------------------
# -- include -----------------------------------------------------------------
import asyncio
from abc import ABCMeta, abstractmethod
from collections.abc import Awaitable
from threading import Event, Thread
from typing import Optional, TypeVar
from qblox_instruments.ieee488_2.helpers import LoopError
# -- helpers ------------------------------------------------------------------
class TransportEventLoopThread(Thread):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.loop = None
self.loop_ready = Event()
def run(self) -> None:
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.loop_ready.set()
try:
self.loop.run_forever()
finally:
self.loop.close()
self.loop_ready.clear()
TRANSPORT_THREAD: Optional[TransportEventLoopThread] = None
def get_threaded_event_loop() -> asyncio.BaseEventLoop:
global TRANSPORT_THREAD # noqa: PLW0603, discourage globals
if TRANSPORT_THREAD is None:
TRANSPORT_THREAD = TransportEventLoopThread(daemon=True)
TRANSPORT_THREAD.start()
TRANSPORT_THREAD.loop_ready.wait()
return TRANSPORT_THREAD.loop
# -- class --------------------------------------------------------------------
T = TypeVar("T")
[docs]
class Transport(metaclass=ABCMeta):
"""
Abstract base class for data transport to instruments.
"""
[docs]
def __init__(
self, loop_from: Optional["Transport"] = None, use_running_loop: bool = False
) -> None:
"""
Create transport instance.
Parameters
----------
loop_from: Transport | None = None
Transport to use event loop from: bypasses `use_running_loop` checks.
use_running_loop : bool = False
Whether to use an existing event loop if it is already running;
use this only if you are supplying your own event loop and only invoking
asynchronous functions, or if the event loop is not running when invoking
synchronous command functions. Default to `False`.
"""
if loop_from is not None:
self._loop = loop_from._loop
self._loop_threaded = loop_from._loop_threaded
else:
self._loop_threaded = False
try:
self._loop = asyncio.get_running_loop()
except RuntimeError:
self._loop = asyncio.new_event_loop()
if self._loop.is_running() and not use_running_loop:
self._loop = get_threaded_event_loop()
self._loop_threaded = True
# ------------------------------------------------------------------------
def _run_in_loop(self, fn: Awaitable[T]) -> T:
# NOTE: We need to manually cancel the coroutine when it isn't run.
# In some cases, it might be destroyed by the destructor of the coroutine
# without having been awaited, which leads to RuntimeWarning being emitted
# and polluting the logs.
if self._loop_threaded:
return asyncio.run_coroutine_threadsafe(fn, self._loop).result()
elif self._loop.is_running():
fn.close() # Cancel the coroutine if it can't be run.
raise LoopError(f"An event loop is already running. Cannot run {fn}.")
else:
try:
return self._loop.run_until_complete(fn)
except RuntimeError as e:
fn.close() # Cancel the coroutine if it failed to run
if str(e) == "Event loop is closed":
# Raise a more specific error if the event loop is already closed,
# so it can be easily caught and suppressed by the caller.
raise LoopError(str(e)) from None
raise
# ------------------------------------------------------------------------
@property
def loop(self) -> asyncio.BaseEventLoop:
"""
Event loop for this transport.
"""
return self._loop
# ------------------------------------------------------------------------
@property
@abstractmethod
def timeout(self) -> float:
"""
Timeout for this transport.
"""
pass
# ------------------------------------------------------------------------
@timeout.setter
@abstractmethod
def timeout(self, value: float) -> None:
"""
Set timeout for this transport.
"""
pass
# ------------------------------------------------------------------------
[docs]
@abstractmethod
async def close(self) -> None:
"""
Abstract method to close instrument.
"""
pass
# ------------------------------------------------------------------------
[docs]
@abstractmethod
def reconnect(self) -> None:
"""
Close and re-open connection to instrument.
"""
# ------------------------------------------------------------------------
[docs]
@abstractmethod
async def write(self, cmd_str: str) -> None:
"""
Abstract method to write command to instrument.
Parameters
----------
cmd_str : str
Command
"""
pass
# ------------------------------------------------------------------------
[docs]
@abstractmethod
async def write_binary(self, *data: bytes) -> None:
"""
Abstract method to write binary data to instrument.
Parameters
----------
*data : bytes
Binary data
"""
pass
# ------------------------------------------------------------------------
[docs]
@abstractmethod
async def read_binary(self, size: int) -> bytes:
"""
Abstract method to read binary data from instrument.
Parameters
----------
size : int
Number of bytes
Returns
-------
bytes
Binary data array of length "size".
"""
pass
# ------------------------------------------------------------------------
[docs]
@abstractmethod
async def readline(self) -> str:
"""
Abstract method to read data from instrument.
Returns
-------
str
String with data.
"""
pass
# ------------------------------------------------------------------------
def __enter__(self) -> "Transport":
"""
Context manager entry.
Returns
-------
Any
Returns self
"""
return self._run_in_loop(self.__aenter__())
# ------------------------------------------------------------------------
def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
"""
Context manager exit. Closes the transport.
"""
return self._run_in_loop(self.__aexit__(exc_type, exc_value, traceback))
# ------------------------------------------------------------------------
async def __aenter__(self) -> "Transport":
"""
Asynchronous context manager entry.
Returns
-------
Any
Returns self
"""
return self
# ------------------------------------------------------------------------
async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
"""
Context manager exit. Closes the transport.
"""
await self.close()