# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import keyword import logging import re import task_loop import wardmodem_exceptions as wme class StateMachine(object): """ Base class for all state machines in wardmodem. All derived objects bundled as part of wardmodem (1) Reside in state_machines/ (2) Have their own module e.g., my_module (3) The main state machine class in my_module is called MyModule. """ def __init__(self, state, transceiver, modem_conf): """ @param state: The GlobalState object shared by all state machines. @param transceiver: The ATTransceiver object to interact with. @param modem_conf: A modem configuration object that contains configuration data for different state machines. @raises: SetupException if we attempt to create an instance of a machine that has not been completely specified (see get_well_known_name). """ self._state = state self._transceiver = transceiver self._modem_conf = modem_conf self._logger = logging.getLogger(__name__) self._task_loop = task_loop.get_instance() self._state_update_tag = 0 # Used to tag logs of async updates to # state. # Will raise an exception if this machine should not be instantiated. self.get_well_known_name() # Add all wardmodem response functions used by this machine. self._add_response_function('wm_response_ok') self._add_response_function('wm_response_error') self._add_response_function('wm_response_ring') self._add_response_function('wm_response_text_only') # ########################################################################## # Subclasses must override these. def get_well_known_name(self): """ A well known name of the completely specified state machine. The first derived class that completely specifies some state machine should implement this function to return the name of the defining module as a string. """ # Do not use self._raise_setup_error because it causes infinite # recursion. raise wme.WardModemSetupException( 'Attempted to get well known name for a state machine that is ' 'not completely specified.') # ########################################################################## # Protected convenience methods to be used as is by subclasses. def _respond(self, response, response_delay_ms=0, *response_args): """ Respond to the modem after some delay. @param reponse: String response. This must be one of the response strings recognized by ATTransceiver. @param response_delay_ms: Delay in milliseconds after which the response should be sent. Type: int. @param *response_args: The arguments for the response. @requires: response_delay_ms >= 0 """ assert response_delay_ms >= 0 dbgstr = self._tag_with_name( 'Will respond with "%s(%s)" after %d ms.' % (response, str(response_args), response_delay_ms)) self._logger.debug(dbgstr) self._task_loop.post_task_after_delay( self._transceiver.process_wardmodem_response, response_delay_ms, response, *response_args) def _update_state(self, state_update, state_update_delay_ms=0): """ Post a (delayed) state update. @param state_update: The state update to apply. This is a map {string --> state enum} that specifies all the state components to be updated. @param state_update_delay_ms: Delay in milliseconds after which the state update should be applied. Type: int. @requires: state_update_delay_ms >= 0 """ assert state_update_delay_ms >= 0 dbgstr = self._tag_with_name( '[tag:%d] Will update state as %s after %d ms.' % (self._state_update_tag, str(state_update), state_update_delay_ms)) self._logger.debug(dbgstr) self._task_loop.post_task_after_delay( self._update_state_callback, state_update_delay_ms, state_update, self._state_update_tag) self._state_update_tag += 1 def _respond_ok(self): """ Convenience function to respond when everything is OK. """ self._respond(self.wm_response_ok, response_delay_ms=0) def _respond_error(self): """ Convenience function to respond when an error occured. """ self._respond(self.wm_response_error, response_delay_ms=0) def _respond_ring(self): """ Convenience function to respond with RING. """ self._respond(self.wm_response_ring, response_delay_ms=0) def _respond_with_text(self, text): """ Send back just |text| as the response, without any AT prefix. """ self._respond(self.wm_response_text_only, 0, text) def _add_response_function(self, function): """ Add a response used by this state machine to send to the ATTransceiver. A state machine should register all the responses it will use in its __init__ function by calling self._add_response_function('wm_response_dummy') The response can then be used to respond to the transceiver thus: self._respond(self.wm_response_dummy) @param function: The string function name to add. Must be a valid python identifier in lowercase. Also, these names are conventionally named matching the re 'wm_response_([a-z0-9]*[_]?)*' @raises: WardModemSetupError if the added response function is ill formed. """ if not re.match('wm_response_([a-z0-9]*[_]?)*', function) or \ keyword.iskeyword(function): self._raise_setup_error('Response function name ill-formed: |%s|' % function) try: getattr(self, function) self._raise_setup_error( 'Attempted to add response function %s which already ' 'exists.' % function) except AttributeError: # OK, This is the good case. setattr(self, function, function) def _raise_setup_error(self, errstring): """ Log the error and raise WardModemSetupException. @param errstring: The error string. """ errstring = self._tag_with_name(errstring) self._logger.error(errstring) raise wme.WardModemSetupException(errstring) def _raise_runtime_error(self, errstring): """ Log the error and raise StateMachineException. @param errstring: The error string. """ errstring = self._tag_with_name(errstring) self._logger.error(errstring) raise wme.StateMachineException(errstring) def _tag_with_name(self, log_string): """ If possible, prepend the log string with the well know name of the object. @param log_string: The string to modify. @return: The modified string. """ name = self.get_well_known_name() log_string = '[' + name + '] ' + log_string return log_string # ########################################################################## # Private methods not to be used by subclasses. def _update_state_callback(self, state_update, tag): """ Actually update the state. @param state_update: The state update to effect. This is a map {string --> state enum} that specifies all the state components to be updated. @param tag: The tag for this state update. @raises: StateMachineException if the state update fails. """ dbgstr = self._tag_with_name('[tag:%d] State update applied.' % tag) self._logger.debug(dbgstr) for component, value in state_update.iteritems(): self._state[component] = value