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

4Base classes for the Supercube device. 

5""" 

6 

7import logging 

8from time import sleep 

9 

10from opcua import Node 

11 

12from hvl_ccb import configdataclass 

13from hvl_ccb.comm import OpcUaSubHandler, OpcUaCommunicationConfig, OpcUaCommunication 

14from . import constants 

15from ..base import SingleCommDevice 

16 

17 

18class InvalidSupercubeStatusError(Exception): 

19 """ 

20 Exception raised when supercube has invalid status. 

21 """ 

22 

23 pass 

24 

25 

26class SupercubeSubscriptionHandler(OpcUaSubHandler): 

27 """ 

28 OPC Subscription handler for datachange events and normal events specifically 

29 implemented for the Supercube devices. 

30 """ 

31 

32 def datachange_notification(self, node: Node, val, data): 

33 """ 

34 In addition to the standard operation (debug logging entry of the datachange), 

35 alarms are logged at INFO level using the alarm text. 

36 

37 :param node: the node object that triggered the datachange event 

38 :param val: the new value 

39 :param data: 

40 """ 

41 

42 super().datachange_notification(node, val, data) 

43 

44 # assume an alarm datachange 

45 if node.nodeid.Identifier == constants.Errors.stop_number: 

46 alarm_text = constants.AlarmText.get(val) 

47 logging.getLogger(__name__).info(alarm_text) 

48 

49 

50@configdataclass 

51class SupercubeConfiguration: 

52 """ 

53 Configuration dataclass for the Supercube devices. 

54 """ 

55 

56 #: Namespace of the OPC variables, typically this is 3 (coming from Siemens) 

57 namespace_index: int = 7 

58 

59 

60@configdataclass 

61class SupercubeOpcUaCommunicationConfig(OpcUaCommunicationConfig): 

62 """ 

63 Communication protocol configuration for OPC UA, specifications for the Supercube 

64 devices. 

65 """ 

66 

67 #: Subscription handler for data change events 

68 sub_handler: OpcUaSubHandler = SupercubeSubscriptionHandler() 

69 

70 port: int = 4845 

71 

72 

73class SupercubeOpcUaCommunication(OpcUaCommunication): 

74 """ 

75 Communication protocol specification for Supercube devices. 

76 """ 

77 

78 @staticmethod 

79 def config_cls(): 

80 return SupercubeOpcUaCommunicationConfig 

81 

82 

83class Supercube2015Base(SingleCommDevice): 

84 """ 

85 Base class for Supercube variants. 

86 """ 

87 

88 def __init__(self, com, dev_config=None): 

89 """ 

90 Constructor for Supercube base class. 

91 

92 :param com: the communication protocol or its configuration 

93 :param dev_config: the device configuration 

94 """ 

95 

96 super().__init__(com, dev_config) 

97 

98 self.logger = logging.getLogger(__name__) 

99 

100 @staticmethod 

101 def default_com_cls(): 

102 return SupercubeOpcUaCommunication 

103 

104 def start(self) -> None: 

105 """ 

106 Starts the device. Sets the root node for all OPC read and write commands to 

107 the Siemens PLC object node which holds all our relevant objects and variables. 

108 """ 

109 

110 self.logger.info("Starting Supercube Base device") 

111 super().start() 

112 

113 self.logger.debug("Add monitoring nodes") 

114 self.com.init_monitored_nodes( 

115 map( # type: ignore 

116 str, constants.GeneralSockets 

117 ), 

118 self.config.namespace_index, 

119 ) 

120 self.com.init_monitored_nodes( 

121 map( # type: ignore 

122 str, constants.GeneralSupport 

123 ), 

124 self.config.namespace_index, 

125 ) 

126 self.com.init_monitored_nodes( 

127 map( # type: ignore 

128 str, constants.Safety 

129 ), 

130 self.config.namespace_index, 

131 ) 

132 

133 self.com.init_monitored_nodes( 

134 str(constants.Errors.stop_number), self.config.namespace_index 

135 ) 

136 

137 self.com.init_monitored_nodes( 

138 map( # type: ignore 

139 str, constants.EarthingStick 

140 ), 

141 self.config.namespace_index, 

142 ) 

143 

144 self.logger.debug("Finished starting") 

145 

146 def stop(self) -> None: 

147 """ 

