Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\comm\visa.py : 38%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (c) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET
2#
3"""
4Communication protocol for VISA. Makes use of the pyvisa library.
5The backend can be NI-Visa or pyvisa-py.
7Information on how to install a VISA backend can be found here:
8https://pyvisa.readthedocs.io/en/master/getting_nivisa.html
10So far only TCPIP SOCKET and TCPIP INSTR interfaces are supported.
11"""
13import logging
14from time import sleep
15from typing import Tuple, Union, Type, Optional, cast
17import pyvisa as visa
18from IPy import IP
19from pyvisa.resources import MessageBasedResource
20from pyvisa_py.protocols.rpc import RPCError # type: ignore
22from .base import CommunicationProtocol
23from ..configuration import configdataclass
24from ..utils.enum import AutoNumberNameEnum
27class VisaCommunicationError(Exception):
28 """
29 Base class for VisaCommunication errors.
30 """
32 pass
35@configdataclass
36class VisaCommunicationConfig:
37 """
38 `VisaCommunication` configuration dataclass.
39 """
41 class InterfaceType(AutoNumberNameEnum):
42 """
43 Supported VISA Interface types.
44 """
46 _init_ = "value _address_template"
48 #: VISA-RAW protocol
49 TCPIP_SOCKET = (), "TCPIP{board}::{host}::{port}::SOCKET"
51 #: VXI-11 protocol
52 TCPIP_INSTR = (), "TCPIP::{host}::INSTR"
54 def address(self, host: str, port: int = None, board: int = None) -> str:
55 """
56 Address string specific to the VISA interface type.
58 :param host: host IP address
59 :param port: optional TCP port
60 :param board: optional board number
61 :return: address string
62 """
64 return self._address_template.format(
65 board=board,
66 host=host,
67 port=port,
68 )
70 #: IP address of the VISA device. DNS names are currently unsupported.
71 host: str
73 #: Interface type of the VISA connection, being one of :class:`InterfaceType`.
74 interface_type: Union[str, InterfaceType]
76 #: Board number is typically 0 and comes from old bus systems.
77 board: int = 0
79 #: TCP port, standard is 5025.
80 port: int = 5025
82 #: Timeout for commands in milli seconds.
83 timeout: int = 5000
85 #: Chunk size is the allocated memory for read operations. The standard is 20kB,
86 #: and is increased per default here to 200kB. It is specified in bytes.
87 chunk_size: int = 204800
89 #: Timeout for opening the connection, in milli seconds.
90 open_timeout: int = 1000
92 #: Write termination character.
93 write_termination: str = "\n"
95 #: Read termination character.
96 read_termination: str = "\n"
98 visa_backend: str = ""
99 """
100 Specifies the path to the library to be used with PyVISA as a backend. Defaults
101 to None, which is NI-VISA (if installed), or pyvisa-py (if NI-VISA is not found).
102 To force the use of pyvisa-py, specify '@py' here.
103 """
105 def clean_values(self):
106 # in principle, host is allowed to be IP or FQDN. However, we only allow IP:
107 IP(self.host)
109 if not isinstance(self.interface_type, self.InterfaceType):
110 self.force_value("interface_type", self.InterfaceType(self.interface_type))
112 if self.board < 0:
113 raise ValueError("Board number has to be >= 0.")
115 if self.timeout < 0:
116 raise ValueError("Timeout has to be >= 0.")
118 if self.open_timeout < 0:
119 raise ValueError("Open Timeout has to be >= 0.")
121 allowed_terminators = ("\n", "\r", "\r\n")
122 if self.read_termination not in allowed_terminators:
123 raise ValueError("Read terminator has to be \\n, \\r or \\r\\n.")
125 if self.write_termination not in allowed_terminators:
126 raise ValueError("Write terminator has to be \\n, \\r or \\r\\n.")
128 @property
129 def address(self) -> str:
130 """
131 Address string depending on the VISA protocol's configuration.
133 :return: address string corresponding to current configuration
134 """
136 return self.interface_type.address( # type: ignore
137 self.host,
138 port=self.port,
139 board=self.board,
140 )
143class VisaCommunication(CommunicationProtocol):
144 """
145 Implements the Communication Protocol for VISA / SCPI.
146 """
148 #: The maximum of commands that can be sent in one round is 5 according to the
149 #: VISA standard.
150 MULTI_COMMANDS_MAX = 5
152 #: The character to separate two commands is ; according to the VISA standard.
153 MULTI_COMMANDS_SEPARATOR = ";"
155 #: Small pause in seconds to wait after write operations, allowing devices to
156 #: really do what we tell them before continuing with further tasks.
157 WAIT_AFTER_WRITE = 0.08 # seconds to wait after a write is sent
159 def __init__(self, configuration):
160 """
161 Constructor for VisaCommunication.
162 """
164 super().__init__(configuration)
166 # create a new resource manager
167 if self.config.visa_backend == "":
168 self._resource_manager = visa.ResourceManager()
169 else:
170 self._resource_manager = visa.ResourceManager(self.config.visa_backend)
172 self._instrument: Optional[MessageBasedResource] = None
174 @staticmethod
175 def config_cls() -> Type[VisaCommunicationConfig]:
176 return VisaCommunicationConfig
178 def open(self) -> None:
179 """
180 Open the VISA connection and create the resource.
181 """
183 logging.info("Open the VISA connection.")
185 try:
186 with self.access_lock:
187 self._instrument = cast(
188 MessageBasedResource,
189 self._resource_manager.open_resource(
190 self.config.address,
191 open_timeout=self.config.open_timeout,
192 ),
193 )
194 self._instrument.chunk_size = self.config.chunk_size
195 self._instrument.timeout = self.config.timeout
196 self._instrument.write_termination = self.config.write_termination
197 self._instrument.read_termination = self.config.read_termination
199 # enable keep-alive of the connection. Seems not to work always, but
200 # using the status poller a keepalive of the connection is also
201 # satisfied.
202 self._instrument.set_visa_attribute(
203 visa.constants.ResourceAttribute.tcpip_keepalive,
204 visa.constants.VI_TRUE,
205 )
206 except visa.VisaIOError as e:
208 logging.error(e)
209 if e.error_code != 0:
210 raise VisaCommunicationError from e
212 except (
213 RPCError,
214 ConnectionRefusedError,
215 BrokenPipeError,
216 ) as e:
217 # if pyvisa-py is used as backend, this RPCError can come. As it is
218 # difficult to import (hyphen in package name), we "convert" it here to a
219 # VisaCommunicationError. Apparently on the Linux runners,
220 # a ConnectionRefusedError is raised on fail, rather than an RPCError.
221 # On macOS the BrokenPipeError error is raised (from
222 # pyvisa-py/protocols/rpc.py:320), with puzzling log message from visa.py:
223 # "187 WARNING Could not close VISA connection, was not started."
225 logging.error(e)
226 raise VisaCommunicationError from e
228 def close(self) -> None:
229 """
230 Close the VISA connection and invalidates the handle.
231 """
233 if self._instrument is None:
234 logging.warning("Could not close VISA connection, was not started.")
235 return
237 try:
238 with self.access_lock:
239 self._instrument.close()
240 except visa.InvalidSession:
241 logging.warning("Could not close VISA connection, session invalid.")
243 def write(self, *commands: str) -> None:
244 """
245 Write commands. No answer is read or expected.
247 :param commands: one or more commands to send
248 :raises VisaCommunicationError: when connection was not started
249 """
251 if self._instrument is None:
252 msg = "Could not write to VISA connection, was not started."
253 logging.error(msg)
254 raise VisaCommunicationError(msg)
256 with self.access_lock:
257 self._instrument.write(self._generate_cmd_string(commands))
259 # sleep small amount of time to not overload device
260 sleep(self.WAIT_AFTER_WRITE)
262 def query(self, *commands: str) -> Union[str, Tuple[str, ...]]:
263 """
264 A combination of write(message) and read.
266 :param commands: list of commands
267 :return: list of values
268 :raises VisaCommunicationError: when connection was not started, or when trying
269 to issue too many commands at once.
270 """
272 if self._instrument is None:
273 msg = "Could not query VISA connection, was not started."
274 logging.error(msg)
275 raise VisaCommunicationError(msg)
277 cmd_string = self._generate_cmd_string(commands)
278 with self.access_lock:
279 return_string = self._instrument.query(cmd_string)
281 if len(commands) == 1:
282 return return_string
284 return tuple(return_string.split(self.MULTI_COMMANDS_SEPARATOR))
286 @classmethod
287 def _generate_cmd_string(cls, command_list: Tuple[str, ...]) -> str:
288 """
289 Generate the command string out of a tuple of strings.
291 :param command_list: is the tuple containing multiple commands
292 :return: the command string that can be sent via the protocol
293 """
295 if len(command_list) <= cls.MULTI_COMMANDS_MAX:
296 return cls.MULTI_COMMANDS_SEPARATOR.join(command_list)
298 raise VisaCommunicationError(
299 f"Too many commands at once ({len(command_list)}). Max allowed: "
300 f"{cls.MULTI_COMMANDS_MAX}."
301 )
303 def spoll(self) -> int:
304 """
305 Execute serial poll on the device. Reads the status byte register STB. This
306 is a fast function that can be executed periodically in a polling fashion.
308 :return: integer representation of the status byte
309 :raises VisaCommunicationError: when connection was not started
310 """
312 if self._instrument is None:
313 msg = "Could not query VISA connection, was not started."
314 logging.error(msg)
315 raise VisaCommunicationError(msg)
317 interface_type = self.config.interface_type
319 if interface_type == VisaCommunicationConfig.InterfaceType.TCPIP_INSTR:
320 with self.access_lock:
321 stb = self._instrument.read_stb()
322 return stb
324 if interface_type == VisaCommunicationConfig.InterfaceType.TCPIP_SOCKET:
325 return int(self.query("*STB?")) # type: ignore
327 assert False, "Forgot to cover interface_type case?"