Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\supercube2015\base.py : 0%

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"""
7import logging
8from time import sleep
10from opcua import Node
12from hvl_ccb import configdataclass
13from hvl_ccb.comm import OpcUaSubHandler, OpcUaCommunicationConfig, OpcUaCommunication
14from . import constants
15from ..base import SingleCommDevice
18class InvalidSupercubeStatusError(Exception):
19 """
20 Exception raised when supercube has invalid status.
21 """
23 pass
26class SupercubeSubscriptionHandler(OpcUaSubHandler):
27 """
28 OPC Subscription handler for datachange events and normal events specifically
29 implemented for the Supercube devices.
30 """
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.
37 :param node: the node object that triggered the datachange event
38 :param val: the new value
39 :param data:
40 """
42 super().datachange_notification(node, val, data)
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)
50@configdataclass
51class SupercubeConfiguration:
52 """
53 Configuration dataclass for the Supercube devices.
54 """
56 #: Namespace of the OPC variables, typically this is 3 (coming from Siemens)
57 namespace_index: int = 7
60@configdataclass
61class SupercubeOpcUaCommunicationConfig(OpcUaCommunicationConfig):
62 """
63 Communication protocol configuration for OPC UA, specifications for the Supercube
64 devices.
65 """
67 #: Subscription handler for data change events
68 sub_handler: OpcUaSubHandler = SupercubeSubscriptionHandler()
70 port: int = 4845
73class SupercubeOpcUaCommunication(OpcUaCommunication):
74 """
75 Communication protocol specification for Supercube devices.
76 """
78 @staticmethod
79 def config_cls():
80 return SupercubeOpcUaCommunicationConfig
83class Supercube2015Base(SingleCommDevice):
84 """
85 Base class for Supercube variants.
86 """
88 def __init__(self, com, dev_config=None):
89 """
90 Constructor for Supercube base class.
92 :param com: the communication protocol or its configuration
93 :param dev_config: the device configuration
94 """
96 super().__init__(com, dev_config)
98 self.logger = logging.getLogger(__name__)
100 @staticmethod
101 def default_com_cls():
102 return SupercubeOpcUaCommunication
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 """
110 self.logger.info("Starting Supercube Base device")
111 super().start()
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 )
133 self.com.init_monitored_nodes(
134 str(constants.Errors.stop_number), self.config.namespace_index
135 )
137 self.com.init_monitored_nodes(
138 map( # type: ignore
139 str, constants.EarthingStick
140 ),
141 self.config.namespace_index,
142 )
144 self.logger.debug("Finished starting")
146 def stop(self) -> None:
147 """
148 Stop the Supercube device. Deactivates the remote control and closes the
149 communication protocol.
150 """
152 super().stop()
154 @staticmethod
155 def config_cls():
156 return SupercubeConfiguration
158 def read(self, node_id: str):
159 """
160 Local wrapper for the OPC UA communication protocol read method.
162 :param node_id: the id of the node to read.
163 :return: the value of the variable
164 """
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
170 def write(self, node_id, value) -> None:
171 """
172 Local wrapper for the OPC UA communication protocol write method.
174 :param node_id: the id of the node to read
175 :param value: the value to write to the variable
176 """
178 self.logger.debug(f"Write to node ID {node_id}: {value}")
179 self.com.write(str(node_id), self.config.namespace_index, value)
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.
186 :param state: desired remote control state
187 """
189 raise NotImplementedError("Function does not exist in Supercube 2015.")
191 def get_support_input(self, port: int, contact: int) -> bool:
192 """
193 Get the state of a support socket input.
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 """
200 return bool(self.read(constants.GeneralSupport.input(port, contact)))
202 def get_support_output(self, port: int, contact: int) -> bool:
203 """
204 Get the state of a support socket output.
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 """
211 return bool(self.read(constants.GeneralSupport.output(port, contact)))
213 def set_support_output(self, port: int, contact: int, state: bool) -> None:
214 """
215 Set the state of a support output socket.
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 """
222 self.write(constants.GeneralSupport.output(port, contact), bool(state))
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.
232 This function is blocking.
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 """
240 self.set_support_output(port, contact, pos_pulse)
241 sleep(duration)
242 self.set_support_output(port, contact, not pos_pulse)
244 def get_t13_socket(self, port: int) -> bool:
245 """
246 Read the state of a SEV T13 power socket.
248 :param port: is the socket number, one of `constants.T13_SOCKET_PORTS`
249 :return: on-state of the power socket
250 """
252 if port not in constants.T13_SOCKET_PORTS:
253 raise ValueError(f"port not in {constants.T13_SOCKET_PORTS}: {port}")
255 return bool(self.read(getattr(constants.GeneralSockets, f"t13_{port}")))
257 def set_t13_socket(self, port: int, state: bool) -> None:
258 """
259 Set the state of a SEV T13 power socket.
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 """
265 if not isinstance(state, bool):
266 raise ValueError(f"state is not <bool>: {state}")
268 if port not in constants.T13_SOCKET_PORTS:
269 raise ValueError(f"port not in {constants.T13_SOCKET_PORTS}: {port}")
271 self.write(getattr(constants.GeneralSockets, f"t13_{port}"), state)
273 def get_cee16_socket(self) -> bool:
274 """
275 Read the on-state of the IEC CEE16 three-phase power socket.
277 :return: the on-state of the CEE16 power socket
278 """
280 return bool(self.read(constants.GeneralSockets.cee16))
282 def set_cee16_socket(self, state: bool) -> None:
283 """
284 Switch the IEC CEE16 three-phase power socket on or off.
286 :param state: desired on-state of the power socket
287 :raises ValueError: if state is not of type bool
288 """
290 if not isinstance(state, bool):
291 raise ValueError(f"state is not <bool>: {state}")
293 self.write(constants.GeneralSockets.cee16, state)
295 def get_status(self) -> int:
296 """
297 Get the safety circuit status of the Supercube.
299 :return: the safety status of the supercube's state machine;
300 see `constants.SafetyStatus`.
301 """
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)
311 if error:
312 return constants.SafetyStatus.Error
314 if triggered and fso_active:
315 return constants.SafetyStatus.QuickStop
317 if not ready_for_red and not red and green:
318 return constants.SafetyStatus.GreenNotReady
320 if ready_for_red and not red and not operate:
321 return constants.SafetyStatus.GreenReady
323 if red and not green and not operate:
324 return constants.SafetyStatus.RedReady
326 if red and not green and operate:
327 return constants.SafetyStatus.RedOperate
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 )
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.
339 :param state: set ready state
340 """
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)
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.
357 :param state: set operate state
358 """
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)
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.
372 **Attention:** Supercube 2015 does not have a separate ratio for every analog
373 input. Therefore there is only one ratio for ``channel = 1``.
375 :param channel: number of the input channel (1..4)
376 :return: the ratio
377 """
379 return float(self.read(constants.MeasurementsDividerRatio.get(channel)))
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.
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.
389 :param channel: number of the input channel (1..4)
390 :return: measured voltage
391 """
393 return float(self.read(constants.MeasurementsScaledInput.get(channel)))
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).
400 :param number: number of the earthing stick (1..6)
401 :return: earthing stick status; see constants.EarthingStickStatus
402 """
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))
408 if not connected:
409 return constants.EarthingStickStatus.inactive
411 if open_ and not closed:
412 return constants.EarthingStickStatus.open
414 if not open_ and closed:
415 return constants.EarthingStickStatus.closed
417 return constants.EarthingStickStatus.error
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.
424 :param number: number of the earthing stick (1..6)
425 :return: earthing stick manual status
426 """
428 return bool(self.read(constants.EarthingStick.manual(number)))
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.
435 :param number: number of the earthing stick (1..6)
436 :param manual: earthing stick manual status (True or False)
437 """
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)
443 def quit_error(self) -> None:
444 """
445 Quits errors that are active on the Supercube.
446 """
448 self.write(constants.Errors.quit, True)
449 sleep(0.1)
450 self.write(constants.Errors.quit, False)
452 def horn(self, state: bool) -> None:
453 """
454 Turns acoustic horn on or off.
456 :param state: Turns horn on (True) or off (False)
457 """
458 self.write(constants.Safety.horn, state)
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.
465 :param door: the door number (1..3)
466 :return: the door status
467 """
469 raise NotImplementedError(
470 "Door status not supported in old Supercube 2015 version."
471 )