148 Stop the Supercube device. Deactivates the remote control and closes the 

149 communication protocol. 

150 """ 

151 

152 super().stop() 

153 

154 @staticmethod 

155 def config_cls(): 

156 return SupercubeConfiguration 

157 

158 def read(self, node_id: str): 

159 """ 

160 Local wrapper for the OPC UA communication protocol read method. 

161 

162 :param node_id: the id of the node to read. 

163 :return: the value of the variable 

164 """ 

165 

166 result = self.com.read(str(node_id), self.config.namespace_index) 

167 self.logger.debug(f"Read from node ID {node_id}: {result}") 

168 return result 

169 

170 def write(self, node_id, value) -> None: 

171 """ 

172 Local wrapper for the OPC UA communication protocol write method. 

173 

174 :param node_id: the id of the node to read 

175 :param value: the value to write to the variable 

176 """ 

177 

178 self.logger.debug(f"Write to node ID {node_id}: {value}") 

179 self.com.write(str(node_id), self.config.namespace_index, value) 

180 

181 def set_remote_control(self, state: bool) -> None: 

182 """ 

183 Enable or disable remote control for the Supercube. This will effectively 

184 display a message on the touchscreen HMI. 

185 

186 :param state: desired remote control state 

187 """ 

188 

189 raise NotImplementedError("Function does not exist in Supercube 2015.") 

190 

191 def get_support_input(self, port: int, contact: int) -> bool: 

192 """ 

193 Get the state of a support socket input. 

194 

195 :param port: is the socket number (1..6) 

196 :param contact: is the contact on the socket (1..2) 

197 :return: digital input read state 

198 """ 

199 

200 return bool(self.read(constants.GeneralSupport.input(port, contact))) 

201 

202 def get_support_output(self, port: int, contact: int) -> bool: 

203 """ 

204 Get the state of a support socket output. 

205 

206 :param port: is the socket number (1..6) 

207 :param contact: is the contact on the socket (1..2) 

208 :return: digital output read state 

209 """ 

210 

211 return bool(self.read(constants.GeneralSupport.output(port, contact))) 

212 

213 def set_support_output(self, port: int, contact: int, state: bool) -> None: 

214 """ 

215 Set the state of a support output socket. 

216 

217 :param port: is the socket number (1..6) 

218 :param contact: is the contact on the socket (1..2) 

219 :param state: is the desired state of the support output 

220 """ 

221 

222 self.write(constants.GeneralSupport.output(port, contact), bool(state)) 

223 

224 def set_support_output_impulse( 

225 self, port: int, contact: int, duration: float = 0.2, pos_pulse: bool = True 

226 ) -> None: 

227 """ 

228 Issue an impulse of a certain duration on a support output contact. The polarity 

229 of the pulse (On-wait-Off or Off-wait-On) is specified by the pos_pulse 

230 argument. 

231 

232 This function is blocking. 

233 

234 :param port: is the socket number (1..6) 

235 :param contact: is the contact on the socket (1..2) 

236 :param duration: is the length of the impulse in seconds 

237 :param pos_pulse: is True, if the pulse shall be HIGH, False if it shall be LOW 

238 """ 

239 

240 self.set_support_output(port, contact, pos_pulse) 

241 sleep(duration) 

242 self.set_support_output(port, contact, not pos_pulse) 

243 

244 def get_t13_socket(self, port: int) -> bool: 

245 """ 

246 Read the state of a SEV T13 power socket. 

247 

248 :param port: is the socket number, one of `constants.T13_SOCKET_PORTS` 

249 :return: on-state of the power socket 

250 """ 

251 

252 if port not in constants.T13_SOCKET_PORTS: 

253 raise ValueError(f"port not in {constants.T13_SOCKET_PORTS}: {port}") 

254 

255 return bool(self.read(getattr(constants.GeneralSockets, f"t13_{port}"))) 

256 

257 def set_t13_socket(self, port: int, state: bool) -> None: 

258 """ 

