diff --git a/asyncua/common/parameter_set.py b/asyncua/common/parameter_set.py new file mode 100644 index 000000000..54e5fafba --- /dev/null +++ b/asyncua/common/parameter_set.py @@ -0,0 +1,153 @@ +from asyncore import dispatcher +from asyncua import Node, Server, ua +from asyncua.common.callback import CallbackType, CallbackService, ServerItemCallback + +class ParameterSet: + """ + Parmeter set for easy access of parameters + + A parameter set is an object only containing variables. + This class can be used for e.g. devices or state machines. + + @param node: the node to the object representing the parameter set + @param subscribe: subscribe to value changes of the parameters + @param notifier: method to be called when a value in the parameter set changes + @param source: server or client object as source of the information + """ + def __init__(self, node : Node, subscribe=False, notifier=None, source=None, interval=100): + self._parameters = {} + self._node = node + self._source = source + self.name = '' + self._notify_data_change = notifier + self._subscribe_data_change = subscribe + self._parameter_nodes = [] + self._subscribe = subscribe + self._parameter_ids = [] + self._subscription_interval = interval + self._subscription = None + + async def init(self): + # Get the ParameterSet name + bn = await self._node.read_browse_name() + self.name = bn.Name + + # Browse ParameterSet object + parameters = await self._node.get_children(refs=33) + for p in parameters: + bn = await p.read_browse_name() + val = await p.read_value() + # Add the parameter to parameter dictionary + self._parameters[bn.Name] = {'Name': bn.Name, 'Default': val, 'Node': p, 'Value': val} # TODO: add unit and range information + self._parameter_nodes.append(p) + self._parameter_ids.append(p.nodeid) + setattr(p, 'value', val) + # Add parameter node as an class attribute + setattr(self, bn.Name, p) + + if self._subscribe_data_change and self._source: + self._subscription = await self._source.create_subscription(self._subscription_interval, self) + self._state_change_subscription = await self._subscription.subscribe_data_change(self._parameter_nodes) + + return self._parameters + + async def datachange_notification(self, node, val, data): + for p in self._parameters: + if self._parameters[p]['Node'] == node: + self._parameters[p]['Node'].value = val + self._parameters[p]['Value'] = val + + if self._notify_data_change: + await self._notify_data_change(node, val) + + async def update_subscription_interval(self, interval): + if self._subscription: + p = ua.ModifySubscriptionParameters() + p.RequestedPublishingInterval = interval + p.SubscriptionId = self._subscription.subscription_id + self._subscription_interval = interval + await self._subscription.update(p) + + def get_parameter_list(self): + return self._parameter_nodes + + def get_parameter_node(self, name): + return self._parameters[name]['Node'] + + async def get_parameter_dict(self): + """ + Return the parameter dict with current values + """ + for p in self._parameters: + node = self._parameters[p]['Node'] + self._parameters[p]['Value'] = await node.read_value() + return self._parameters + + async def read_value(self, name: str): + """ + Read the value for the parameter with the given name + + @param name: name of the parameters + """ + return await self._parameters[name]['Node'].read_value() + + def get_value(self, name): + """ + Read the value for the parameter with the given name. + + @param name: name of the parameters + """ + return self._parameters[name]['Value'] + + async def set_value(self, name, val, varianttype=None): + """ + Write the value for the parameter with the given name + + @param val: value of the parameter + @param varianttype: type of the parameter + """ + if not varianttype: + self._parameters[name]['Node'].value = val + else: + self._parameters[name]['Node'].value = ua.Variant(val, varianttype) + + await self._parameters[name]['Node'].write_value(val, varianttype) + + async def set_default_value(self, name, val=None): + """ + Set the default value to a parameter + + @param name: name of the parameter + @param val: if this is given default and current value is set to it + """ + if val: + self._parameters[name]['Devault'] = val + await self._parameters[name]['Node'].write_value(val) + else: + val = self._parameters[name]['Default'] + await self._parameters[name]['Node'].write_value(val) + + async def print_parameter_list(self): + """ + Print the parameter list + """ + s = '\n#####################################################\n' + s += '|{:^51}|'.format(self.name) + s += '\n-----------------------------------------------------\n' + s += '|{:^30}|{:^20}|'.format('Name', 'Value') + s += '\n#####################################################' + print(s) + for p in self._parameters: + name = self._parameters[p]['Name'] + node = self._parameters[p]['Node'] + value = await node.read_value() + typedefinition = await node.read_type_definition() + if value != None: + s = '|{:^30}|{:^20}|'.format(name, value) + else: + nan = 'nan' + s = f'|{name:^30}|{nan:^20}|' + s += '\n-----------------------------------------------------' + print(s) + + print() diff --git a/asyncua/common/statemachine.py b/asyncua/common/statemachine.py index 736d9a3c0..76b5adfb1 100644 --- a/asyncua/common/statemachine.py +++ b/asyncua/common/statemachine.py @@ -16,9 +16,12 @@ ''' import logging import datetime +from msilib.schema import Property +from re import A from asyncua import Server, ua, Node -from asyncua.common.event_objects import TransitionEvent, ProgramTransitionEvent +from asyncua.common.parameter_set import ParameterSet +from asyncua.common.event_objects import TransitionEvent, ProgramTransitionEvent, AlarmCondition from typing import Union, List _logger = logging.getLogger(__name__) @@ -42,6 +45,22 @@ def __init__(self, id, name: str=None, number: int=None, node: Node=None): self.effectivedisplayname = ua.LocalizedText(name, "en-US") self.node = node #will be written from statemachine.add_state() or you need to overwrite it if the state is part of xml + async def init(self): + nbr = await self.node.get_child('StateNumber') + self.number = await nbr.read_value() + self.name = (await self.node.read_browse_name()).Name + + async def on_entry(self): + _logger.debug(f'Entering the {self.name} state.') + + async def on_exit(self): + _logger.debug(f'Leaving the {self.name} state.') + + async def execute(self): + _logger.debug(f'Executing the {self.name} state.') + + def __eq__(self, __o: object) -> bool: + return __o != None and self.name == __o.name and self.number == __o.number class Transition(object): ''' @@ -66,7 +85,43 @@ def __init__(self, id, name: str=None, number: int=None, node: Node=None): self.number = number self._transitiontime = datetime.datetime.utcnow() #will be overwritten from _write_transition() self.node = node #will be written from statemachine.add_state() or you need to overwrite it if the state is part of xml + self.from_state: State = None + self.to_state: State = None + self.cause:Cause = None + + async def init(self): + nbr = await self.node.get_child('TransitionNumber') + self.number = await nbr.read_value() + self.name = (await self.node.read_browse_name()).Name + + def check_transition(self, to_state): + return True if self.to_state == to_state else False + + def __eq__(self, __o: object) -> bool: + return self.name == __o.name and self.number == __o.number +class Cause: + def __init__(self, node, name, state_machine): + self.node = node + self.state_machine = state_machine + self.name = name + + async def callback(self, parent): + transition = self.state_machine.get_transition_by_cause(self) + if transition: + from_state = transition.from_state + to_state = transition.to_state + _logger.debug(f'Transitioning from {from_state.name} to {to_state.name}. Caused by {self.name} ({self.node}).') + + msg = f'Event was triggered by invoking the {self.name} method' + return await self.state_machine.change_state(transition.to_state, event_msg=msg) + + else: + _logger.error(f'Triggering transition by cause {self.name} ({self.node}) failed. InvalidState.') + return ua.StatusCode(ua.StatusCodes.BadInvalidState) + + def __eq__(self, __o: object) -> bool: + return __o != None and self.name == __o.name and self.node.nodeid == __o.node.nodeid class StateMachine(object): ''' @@ -75,19 +130,22 @@ class StateMachine(object): LastTransition: Optional "TransitionVariableType" Generates TransitionEvent's ''' - def __init__(self, server: Server=None, parent: Node=None, idx: int=None, name: str=None): + def __init__(self, server: Server=None, node: Node=None, parent: Node=None, idx: int=None, name: str=None): if not isinstance(server, Server): raise ValueError(f"server: {type(server)} is not a instance of Server class") - if not isinstance(parent, Node): - raise ValueError(f"parent: {type(parent)} is not a instance of Node class") - if idx is None: + # if not isinstance(parent, Node): + # raise ValueError(f"parent: {type(parent)} is not a instance of Node class") + + if parent and idx is None: idx = parent.nodeid.NamespaceIndex + if node and idx is None: + idx = node.nodeid.NamespaceIndex if name is None: name = "StateMachine" self.locale = "en-US" self._server = server self._parent = parent - self._state_machine_node = None + self._state_machine_node = node self._state_machine_type = ua.NodeId(2299, 0) #StateMachineType self._name = name self._idx = idx @@ -106,33 +164,53 @@ def __init__(self, server: Server=None, parent: Node=None, idx: int=None, name: self.evtype = TransitionEvent() self._current_state = State(None) - async def install(self, optionals: bool=False): + self.states = [] + self.transitions = [] + self.causes = [] + + async def install(self, optionals: bool=True): ''' setup adressspace ''' self._optionals = optionals - self._state_machine_node = await self._parent.add_object( - self._idx, - self._name, - objecttype=self._state_machine_type, - instantiate_optional=optionals - ) + if not self._state_machine_node and self._parent: + self._state_machine_node = await self._parent.add_object( + self._idx, + self._name, + objecttype=self._state_machine_type, + instantiate_optional=optionals + ) + + elif self._state_machine_node: + self.name = (await self._state_machine_node.read_browse_name()).Name + self._state_machine_type = await self._state_machine_node.read_type_definition() + type_node = self._server.get_node(self._state_machine_type) + children = await type_node.get_children() + await self._add_states(children) + await self._add_transitions(children) + + children = await self._state_machine_node.get_children() + await self._add_methods(children) + else: + raise ValueError(f"Failed to set up state machine {self._state_machine_node}." ) + if self._optionals: self._last_transition_node = await self._state_machine_node.get_child(["LastTransition"]) children = await self._last_transition_node.get_children() childnames = [] for each in children: - childnames.append(await each.read_browse_name()) + childnames.append((await each.read_browse_name()).Name) if "TransitionTime" not in childnames: self._last_transition_transitiontime_node = await self._last_transition_node.add_property( 0, "TransitionTime", - ua.Variant(datetime.datetime.utcnow(), varianttype=ua.VariantType.DateTime) + ua.Variant(datetime.datetime.utcnow(), VariantType=ua.VariantType.DateTime) ) else: self._last_transition_transitiontime_node = await self._last_transition_node.get_child("TransitionTime") + await self.init(self._state_machine_node) - + async def init(self, statemachine: Node): ''' initialize and get subnodes @@ -170,28 +248,220 @@ async def init(self, statemachine: Node): async def change_state(self, state: State, transition: Transition=None, event_msg:Union[str, ua.LocalizedText]=None, severity: int=500): ''' - method to change the state of the statemachine - state: "State" mandatory - transition: "Transition" optional - event_msg: "LocalizedText" optional - severity: "Int" optional + Triggering a transition to change the state + + :param state: target state + :param transition: transition to trigger + :param event_msg: "LocalizedText" optional + :param severity: "Int" optional ''' + if not transition: + transition = self.get_transition(self._current_state, state) + + if transition: + res = await self._current_state.on_exit() + if res != False: + _logger.debug(f"Transitioning from state {self._current_state.name} to state {transition.to_state.name}.") + self._current_state = transition.to_state + await self._write_state(transition.to_state) + if self._optionals: + await self._write_transition(transition) + + if event_msg: + if isinstance(event_msg, str): + event_msg = ua.LocalizedText(event_msg, self.locale) + if not isinstance(event_msg, ua.LocalizedText): + raise ValueError(f"Statemachine: {self._name} -> event_msg: {event_msg} is not a instance of LocalizedText") + self._evgen.event.Message = event_msg + self._evgen.event.Severity = severity + self._evgen.event.ToState = ua.LocalizedText(state.name, self.locale) + if transition: + self._evgen.event.Transition = ua.LocalizedText(transition.name, self.locale) + self._evgen.event.FromState = ua.LocalizedText(self._current_state.name) + await self._evgen.trigger() + + await self._current_state.on_entry() + return ua.StatusCode(ua.StatusCodes.Good) + else: + return ua.StatusCode(ua.StatusCodes.BadUnexpectedError) + else: + _logger.debug(f'Invalid transition for leaving the {self._current_state.name} state in {self._name} ({self._state_machine_node}).') + return ua.StatusCode(ua.StatusCodes.BadInvalidState) + + @property + def current_state(self): + return self._current_state + + @property + def current_state_id(self): + return self._current_state.id + + @property + def current_state_number(self): + return self._current_state.number + + @property + def current_state_name(self): + return self._current_state.name + + def get_state_by_name(self, name: str): + for state in self.states: + if state.name == name: + return state + + def get_transition_by_name(self, name: str): + for transition in self.transitions: + if transition.name == name: + return transition + + def get_cause_by_name(self, name: str): + for cause in self.causes: + if cause.name == name: + return cause + + def get_state_by_number(self, number: ua.Int32): + for state in self.states: + if state.number == number: + return state + + def get_transition_by_number(self, number: ua.Int32): + for transition in self.transitions: + if transition.number == number: + return transition + + def get_state_by_id(self, id: ua.NodeId): + for state in self.states: + if state.id.Value == id: + return state + + def get_transition_by_id(self, id: ua.NodeId): + for transition in self.transitions: + if transition.id.Value == id: + return transition + + def get_cause_by_id(self, id: ua.NodeId): + for cause in self.causes: + if cause.id == id: + return cause + + def get_transition_by_cause(self, cause: Cause): + for transition in self.transitions: + if transition.from_state ==self._current_state and transition.cause == cause: + return transition + + async def set_initial_state(self, state: State): await self._write_state(state) - if transition: - await self._write_transition(transition) - if event_msg: - if isinstance(event_msg, str): - event_msg = ua.LocalizedText(event_msg, self.locale) - if not isinstance(event_msg, ua.LocalizedText): - raise ValueError(f"Statemachine: {self._name} -> event_msg: {event_msg} is not a instance of LocalizedText") - self._evgen.event.Message = event_msg - self._evgen.event.Severity = severity - self._evgen.event.ToState = ua.LocalizedText(state.name, self.locale) - if transition: - self._evgen.event.Transition = ua.LocalizedText(transition.name, self.locale) - self._evgen.event.FromState = ua.LocalizedText(self._current_state.name) - await self._evgen.trigger() - self._current_state = state + self._current_state = state + pass + + def get_transition(self, from_state: State, to_state: State): + for transition in self.transitions: + if transition.from_state.number == from_state.number \ + and transition.to_state.number == to_state.number: + return transition + return None + + async def _add_states(self, children): + """ + Create an instance of each state in the type definition + + :param children: children of the state machine type node + """ + for child in children: + type_id = await child.read_type_definition() + if type_id == ua.NodeId(ua.ObjectIds.StateType): + await self._add_state(child.nodeid) + elif type_id == ua.NodeId(ua.ObjectIds.InitialStateType): + await self._add_state(child.nodeid) + await self.change_state(self.get_state_by_id(child.nodeid)) + + async def _add_transitions(self, children): + """ + Create an instance for each transition in the type defintion + + :param children: children of the state machine type node + """ + for child in children: + type_id = await child.read_type_definition() + if type_id == ua.NodeId(ua.ObjectIds.TransitionType): + await self._add_transition(child.nodeid) + + async def _add_methods(self, children): + """ + Create an instance for each method (Cause) in the state machine instance node + + :param children: children of the state machine instance node + """ + for child in children: + node_class = await child.read_node_class() + if node_class == ua.NodeClass.Method: + name = (await child.read_browse_name()).Name + cause = self.get_cause_by_name(name) + self._server.link_method(child, cause.callback) + + async def _add_state(self, id): + """ + Create a new state object and add it to the state machine + + :param id: node id of the state node + """ + try: + node = self._server.get_node(id) + state = State(id, node=node) + name = (await node.read_browse_name()).Name + await state.init() + setattr(self, name, state) + self.states.append(state) + _logger.debug(f'Added state {name} to {self.name} ({self._state_machine_node})') + + except Exception as e: + _logger.warning(f'Failed to add state {name}. {e}') + + async def _add_transition(self, id): + """ + Createing a new transition object and adding it to the state machine + + :param id: node id of the transition node + """ + + node = self._server.get_node(id) + name = (await node.read_browse_name()).Name + transition = Transition(id, node=node) + await transition.init() + setattr(self, name, transition) + self.transitions.append(transition) + + from_state = (await transition.node.get_referenced_nodes(refs=ua.ObjectIds.FromState))[0] + to_state = (await transition.node.get_referenced_nodes(refs=ua.ObjectIds.ToState))[0] + cause = await transition.node.get_referenced_nodes(refs=ua.ObjectIds.HasCause) + transition.from_state = self.get_state_by_id(from_state.nodeid) + transition.to_state = self.get_state_by_id(to_state.nodeid) + + # Not all transitions have a cause reference + if cause: + await self._add_cause(cause[0], transition) + _logger.debug(f'Added cause {self.causes[-1].name} to transition {name}.') + + _logger.debug(f'Added transition {name} to {self.name} ({self._state_machine_node}).') + + + async def _add_cause(self, id: ua.NodeId, transition: Transition): + """ + Creating a new cause object and adding it to the transition and state machine + + :param id: id of the method (cause) node + :param transition: the transition that is triggered by the method + """ + node = self._server.get_node(id) + name = (await node.read_browse_name()).Name + cause = Cause(node, name, self) + if cause in self.causes: + cause = self.causes[self.causes.index(cause)] + else: + setattr(self, name, cause) + self.causes.append(cause) + self._server.link_method(node, cause.callback) + transition.cause = cause async def _write_state(self, state: State): if not isinstance(state, State): @@ -199,9 +469,9 @@ async def _write_state(self, state: State): await self._current_state_node.write_value(ua.LocalizedText(state.name, self.locale), ua.VariantType.LocalizedText) if state.node: if self._current_state_id_node: - await self._current_state_id_node.write_value(state.id) + await self._current_state_id_node.write_value(state.node.nodeid, varianttype=ua.VariantType.NodeId) if self._current_state_name_node and state.name: - await self._current_state_name_node.write_value(state.name, ua.VariantType.QualifiedName) + await self._current_state_name_node.write_value(ua.QualifiedName(state.name, state.node.nodeid.NamespaceIndex), ua.VariantType.QualifiedName) if self._current_state_number_node and state.number: await self._current_state_number_node.write_value(state.number, ua.VariantType.UInt32) if self._current_state_effective_display_name_node and state.effectivedisplayname: @@ -218,9 +488,9 @@ async def _write_transition(self, transition: Transition): await self._last_transition_node.write_value(ua.LocalizedText(transition.name, self.locale), ua.VariantType.LocalizedText) if self._optionals: if self._last_transition_id_node: - await self._last_transition_id_node.write_value(transition.id) + await self._last_transition_id_node.write_value(transition.node.nodeid, varianttype=ua.VariantType.NodeId) if self._last_transition_name_node and transition.name: - await self._last_transition_name_node.write_value(ua.QualifiedName(transition.name, self._idx), ua.VariantType.QualifiedName) + await self._last_transition_name_node.write_value(ua.QualifiedName(transition.name, transition.node.nodeid.NamespaceIndex), ua.VariantType.QualifiedName) if self._last_transition_number_node and transition.number: await self._last_transition_number_node.write_value(transition.number, ua.VariantType.UInt32) if self._last_transition_transitiontime_node and transition._transitiontime: @@ -274,15 +544,15 @@ async def add_transition(self, transition: Transition, transition_type: ua.NodeI if not transition.id: transition.id = transition.node.nodeid return transition.node - + class FiniteStateMachine(StateMachine): ''' Implementation of an FiniteStateMachineType a little more advanced than the basic one if you need to know the available states and transition from clientside ''' - def __init__(self, server: Server=None, parent: Node=None, idx: int=None, name: str=None): - super().__init__(server, parent, idx, name) + def __init__(self, server: Server=None, node: Node=None, parent: Node=None, idx: int=None, name: str=None): + super().__init__(server, node, parent, idx, name) if name is None: self._name = "FiniteStateMachine" self._state_machine_type = ua.NodeId(2771, 0) @@ -308,13 +578,33 @@ async def set_available_transitions(self, transitions: List[ua.NodeId]): class ExclusiveLimitStateMachine(FiniteStateMachine): ''' NOT IMPLEMENTED "ExclusiveLimitStateMachineType" + https://reference.opcfoundation.org/v104/Core/docs/Part9/5.8.12/ ''' - def __init__(self, server=None, parent=None, idx=None, name=None): - super().__init__(server, parent, idx, name) + def __init__(self, server=None, node=None, parent=None, idx=None, name=None): + super().__init__(server, node, parent, idx, name) if name is None: name = "ExclusiveLimitStateMachine" self._state_machine_type = ua.NodeId(9318, 0) - raise NotImplementedError + + self.evtype = TransitionEvent()#AlarmCondition() + + self.High: State = None + self.HighHigh: State = None + self.Low: State = None + self.LowLow: State = None + + self.HighHighToHigh: Transition = None + self.HighToHighHigh: Transition = None + self.LowLowToLow: Transition = None + self.LowToLowLow: Transition = None + + # TODO: Needs to be implemented by an alarm. With the information from the type definition + # it is only possible to switch between either, High<->HighHigh or Low<->LowLow, since there + # are not other valid transitions. + #raise NotImplementedError('This is not ready to use. Needs to be part of an miltilevel Alarm.') + + async def install(self, optionals=True): + await super().install(optionals) class FileTransferStateMachine(FiniteStateMachine): @@ -328,23 +618,190 @@ def __init__(self, server=None, parent=None, idx=None, name=None): if name is None: name = "FileTransferStateMachine" self._state_machine_type = ua.NodeId(15803, 0) - raise NotImplementedError + #raise NotImplementedError + + self.ApplyWrite: State = None + self.Error: State = None + self.Idle: State = None + self.ReadPrepare: State = None + self.ReadTransfer: State = None + + self.ApplyWriteToError: Transition = None + self.ApplyWriteToIdle: Transition = None + self.ErrorToIdle: Transition = None + self.IdleToApplyWrite: Transition = None + self.IdleToReadPrepare: Transition = None + self.ReadPrepareToError: Transition = None + self.ReadPrepareToReadTransfer: Transition = None + self.ReadTransferToError: Transition = None + self.ReadTransferToIdle: Transition = None + + self.Reset: Cause = None + + # Needs to resolve the missing HasCause references + raise NotImplementedError('Needs to be implemented by a TemporaryFileTransfer object.') + + async def install(self): + await super().install() + + self.ApplyWrite.on_entry = self.on_entry_apply_write + self.ApplyWrite.on_exit = self.on_exit_apply_write + self.Error.on_entry = self.on_entry_error + self.Error.on_exit = self.on_exit_error + self.Idle.on_entry = self.on_entry_idle + self.Idle.on_exit = self.on_exit_idle + self.ReadPrepare.on_entry = self.on_entry_read_prepare + self.ReadPrepare.on_exit = self.on_exit_read_prepare + self.ReadTransfer.on_entry = self.on_entry_read_transfer + self.ReadTransfer.on_exit = self.on_exit_read_transfer + + await self._add_reset_cause() + + async def on_entry_apply_write(self): + _logger.debug(f'Entering the ApplyWrite state of {self.name} ({self._state_machine_node}).') + + async def on_exit_apply_write(self): + _logger.debug(f'Leaving the ApplyWrite state of {self.name} ({self._state_machine_node}).') + + async def on_entry_error(self): + _logger.debug(f'Entering the Error state of {self.name} ({self._state_machine_node}).') + + async def on_exit_error(self): + _logger.debug(f'Leaving the Error state of {self.name} ({self._state_machine_node}).') + + async def on_entry_idle(self): + _logger.debug(f'Entering the Idle state of {self.name} ({self._state_machine_node}).') + + async def on_exit_idle(self): + _logger.debug(f'Leaving the Idle state of {self.name} ({self._state_machine_node}).') + + async def on_entry_read_prepare(self): + _logger.debug(f'Entering the ReadPrepare state of {self.name} ({self._state_machine_node}).') + + async def on_exit_read_prepare(self): + _logger.debug(f'Leaving the ReadPrepare state of {self.name} ({self._state_machine_node}).') + + async def on_entry_read_transfer(self): + _logger.debug(f'Entering the ReadTransfer state of {self.name} ({self._state_machine_node}).') + + async def on_exit_read_transfer(self): + _logger.debug(f'Leaving the ReadTransfer state of {self.name} ({self._state_machine_node}).') + async def _add_causes(self): + #TODO:It seems the ErrorToIdle transition has no HasCause reference in the type definition. So we add it here. + # Also additional cause references are defined by the instance not within the type definition. These + # have to come from the methods of an TemporaryFileTransfer object. + pass -class ProgramStateMachine(FiniteStateMachine): + async def reset(self, msg=''): + return await self.change_state(self.Idle, event_msg=msg) + + +class ProgramStateMachine(FiniteStateMachine): ''' https://reference.opcfoundation.org/v104/Core/docs/Part10/4.2.3/ Implementation of an ProgramStateMachine its quite a complex statemachine with the optional possibility to make the statchange from clientside via opcua-methods ''' - def __init__(self, server=None, parent=None, idx=None, name=None): - super().__init__(server, parent, idx, name) - if name is None: - name = "ProgramStateMachine" - self._state_machine_type = ua.NodeId(2391, 0) - self.evtype = ProgramTransitionEvent() - raise NotImplementedError + def __init__(self, server: Server=None, node:Node=None, parent: Node=None, idx: int=None, name: str=None): + super().__init__(server, node, parent, idx, name) + + self.evtype = ProgramTransitionEvent() + + self.Running: State = None + self.Ready: State = None + self.Halted: State = None + self.Suspended: State = None + + self.ReadyToRunning: Transition = None + self.ReadyToHalted: Transition = None + self.RunningToReady: Transition = None + self.RunningToHalted: Transition = None + self.RunningToSuspended: Transition = None + self.SuspendedToReady: Transition = None + self.SuspendedToRunning: Transition = None + self.SuspendedToHalted: Transition = None + self.HaltedToReady: Transition = None + + self.Start: Cause = None + self.Suspend: Cause = None + self.Resume: Cause = None + self.Halt: Cause = None + + self.FinalResultData: ParameterSet = None + + self._auto_delete_node: Node = None # ua.Boolean + self._deletable_node: Node = None # ua.Boolean + self._program_diagnostics_node: Node = None # ua.ProgramDiagnostic2DataType + self._max_instance_count_node: Node = None # ua.UInt32 + self._max_recycle_count_node: Node = None # ua.UInt32 + self._instance_count_node: Node = None # ua.UInt32 + + async def install(self): + await super().install() + + self.Ready.on_entry = self.on_entry_ready + self.Ready.on_exit = self.on_exit_ready + #self.Ready.execute = self.execute_ready + self.Running.on_entry = self.on_entry_running + self.Running.on_exit = self.on_exit_running + #self.Running.execute = self.execute_running + self.Suspended.on_entry = self.on_entry_suspended + self.Suspended.on_exit = self.on_exit_suspended + #self.Suspended.execute = self.execute_suspended + self.Halted.on_entry = self.on_entry_halted + self.Halted.on_exit = self.on_exit_halted + #self.Halted.execute = self.execute_halted + await self._add_final_result_data_set() + + async def _add_final_result_data_set(self): + try: + final_result_data_node = await self._state_machine_node.get_child(['FinalResultData']) + if final_result_data_node: + self.FinalResultData = ParameterSet(final_result_data_node, subscribe=True, source=self._server) + await self.FinalResultData.init() + except Exception as e: + _logger.debug(f'ProgramStateMachine {self._state_machine_node} has no FinalResultData set. {e}') + + async def on_entry_ready(self): + _logger.debug(f'Entering the Ready state of {self.name} ({self._state_machine_node}).') + + async def on_exit_ready(self): + _logger.debug(f'Leaving the Ready state of {self.name} ({self._state_machine_node}).') + + async def on_entry_running(self): + _logger.debug(f'Entering the Running state of {self.name} ({self._state_machine_node}).') + + async def on_exit_running(self): + _logger.debug(f'Leaving the Running state of {self.name} ({self._state_machine_node}).') + + async def on_entry_suspended(self): + _logger.debug(f'Entering the Suspended state of {self.name} ({self._state_machine_node}).') + + async def on_exit_suspended(self): + _logger.debug(f'Leaving the Suspended state of {self.name} ({self._state_machine_node}).') + + async def on_entry_halted(self): + _logger.debug(f'Entering the Halted state of {self.name} ({self._state_machine_node}).') + + async def on_exit_halted(self): + _logger.debug(f'Leaving the Halted state of {self.name} ({self._state_machine_node}).') + + async def start(self, msg=''): + return await self.change_state(self.Running, event_msg=msg) + + async def suspend(self, msg=''): + return await self.change_state(self.Suspended, event_msg=msg) + + async def resume(self, msg=''): + return await self.change_state(self.Running, event_msg=msg) + + async def halt(self, msg=''): + return await self.change_state(self.Halted, event_msg=msg) + + async def reset(self, msg=''): + return await self.change_state(self.Ready, event_msg=msg) class ShelvedStateMachine(FiniteStateMachine): ''' diff --git a/examples/StateMachine.Example.NodeSet2.xml b/examples/StateMachine.Example.NodeSet2.xml new file mode 100644 index 000000000..c2039790b --- /dev/null +++ b/examples/StateMachine.Example.NodeSet2.xml @@ -0,0 +1,339 @@ + + + + /StateMachine/Example/ + + + + + + + + i=1 + i=5 + i=6 + i=7 + i=13 + i=12 + i=17 + i=20 + i=21 + i=47 + i=46 + i=35 + i=40 + + + + + + + + + + + /StateMachine/Example/ + + i=11715 + i=11616 + + + + IsNamespaceSubset + + ns=1;i=5000 + i=68 + + + + NamespacePublicationDate + + ns=1;i=5000 + i=68 + + + 2022-01-15T00:00:00Z + + + + NamespaceUri + + ns=1;i=5000 + i=68 + + + /StateMachine/Example/ + + + + NamespaceVersion + + ns=1;i=5000 + i=68 + + + 1.00 + + + + StaticNodeIdTypes + + ns=1;i=5000 + i=68 + + + + StaticNumericNodeIdRange + + ns=1;i=5000 + i=68 + + + + StaticStringNodeIdPattern + + ns=1;i=5000 + i=68 + + + + DefaultRolePermissions + + ns=1;i=5000 + i=68 + + + + State Machine Example + + i=85 + i=58 + + + + ExampleProgram + + ns=1;i=5001 + i=2391 + + + + AutoDelete + + ns=1;i=5002 + i=68 + + + + CurrentState + + ns=1;i=5002 + i=2760 + + + + Deletable + + ns=1;i=5002 + i=68 + + + + LastTransition + + ns=1;i=5002 + i=2767 + + + + RecycleCount + + ns=1;i=5002 + i=68 + + + + Id + + ns=1;i=6009 + i=68 + + + + Number + + ns=1;i=6009 + i=68 + + + + Id + + ns=1;i=6011 + i=68 + + + + Number + + ns=1;i=6011 + i=68 + + + + TransitionTime + + ns=1;i=6011 + i=68 + + + + Halt + + ns=1;i=5002 + + + + Reset + + ns=1;i=5002 + + + + Resume + + ns=1;i=5002 + + + + Start + + ns=1;i=5002 + + + + Suspend + + ns=1;i=5002 + + + + FinalResultData + + ns=1;i=5002 + i=58 + + + + Result + + ns=1;i=5003 + i=63 + + + false + + + + FileTransfer + + ns=1;i=5001 + i=15803 + + + + CurrentState + + ns=1;i=5004 + i=2760 + + + + Reset + + ns=1;i=5004 + + + + Id + + ns=1;i=6019 + i=68 + + + + ExclusiveLimitState + + ns=1;i=5001 + i=9318 + + + + CurrentState + + ns=1;i=5012 + i=2760 + + + + Id + + ns=1;i=6083 + i=68 + + + + Name + + ns=1;i=6083 + i=68 + + + + 0 + None + + + + + Number + + ns=1;i=6083 + i=68 + + + + EffectiveDisplayName + + ns=1;i=6083 + i=68 + + + + ExampleParameterSet + + ns=1;i=5001 + i=58 + + + + MaxProgramIterationsCount + + ns=1;i=5013 + i=63 + + + 10 + + + + CurrentProgramIterationsCount + + ns=1;i=5013 + i=63 + + + 0 + + + \ No newline at end of file diff --git a/examples/server_state_machine_type.py b/examples/server_state_machine_type.py new file mode 100644 index 000000000..1ab5c9d52 --- /dev/null +++ b/examples/server_state_machine_type.py @@ -0,0 +1,124 @@ +import os +import asyncio +import logging +from asyncua import ua, Server, Node +from asyncua.common.statemachine import ProgramStateMachine, ExclusiveLimitStateMachine +from asyncua.common.parameter_set import ParameterSet + +class ExampleParameterSet(ParameterSet): + MaxProgramIterationsCount: Node = None + CurrentProgramIterationsCount: Node = None + + @property + def CurrentProgramIterationsCountValue(self): + return self.CurrentProgramIterationsCount.value + +class ExampleStateMachine(ProgramStateMachine): + def __init__(self, server: Server, node: Node, max: Node, cnt: Node): + super().__init__(server, node) + self._running = False + self._task_running: asyncio.Task = None + self._index = 0 + self._max = max + self._result = None + self._index = cnt + + # Make the parameter value a read only property + @property + def max(self): + return self._max.value + + async def install(self): + await super().install() + self._result = self.FinalResultData.Result + + async def on_entry_ready(self): + await self._index.write_value(0, varianttype=ua.VariantType.UInt16) + + async def execute_running(self): + while self._running and self._index.value < self.max: + print(f'Executing the Running state {self._index.value+1}/{self.max}') + await self._index.write_value(self._index.value+1, varianttype=ua.VariantType.UInt16) + await asyncio.sleep(1) + + if self._index.value == self.max and self._result.value: + await self.change_state(self.Ready, event_msg=f'Execution successfully finished. Result: {self._result.value}') + + async def on_entry_running(self): + if not self._running: + self._running = True + self._task_running = asyncio.create_task(self.execute_running()) + + async def on_exit_running(self): + if self._running: + #self._task_running.cancel() + self._running = False + +async def main(): + logging.basicConfig(level=logging.FATAL) + al = logging.getLogger('asyncua') + al.setLevel(logging.ERROR) + al = logging.getLogger('asyncua.common.statemachine') + al.setLevel(logging.DEBUG) + + server = Server() + await server.init() + + nodeset_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'StateMachine.Example.NodeSet2.xml') + await server.import_xml(nodeset_path) + idx = await server.get_namespace_index('/StateMachine/Example/') + + # Example ParameterSet + parameter_set_id = ua.NodeId(Identifier=5013, NamespaceIndex=idx) + parameter_set_node = server.get_node(parameter_set_id) + eps = ExampleParameterSet(parameter_set_node, subscribe=True, source=server, interval=50) + await eps.init() + + # Example ProgramStateMachine + state_machine_id = ua.NodeId(Identifier=5002, NamespaceIndex=idx) + state_machine_node = server.get_node(state_machine_id) + esm = ExampleStateMachine(server, node=state_machine_node, max=eps.MaxProgramIterationsCount, cnt=eps.CurrentProgramIterationsCount) + await esm.install() + await esm.set_initial_state(esm.Ready) + + await esm.FinalResultData.print_parameter_list() + await esm.FinalResultData.set_value('Result', True) + print(esm.FinalResultData.Result.value) + + await esm.start('Starting execution.') + #await esm.FinalResultDataSet.update_subscription_interval(50) + + # ExclusiveLimitStateMachine + limit_state_machine_id = ua.NodeId(Identifier=5012, NamespaceIndex=idx) + limit_state_machine_node = server.get_node(limit_state_machine_id) + limit_sm = ExclusiveLimitStateMachine(server, limit_state_machine_node) + await limit_sm.install(optionals=False) + await limit_sm.set_initial_state(limit_sm.Low) + + val = 0 + async with server: + while True: + if val == 5: + await limit_sm.change_state(limit_sm.LowLow, event_msg='Now its freezing cold.') + elif val == 10: + await limit_sm.change_state(limit_sm.Low, event_msg='Temperature is to low.') + elif val == 12: + # Since there is no valid transition defined by the type to go from Low to High it will fail here + res = await limit_sm.change_state(limit_sm.High, event_msg='Temperature is to high.') + if not res.is_good(): + print(f'Changeing to high failed. {res.name}') + + val += 1 + if val > 12: + val = 0 + + if esm.current_state_name == 'Suspended': + print('The example state machine is now paused.') + + if eps.CurrentProgramIterationsCountValue == 5: + print('Program count reached 5') + + await asyncio.sleep(1) + +if __name__ == "__main__": + asyncio.run(main())