1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import keyword
6import logging
7import re
8
9import task_loop
10import wardmodem_exceptions as wme
11
12class StateMachine(object):
13    """
14    Base class for all state machines in wardmodem.
15
16    All derived objects bundled as part of wardmodem
17        (1) Reside in state_machines/
18        (2) Have their own module e.g., my_module
19        (3) The main state machine class in my_module is called MyModule.
20
21    """
22
23    def __init__(self, state, transceiver, modem_conf):
24        """
25        @param state: The GlobalState object shared by all state machines.
26
27        @param transceiver: The ATTransceiver object to interact with.
28
29        @param modem_conf: A modem configuration object that contains
30                configuration data for different state machines.
31
32        @raises: SetupException if we attempt to create an instance of a machine
33        that has not been completely specified (see get_well_known_name).
34
35        """
36        self._state = state
37        self._transceiver = transceiver
38        self._modem_conf = modem_conf
39
40        self._logger = logging.getLogger(__name__)
41        self._task_loop = task_loop.get_instance()
42
43        self._state_update_tag = 0  # Used to tag logs of async updates to
44                                    # state.
45
46        # Will raise an exception if this machine should not be instantiated.
47        self.get_well_known_name()
48
49        # Add all wardmodem response functions used by this machine.
50        self._add_response_function('wm_response_ok')
51        self._add_response_function('wm_response_error')
52        self._add_response_function('wm_response_ring')
53        self._add_response_function('wm_response_text_only')
54
55
56    # ##########################################################################
57    # Subclasses must override these.
58    def get_well_known_name(self):
59        """
60        A well known name of the completely specified state machine.
61
62        The first derived class that completely specifies some state machine
63        should implement this function to return the name of the defining module
64        as a string.
65
66        """
67        # Do not use self._raise_setup_error because it causes infinite
68        # recursion.
69        raise wme.WardModemSetupException(
70                'Attempted to get well known name for a state machine that is '
71                'not completely specified.')
72
73
74    # ##########################################################################
75    # Protected convenience methods to be used as is by subclasses.
76
77    def _respond(self, response, response_delay_ms=0, *response_args):
78        """
79        Respond to the modem after some delay.
80
81        @param reponse: String response. This must be one of the response
82                strings recognized by ATTransceiver.
83
84        @param response_delay_ms: Delay in milliseconds after which the response
85                should be sent. Type: int.
86
87        @param *response_args: The arguments for the response.
88
89        @requires: response_delay_ms >= 0
90
91        """
92        assert response_delay_ms >= 0
93        dbgstr = self._tag_with_name(
94                'Will respond with "%s(%s)" after %d ms.' %
95                (response, str(response_args), response_delay_ms))
96        self._logger.debug(dbgstr)
97        self._task_loop.post_task_after_delay(
98                self._transceiver.process_wardmodem_response,
99                response_delay_ms,
100                response,
101                *response_args)
102
103
104    def _update_state(self, state_update, state_update_delay_ms=0):
105        """
106        Post a (delayed) state update.
107
108        @param state_update: The state update to apply. This is a map {string
109                --> state enum} that specifies all the state components to be
110                updated.
111
112        @param state_update_delay_ms: Delay in milliseconds after which the
113                state update should be applied. Type: int.
114
115        @requires: state_update_delay_ms >= 0
116
117        """
118        assert state_update_delay_ms >= 0
119        dbgstr = self._tag_with_name(
120                '[tag:%d] Will update state as %s after %d ms.' %
121                (self._state_update_tag, str(state_update),
122                 state_update_delay_ms))
123        self._logger.debug(dbgstr)
124        self._task_loop.post_task_after_delay(
125                self._update_state_callback,
126                state_update_delay_ms,
127                state_update,
128                self._state_update_tag)
129        self._state_update_tag += 1
130
131
132    def _respond_ok(self):
133        """ Convenience function to respond when everything is OK. """
134        self._respond(self.wm_response_ok, response_delay_ms=0)
135
136
137    def _respond_error(self):
138        """ Convenience function to respond when an error occured. """
139        self._respond(self.wm_response_error, response_delay_ms=0)
140
141
142    def _respond_ring(self):
143        """ Convenience function to respond with RING. """
144        self._respond(self.wm_response_ring, response_delay_ms=0)
145
146
147    def _respond_with_text(self, text):
148        """ Send back just |text| as the response, without any AT prefix. """
149        self._respond(self.wm_response_text_only, 0, text)
150
151
152    def _add_response_function(self, function):
153        """
154        Add a response used by this state machine to send to the ATTransceiver.
155
156        A state machine should register all the responses it will use in its
157        __init__ function by calling
158            self._add_response_function('wm_response_dummy')
159        The response can then be used to respond to the transceiver thus:
160            self._respond(self.wm_response_dummy)
161
162        @param function: The string function name to add. Must be a valid python
163                identifier in lowercase.
164                Also, these names are conventionally named matching the re
165                'wm_response_([a-z0-9]*[_]?)*'
166
167        @raises: WardModemSetupError if the added response function is ill
168                formed.
169
170        """
171        if not re.match('wm_response_([a-z0-9]*[_]?)*', function) or \
172           keyword.iskeyword(function):
173            self._raise_setup_error('Response function name ill-formed: |%s|' %
174                                    function)
175        try:
176            getattr(self, function)
177            self._raise_setup_error(
178                    'Attempted to add response function %s which already '
179                    'exists.' % function)
180        except AttributeError:  # OK, This is the good case.
181            setattr(self, function, function)
182
183
184    def _raise_setup_error(self, errstring):
185        """
186        Log the error and raise WardModemSetupException.
187
188        @param errstring: The error string.
189
190        """
191        errstring = self._tag_with_name(errstring)
192        self._logger.error(errstring)
193        raise wme.WardModemSetupException(errstring)
194
195
196    def _raise_runtime_error(self, errstring):
197        """
198        Log the error and raise StateMachineException.
199
200        @param errstring: The error string.
201
202        """
203        errstring = self._tag_with_name(errstring)
204        self._logger.error(errstring)
205        raise wme.StateMachineException(errstring)
206
207    def _tag_with_name(self, log_string):
208        """
209        If possible, prepend the log string with the well know name of the
210        object.
211
212        @param log_string: The string to modify.
213
214        @return: The modified string.
215
216        """
217        name = self.get_well_known_name()
218        log_string = '[' + name + '] ' + log_string
219        return log_string
220
221    # ##########################################################################
222    # Private methods not to be used by subclasses.
223
224    def _update_state_callback(self, state_update, tag):
225        """
226        Actually update the state.
227
228        @param state_update: The state update to effect. This is a map {string
229                --> state enum} that specifies all the state components to be
230                updated.
231
232        @param tag: The tag for this state update.
233
234        @raises: StateMachineException if the state update fails.
235
236        """
237        dbgstr = self._tag_with_name('[tag:%d] State update applied.' % tag)
238        self._logger.debug(dbgstr)
239        for component, value in state_update.iteritems():
240            self._state[component] = value
241