Hide keyboard shortcuts

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. 

6 

7Information on how to install a VISA backend can be found here: 

8https://pyvisa.readthedocs.io/en/master/getting_nivisa.html 

9 

10So far only TCPIP SOCKET and TCPIP INSTR interfaces are supported. 

11""" 

12 

13import logging 

14from time import sleep 

15from typing import Tuple, Union, Type, Optional, cast 

16 

17import pyvisa as visa 

18from IPy import IP 

19from pyvisa.resources import MessageBasedResource 

20from pyvisa_py.protocols.rpc import RPCError # type: ignore 

21 

22from .base import CommunicationProtocol 

23from ..configuration import configdataclass 

24from ..utils.enum import AutoNumberNameEnum 

25 

26 

27class VisaCommunicationError(Exception): 

28 """ 

29 Base class for VisaCommunication errors. 

30 """ 

31 

32 pass 

33 

34 

35@configdataclass 

36class VisaCommunicationConfig: 

37 """ 

38 `VisaCommunication` configuration dataclass. 

39 """ 

40 

41 class InterfaceType(AutoNumberNameEnum): 

42 """ 

43 Supported VISA Interface types. 

44 """ 

45 

46 _init_ = "value _address_template" 

47 

48 #: VISA-RAW protocol 

49 TCPIP_SOCKET = (), "TCPIP{board}::{host}::{port}::SOCKET" 

50 

51 #: VXI-11 protocol 

52 TCPIP_INSTR = (), "TCPIP::{host}::INSTR" 

53 

54 def address(self, host: str, port: int = None, board: int = None) -> str: 

55 """ 

56 Address string specific to the VISA interface type. 

57 

58 :param host: host IP address 

59 :param port: optional TCP port 

60 :param board: optional board number 

61 :return: address string 

62 """ 

63 

64 return self._address_template.format( 

65 board=board, 

66 host=host, 

67 port=port, 

68 ) 

69 

70 #: IP address of the VISA device. DNS names are currently unsupported. 

71 host: str 

72 

73 #: Interface type of the VISA connection, being one of :class:`InterfaceType`. 

74 interface_type: Union[str, InterfaceType] 

75 

76 #: Board number is typically 0 and comes from old bus systems. 

77 board: int = 0 

78 

79 #: TCP port, standard is 5025. 

80 port: int = 5025 

81 

82 #: Timeout for commands in milli seconds. 

83 timeout: int = 5000 

84 

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 

88 

89 #: Timeout for opening the connection, in milli seconds. 

90 open_timeout: int = 1000 

91 

92 #: Write termination character. 

93 write_termination: str = "\n" 

94 

95 #: Read termination character. 

96 read_termination: str = "\n" 

97 

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 """ 

104 

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) 

108 

109 if not isinstance(self.interface_type, self.InterfaceType): 

110 self.force_value("interface_type", self.InterfaceType(self.interface_type)) 

111 

112 if self.board < 0: 

113 raise ValueError("Board number has to be >= 0.") 

114 

115 if self.timeout < 0: 

116 raise ValueError("Timeout has to be >= 0.") 

117 

118 if self.open_timeout < 0: 

119 raise ValueError("Open Timeout has to be >= 0.") 

120 

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.") 

124 

125 if self.write_termination not in allowed_terminators: 

126 raise ValueError("Write terminator has to be \\n, \\r or \\r\\n.") 

127 

128 @property 

129 def address(self) -> str: 

130 """ 

131 Address string depending on the VISA protocol's configuration. 

132 

133 :return: address string corresponding to current configuration 

134 """ 

135 

136 return self.interface_type.address( # type: ignore 

137 self.host, 

138 port=self.port, 

139 board=self.board, 

140 ) 

141 

142 

143class VisaCommunication(CommunicationProtocol): 

144 """ 

145 Implements the Communication Protocol for VISA / SCPI. 

146 """ 

147 

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 

151 

152 #: The character to separate two commands is ; according to the VISA standard. 

153 MULTI_COMMANDS_SEPARATOR = ";" 

154 

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 

158 

159 def __init__(self, configuration): 

160 """ 

161 Constructor for VisaCommunication. 

162 """ 

163 

164 super().__init__(configuration) 

165 

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) 

171 

172 self._instrument: Optional[MessageBasedResource] = None 

173 

174 @staticmethod 

175 def config_cls() -> Type[VisaCommunicationConfig]: 

176 return VisaCommunicationConfig 

177 

178 def open(self) -> None: 

179 """ 

180 Open the VISA connection and create the resource. 

181 """ 

182 

183 logging.info("Open the VISA connection.") 

184 

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 

198 

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: 

207 

208 logging.error(e) 

209 if e.error_code != 0: 

210 raise VisaCommunicationError from e 

211 

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." 

224 

225 logging.error(e) 

226 raise VisaCommunicationError from e 

227 

228 def close(self) -> None: 

229 """ 

230 Close the VISA connection and invalidates the handle. 

231 """ 

232 

233 if self._instrument is None: 

234 logging.warning("Could not close VISA connection, was not started.") 

235 return 

236 

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.") 

242 

243 def write(self, *commands: str) -> None: 

244 """ 

245 Write commands. No answer is read or expected. 

246 

247 :param commands: one or more commands to send 

248 :raises VisaCommunicationError: when connection was not started 

249 """ 

250 

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) 

255 

256 with self.access_lock: 

257 self._instrument.write(self._generate_cmd_string(commands)) 

258 

259 # sleep small amount of time to not overload device 

260 sleep(self.WAIT_AFTER_WRITE) 

261 

262 def query(self, *commands: str) -> Union[str, Tuple[str, ...]]: 

263 """ 

264 A combination of write(message) and read. 

265 

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 """ 

271 

272 if self._instrument is None: 

273 msg = "Could not query VISA connection, was not started." 

274 logging.error(msg) 

275 raise VisaCommunicationError(msg) 

276 

277 cmd_string = self._generate_cmd_string(commands) 

278 with self.access_lock: 

279 return_string = self._instrument.query(cmd_string) 

280 

281 if len(commands) == 1: 

282 return return_string 

283 

284 return tuple(return_string.split(self.MULTI_COMMANDS_SEPARATOR)) 

285 

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. 

290 

291 :param command_list: is the tuple containing multiple commands 

292 :return: the command string that can be sent via the protocol 

293 """ 

294 

295 if len(command_list) <= cls.MULTI_COMMANDS_MAX: 

296 return cls.MULTI_COMMANDS_SEPARATOR.join(command_list) 

297 

298 raise VisaCommunicationError( 

299 f"Too many commands at once ({len(command_list)}). Max allowed: " 

300 f"{cls.MULTI_COMMANDS_MAX}." 

301 ) 

302 

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. 

307 

308 :return: integer representation of the status byte 

309 :raises VisaCommunicationError: when connection was not started 

310 """ 

311 

312 if self._instrument is None: 

313 msg = "Could not query VISA connection, was not started." 

314 logging.error(msg) 

315 raise VisaCommunicationError(msg) 

316 

317 interface_type = self.config.interface_type 

318 

319 if interface_type == VisaCommunicationConfig.InterfaceType.TCPIP_INSTR: 

320 with self.access_lock: 

321 stb = self._instrument.read_stb() 

322 return stb 

323 

324 if interface_type == VisaCommunicationConfig.InterfaceType.TCPIP_SOCKET: 

325 return int(self.query("*STB?")) # type: ignore 

326 

327 assert False, "Forgot to cover interface_type case?"