259 Set the state of a SEV T13 power socket. 

260 

261 :param port: is the socket number, one of `constants.T13_SOCKET_PORTS` 

262 :param state: is the desired on-state of the socket 

263 """ 

264 

265 if not isinstance(state, bool): 

266 raise ValueError(f"state is not <bool>: {state}") 

267 

268 if port not in constants.T13_SOCKET_PORTS: 

269 raise ValueError(f"port not in {constants.T13_SOCKET_PORTS}: {port}") 

270 

271 self.write(getattr(constants.GeneralSockets, f"t13_{port}"), state) 

272 

273 def get_cee16_socket(self) -> bool: 

274 """ 

275 Read the on-state of the IEC CEE16 three-phase power socket. 

276 

277 :return: the on-state of the CEE16 power socket 

278 """ 

279 

280 return bool(self.read(constants.GeneralSockets.cee16)) 

281 

282 def set_cee16_socket(self, state: bool) -> None: 

283 """ 

284 Switch the IEC CEE16 three-phase power socket on or off. 

285 

286 :param state: desired on-state of the power socket 

287 :raises ValueError: if state is not of type bool 

288 """ 

289 

290 if not isinstance(state, bool): 

291 raise ValueError(f"state is not <bool>: {state}") 

292 

293 self.write(constants.GeneralSockets.cee16, state) 

294 

295 def get_status(self) -> int: 

296 """ 

297 Get the safety circuit status of the Supercube. 

298 

299 :return: the safety status of the supercube's state machine; 

300 see `constants.SafetyStatus`. 

301 """ 

302 

303 ready_for_red = self.read(constants.Safety.status_ready_for_red) 

304 red = self.read(constants.Safety.status_red) 

305 green = self.read(constants.Safety.status_green) 

306 operate = self.read(constants.Safety.switchto_operate) 

307 error = self.read(constants.Safety.status_error) 

308 triggered = self.read(constants.BreakdownDetection.triggered) 

309 fso_active = self.read(constants.BreakdownDetection.activated) 

310 

311 if error: 

312 return constants.SafetyStatus.Error 

313 

314 if triggered and fso_active: 

315 return constants.SafetyStatus.QuickStop 

316 

317 if not ready_for_red and not red and green: 

318 return constants.SafetyStatus.GreenNotReady 

319 

320 if ready_for_red and not red and not operate: 

321 return constants.SafetyStatus.GreenReady 

322 

323 if red and not green and not operate: 

324 return constants.SafetyStatus.RedReady 

325 

326 if red and not green and operate: 

327 return constants.SafetyStatus.RedOperate 

328 

329 raise InvalidSupercubeStatusError( 

330 f"ready_for_red: {ready_for_red}, red: {red}, green: {green}, " 

331 f"operate: {operate}, triggered: {triggered}, fso_active: {fso_active}" 

332 ) 

333 

334 def ready(self, state: bool) -> None: 

335 """ 

336 Set ready state. Ready means locket safety circuit, red lamps, but high voltage 

337 still off. 

338 

339 :param state: set ready state 

340 """ 

341 

342 if state: 

343 if self.get_status() is constants.SafetyStatus.GreenReady: 

344 self.write(constants.Safety.switchto_ready, True) 

345 sleep(0.02) 

346 self.write(constants.Safety.switchto_ready, False) 

347 else: 

348 self.write(constants.Safety.switchto_green, True) 

349 sleep(0.02) 

350 self.write(constants.Safety.switchto_green, False) 

351 

352 def operate(self, state: bool) -> None: 

353 """ 

354 Set operate state. If the state is RedReady, this will turn on the high 

355 voltage and close the safety switches. 

356 

357 :param state: set operate state 

