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

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

## 125 statements

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

4 Communication protocol for VISA. Makes use of the pyvisa library.

5 The backend can be NI-Visa or pyvisa-py.

6

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

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

9

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

11"""

12

13 import logging

14 from time import sleep

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

16

17 import pyvisa as visa

18 from IPy import IP

19 from pyvisa.resources import MessageBasedResource

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

21

22 from .base import CommunicationProtocol

23 from ..configuration import configdataclass

24 from ..utils.enum import AutoNumberNameEnum

25

26

27 class VisaCommunicationError(Exception):

28 """

29 Base class for VisaCommunication errors.

30 """

31

32 pass

33

34

35@configdataclass

36 class 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

143 class 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?"