diff --git a/RCUSCC/RCUSCC/RCUSCC.py b/RCUSCC/RCUSCC/RCUSCC.py index 30bd2aabe9c0feeb0200ce907af1640e25062438..eec910de17bde6678de7e4a6dbef8da558f405de 100644 --- a/RCUSCC/RCUSCC/RCUSCC.py +++ b/RCUSCC/RCUSCC/RCUSCC.py @@ -25,7 +25,7 @@ import sys import opcua import numpy -from wrappers import only_when_on, fault_on_error +from wrappers import only_in_states, only_when_on, fault_on_error from opcua_connection import OPCUAConnection __all__ = ["RCUSCC", "main"] @@ -42,6 +42,25 @@ class RCUSCC(Device): - Type:'DevULong' OPC_Time_Out - Type:'DevDouble' + + States are as follows: + INIT = Device is initialising. + STANDBY = Device is initialised, but pends external configuration and an explicit turning on, + ON = Device is fully configured, functional, controls the hardware, and is possibly actively running, + FAULT = Device detected an unrecoverable error, and is thus malfunctional, + OFF = Device is turned off, drops connection to the hardware, + + The following state transitions are implemented: + boot -> OFF: Triggered by tango. Device will be instantiated, + OFF -> INIT: Triggered by device. Device will initialise (connect to hardware, other devices), + INIT -> STANDBY: Triggered by device. Device is initialised, and is ready for additional configuration by the user, + STANDBY -> ON: Triggered by user. Device reports to be functional, + * -> FAULT: Triggered by device. Device has degraded to malfunctional, for example because the connection to the hardware is lost, + * -> FAULT: Triggered by user. Emulate a forced malfunction for integration testing purposes, + * -> OFF: Triggered by user. Device is turned off. Triggered by the Off() command, + FAULT -> INIT: Triggered by user. Device is reinitialised to recover from an error, + + The user triggers their transitions by the commands reflecting the target state (Init(), On(), Fault()). """ client = 0 name_space_index = 0 @@ -207,11 +226,16 @@ class RCUSCC(Device): self.debug_stream("Mapping OPC-UA MP/CP to attributes done.") def init_device(self): - """Initialises the attributes and properties of the RCUSCC.""" + """ Instantiates the device in the OFF state. """ # NOTE: Will delete_device first, if necessary Device.init_device(self) + self.set_state(DevState.OFF) + + def initialise(self): + """Initialises the attributes and properties of the RCUSCC.""" + self.set_state(DevState.INIT) # Init the dict that contains attribute to OPC-UA MP/CP mappings. @@ -257,9 +281,10 @@ class RCUSCC(Device): self.client = opcua.Client("opc.tcp://{}:{}/".format(self.OPC_Server_Name, self.OPC_Server_Port), self.OPC_Time_Out) # timeout in seconds - # Connect to OPC-UA -- will set ON state on success - self.opcua_connection = OPCUAConnection(self.client, self.On, self.Fault, self) + # Connect to OPC-UA -- will set ON state on success in case of a reconnect + self.opcua_connection = OPCUAConnection(self.client, self.Standby, self.Fault, self) + # Explicitly connect if not self.opcua_connection.connect(): # hardware or infra is down -- needs fixing first self.Fault() @@ -276,16 +301,8 @@ class RCUSCC(Device): # Start keep-alive self.opcua_connection.start() - # Everything went ok -- go online - self.On() - - @DebugIt() - def On(self): - """ - - :return:None - """ - self.set_state(DevState.ON) + # Everything went ok -- go standby. + self.set_state(DevState.STANDBY) def always_executed_hook(self): @@ -432,23 +449,53 @@ class RCUSCC(Device): @command( ) + @only_in_states([DevState.FAULT, DevState.OFF]) @DebugIt() def Init(self): """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + + self.initialise() + + @only_in_states([DevState.INIT]) + def Standby(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. :return:None """ - self.init_device() + self.set_state(DevState.STANDBY) + + @command( + ) + @only_in_states([DevState.STANDBY]) + @DebugIt() + def On(self): + """ + Command to ask for initialisation of this device. Can only be called in FAULT or OFF state. + + :return:None + """ + + self.set_state(DevState.ON) @command( ) @DebugIt() def Off(self): """ + Command to ask for shutdown of this device. :return:None """ + if self.get_state() == DevState.OFF: + # Already off. Don't complain. + return + # Turn off self.set_state(DevState.OFF) @@ -458,15 +505,17 @@ class RCUSCC(Device): # Turn off again, in case of race conditions through reconnecting self.set_state(DevState.OFF) - @command( ) + @only_in_states([DevState.ON, DevState.INIT, DevState.STANDBY]) @DebugIt() def Fault(self): """ FAULT state is used to indicate our connection with the OPC-UA server is down. - This device will try to reconnect, and transition to the ON state on success. + This device will try to reconnect once, and transition to the ON state on success. + + If reconnecting fails, the user needs to call Init() to retry to restart this device. :return:None """ diff --git a/RCUSCC/RCUSCC/wrappers.py b/RCUSCC/RCUSCC/wrappers.py index 964fa46986edbd8eb8f1876deb8fe5c7c36bde56..dfd4fe3f444c0cbb572e53f7b0386b2882171e55 100644 --- a/RCUSCC/RCUSCC/wrappers.py +++ b/RCUSCC/RCUSCC/wrappers.py @@ -2,7 +2,23 @@ from tango import DevState from functools import wraps import traceback -__all__ = ["only_when_on", "fault_on_error"] +__all__ = ["only_in_states", "only_when_on", "fault_on_error"] + +def only_in_states(func, allowed_states): + """ + Wrapper to return None when the device isn't in ON state. + + If in ON state, calls & returns the wrapped function. + """ + + @wraps(func) + def state_check_wrapper(self, *args, **kwargs): + if self.get_state() not in allowed_states: + return None + + return func(self, *args, **kwargs) + + return state_check_wrapper def only_when_on(func): """