358 """ 

359 

360 if state: 

361 if self.get_status() is constants.SafetyStatus.RedReady: 

362 self.write(constants.Safety.switchto_operate, True) 

363 else: 

364 self.write(constants.Safety.switchto_operate, False) 

365 

366 def get_measurement_ratio(self, channel: int) -> float: 

367 """ 

368 Get the set measurement ratio of an AC/DC analog input channel. Every input 

369 channel has a divider ratio assigned during setup of the Supercube system. 

370 This ratio can be read out. 

371 

372 **Attention:** Supercube 2015 does not have a separate ratio for every analog 

373 input. Therefore there is only one ratio for ``channel = 1``. 

374 

375 :param channel: number of the input channel (1..4) 

376 :return: the ratio 

377 """ 

378 

379 return float(self.read(constants.MeasurementsDividerRatio.get(channel))) 

380 

381 def get_measurement_voltage(self, channel: int) -> float: 

382 """ 

383 Get the measured voltage of an analog input channel. The voltage read out 

384 here is already scaled by the configured divider ratio. 

385 

386 **Attention:** In contrast to the *new* Supercube, the old one returns here 

387 the input voltage read at the ADC. It is not scaled by a factor. 

388 

389 :param channel: number of the input channel (1..4) 

390 :return: measured voltage 

391 """ 

392 

393 return float(self.read(constants.MeasurementsScaledInput.get(channel))) 

394 

395 def get_earthing_status(self, number: int) -> int: 

396 """ 

397 Get the status of an earthing stick, whether it is closed, open or undefined 

398 (moving). 

399 

400 :param number: number of the earthing stick (1..6) 

401 :return: earthing stick status; see constants.EarthingStickStatus 

402 """ 

403 

404 connected = self.read(constants.EarthingStick.status_connected(number)) 

405 open_ = self.read(constants.EarthingStick.status_open(number)) 

406 closed = self.read(constants.EarthingStick.status_closed(number)) 

407 

408 if not connected: 

409 return constants.EarthingStickStatus.inactive 

410 

411 if open_ and not closed: 

412 return constants.EarthingStickStatus.open 

413 

414 if not open_ and closed: 

415 return constants.EarthingStickStatus.closed 

416 

417 return constants.EarthingStickStatus.error 

418 

419 def get_earthing_manual(self, number: int) -> bool: 

420 """ 

421 Get the manual status of an earthing stick. If an earthing stick is set to 

422 manual, it is closed even if the system is in states RedReady or RedOperate. 

423 

424 :param number: number of the earthing stick (1..6) 

425 :return: earthing stick manual status 

426 """ 

427 

428 return bool(self.read(constants.EarthingStick.manual(number))) 

429 

430 def set_earthing_manual(self, number: int, manual: bool) -> None: 

431 """ 

432 Set the manual status of an earthing stick. If an earthing stick is set to 

433 manual, it is closed even if the system is in states RedReady or RedOperate. 

434 

435 :param number: number of the earthing stick (1..6) 

436 :param manual: earthing stick manual status (True or False) 

437 """ 

438 

439 if self.get_status() is constants.SafetyStatus.RedOperate: 

440 raise RuntimeError("Status is Red Operate, should not move earthing.") 

441 self.write(constants.EarthingStick.manual(number), manual) 

442 

443 def quit_error(self) -> None: 

444 """ 

445 Quits errors that are active on the Supercube. 

446 """ 

447 

448 self.write(constants.Errors.quit, True) 

449 sleep(0.1) 

450 self.write(constants.Errors.quit, False) 

451 

452 def horn(self, state: bool) -> None: 

453 """ 

454 Turns acoustic horn on or off. 

455 

456 :param state: Turns horn on (True) or off (False) 

457 """ 

458 self.write(constants.Safety.horn, state) 

459 

460 def get_door_status(self, door: int) -> constants.DoorStatus: 

461 """ 

462 Get the status of a safety fence door. See :class:`constants.DoorStatus` for 

463 possible returned door statuses. 

464 

465 :param door: the door number (1..3) 

466 :return: the door status 

467 """ 

468 

469 raise NotImplementedError( 

470 "Door status not supported in old Supercube 2015 version." 

471 )