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 collections
6import inspect
7import logging
8
9import at_channel
10import task_loop
11import wardmodem_exceptions as wme
12
13MODEM_RESPONSE_TIMEOUT_MILLISECONDS = 30000
14ARG_PLACEHOLDER =  '*'
15
16class ATTransceiverMode(object):
17    """
18    Enum to specify what mode the ATTransceiver is operating in.
19
20    There are three modes. These modes determine how the commands to/from
21    the modemmanager are routed.
22        WARDMODEM:  modemmanager interacts with wardmodem alone.
23        SPLIT_VERIFY: modemmanager commands are sent to both the wardmodem
24                and the physical modem on the device. Responses from
25                wardmodem are verified against responses from the physical
26                modem. In case of a mismatch, wardmodem's response is
27                chosen, and a warning is issued.
28        PASS_THROUGH: modemmanager commands are routed to/from the physical
29                modem. Frankly, wardmodem isn't running in this mode.
30
31    """
32    WARDMODEM = 0
33    SPLIT_VERIFY = 1
34    PASS_THROUGH = 2
35
36    MODE_NAME = {
37            WARDMODEM: 'WARDMODEM',
38            SPLIT_VERIFY: 'SPLIT_VERIFY',
39            PASS_THROUGH: 'PASS_THROUGH'
40    }
41
42
43    @classmethod
44    def to_string(cls, value):
45        """
46        A class method to obtain string representation of the enum values.
47
48        @param value: the enum value to stringify.
49
50        """
51        return "%s.%s" % (cls.__name__, cls.MODE_NAME[value])
52
53
54class ATTransceiver(object):
55    """
56    A world facing multiplexer class that orchestrates the communication between
57    modem manager, the physical modem, and wardmodem back-end.
58
59    """
60
61    def __init__(self, mm_at_port, modem_conf,
62                 modem_at_port=None):
63        """
64        @param mm_at_port: File descriptor for AT port used by modem manager.
65                Can not be None.
66
67        @param modem_conf: A ModemConfiguration object containing the
68                configuration data for the current modem.
69
70        @param modem_at_port: File descriptor for AT port used by the modem. May
71                be None, but that forces ATTransceiverMode.WARDMODEM. Default:
72                None.
73
74        """
75        super(ATTransceiver, self).__init__()
76        assert mm_at_port is not None
77
78        self._logger = logging.getLogger(__name__)
79        self._task_loop = task_loop.get_instance()
80        self._mode = ATTransceiverMode.WARDMODEM
81        # The time we wait for any particular response from physical modem.
82        self._modem_response_timeout_milliseconds = (
83                MODEM_RESPONSE_TIMEOUT_MILLISECONDS)
84        # We keep a queue of responses from the wardmodem and physical modem,
85        # so that we can verify they match.
86        self._cached_modem_responses = collections.deque()
87        self._cached_wardmodem_responses = collections.deque()
88
89        # When a wardmodem response has been received but the corresponding
90        # physical modem response hasn't arrived, we post a task to wait for the
91        # response.
92        self._modem_response_wait_task = None
93
94        # We use a map from a set of well known state machine names to actual
95        # objects to dispatch state machine calls. This allows tests to provide
96        # alternative implementations of any state machine to wardmodem.
97        self._state_machines = {}
98
99        # If registered with a non-None machine, the fallback machine is used to
100        # service all AT commands that are not matched with any other machine.
101        self._fallback_state_machine = None
102        self._fallback_machine_function = None
103
104        # Maps an incoming AT command from modemmanager to an internal wardmodem
105        # action.
106        self._at_to_wm_action_map = {}
107        # Maps an internal response from wardmodem to an AT command to be sent
108        # to modemmanager.
109        self._wm_response_to_at_map = {}
110
111        # Load mapping between AT commands and wardmodem actions.
112        self._update_at_to_wm_action_map(modem_conf.base_at_to_wm_action_map)
113        self._update_at_to_wm_action_map(modem_conf.plugin_at_to_wm_action_map)
114        self._update_wm_response_to_at_map(
115                modem_conf.base_wm_response_to_at_map)
116        self._update_wm_response_to_at_map(
117                modem_conf.plugin_wm_response_to_at_map)
118        self._logger.debug('Finished loading AT --> wardmodem configuration.')
119        self._logger.debug(self._at_to_wm_action_map)
120        self._logger.debug('Finished loading wardmodem --> AT configuration.')
121        self._logger.debug(self._wm_response_to_at_map)
122
123        # Initialize channels -- let the session begin.
124        if modem_at_port is not None:
125            self._modem_channel = at_channel.ATChannel(
126                    self._process_modem_at_command,
127                    modem_at_port,
128                    'modem_primary_channel')
129            self._modem_channel.at_prefix = modem_conf.mm_to_modem_at_prefix
130            self._modem_channel.at_suffix = modem_conf.mm_to_modem_at_suffix
131        else:
132            self._modem_channel = None
133
134        self._mm_channel = at_channel.ATChannel(self._process_mm_at_command,
135                                                mm_at_port,
136                                                'mm_primary_channel')
137        self._mm_channel.at_prefix = modem_conf.modem_to_mm_at_prefix
138        self._mm_channel.at_suffix = modem_conf.modem_to_mm_at_suffix
139
140
141    # Verification failure reasons
142    VERIFICATION_FAILED_MISMATCH = 1
143    VERIFICATION_FAILED_TIME_OUT = 2
144
145
146    @property
147    def mode(self):
148        """
149        ATTranscieverMode value. Determines how commands are routed.
150
151        @see ATTransceiverMode
152
153        """
154        return self._mode
155
156
157    @mode.setter
158    def mode(self, value):
159        """
160        Set mode.
161
162        @param value: The value to set. Type: ATTransceiverMode.
163
164        """
165        if value != ATTransceiverMode.WARDMODEM and self._modem_channel is None:
166            self._logger.warning(
167                    'Can not switch to %s mode. No modem port provided.',
168                    ATTransceiverMode.to_string(value))
169            return
170        self._logger.info('Set mode to %s',
171                          ATTransceiverMode.to_string(value))
172        self._mode = value
173
174
175    def get_state_machine(self, well_known_name):
176        """
177        Get the registered state machine for the given well known name.
178
179        @param well_known_name: The name of the desired machine.
180
181        @return: The machine. None if not found.
182
183        """
184        return self._state_machines.get(well_known_name, None)
185
186
187    def register_state_machine(self, state_machine):
188        """
189        Register a new state machine.
190
191        We maintain a map from the well known name of the state machine to the
192        object. Any older object mapped to the same name will be replaced.
193
194        @param state_machine: [StateMachine object] The state machine
195                object to be used to dispatch calls.
196
197        """
198        state_machine_name = state_machine.get_well_known_name()
199        self._state_machines[state_machine_name] = state_machine
200
201
202    def register_fallback_state_machine(self, state_machine_name, function):
203        """
204        Register the fallback state machine to forward AT commands to.
205
206        If this machine is registered, all AT commands for which no matching
207        rule is found will result in the call |state_machine|.|function|(at).
208        where |at| is the actual AT command that could not be matched.
209
210        @param state_machine_name: Well known name of the machine to fallback on
211                if no machine matches an incoming AT command.
212
213        @param function: The function in |state_machine| to call.
214
215        """
216        if state_machine_name not in self._state_machines:
217            self._setup_error('Machine %s, set as fallback, has not been '
218                              'registered. ' % state_machine_name)
219        self._fallback_state_machine = state_machine_name
220        self._fallback_machine_function = function
221
222
223    def process_wardmodem_response(self, response, *args):
224        """
225        Convert responses from the wardmodem into AT commands and send them to
226        modemmanager.
227
228        @param response: wardmodem response to be translated to AT response to
229                the modem manager.
230
231        @param *args: arguments to the wardmodem response.
232
233        @raises: ATTransceiverError if the response can not be translated into
234                an AT command.
235
236        """
237        self._logger.debug('Processing wardmodem response %s%s',
238                           response, str(args) if args else '')
239        if response not in self._wm_response_to_at_map:
240            self._runtime_error('Unknown wardmodem response |%s|' % response)
241        at_response = self._construct_at_response(
242                self._wm_response_to_at_map[response], *args)
243        self._process_wardmodem_at_command(at_response)
244
245    # ##########################################################################
246    # Callbacks -- These are the functions that process events from the
247    # ATChannel or the TaskLoop. These functions are either
248    #   (1) set as callbacks in the ATChannel, or
249    #   (2) called internally to process the AT command to/from the TaskLoop.
250
251    def _process_modem_at_command(self, command):
252        """
253        Callback called by the physical modem channel when an AT response is
254        received.
255
256        @param command: AT command sent by the physical modem.
257
258        """
259        assert self.mode != ATTransceiverMode.WARDMODEM
260        self._logger.debug('Command {modem ==> []}: |%s|', command)
261        if self.mode == ATTransceiverMode.PASS_THROUGH:
262            self._logger.debug('Command {[] ==> mm}: |%s|' , command)
263            self._mm_channel.send(command)
264        else:
265            self._cached_modem_responses.append(command)
266            self._verify_and_send_mm_commands()
267
268
269    def _process_mm_at_command(self, command):
270        """
271        Callback called by the modem manager channel when an AT command is
272        received.
273
274        @param command: AT command sent by modem manager.
275
276        """
277        self._logger.debug('Command {mm ==> []}: |%s|', command)
278        if(self.mode == ATTransceiverMode.PASS_THROUGH or
279           self.mode == ATTransceiverMode.SPLIT_VERIFY):
280            self._logger.debug('Command {[] ==> modem}: |%s|', command)
281            self._modem_channel.send(command)
282        if(self.mode == ATTransceiverMode.WARDMODEM or
283           self.mode == ATTransceiverMode.SPLIT_VERIFY):
284            self._logger.debug('Command {[] ==> wardmodem}: |%s|', command)
285            self._post_wardmodem_request(command)
286
287
288    def _process_wardmodem_at_command(self, command):
289        """
290        Function called to process an AT command response of wardmodem.
291
292        This function is called after the response from the task loop has been
293        converted to an AT command.
294
295        @param command: The AT command response of wardmodem.
296
297        """
298        assert self.mode != ATTransceiverMode.PASS_THROUGH
299        self._logger.debug('Command {wardmodem ==> []: |%s|', command)
300        if self.mode == ATTransceiverMode.WARDMODEM:
301            self._logger.debug('Command {[] ==> mm}: |%s|', command)
302            self._mm_channel.send(command)
303        else:
304            self._cached_wardmodem_responses.append(command)
305            self._verify_and_send_mm_commands()
306
307
308    def _post_wardmodem_request(self, command):
309        """
310        For an AT command, find out the action to be taken on wardmodem and post
311        the action.
312
313        @param command: AT command for which a request must be posted to
314                wardmodem.
315
316        @raises: ATTransceiverException if no valid action exists for the given
317                AT command.
318
319        """
320        action = self._find_wardmodem_action_for_at(command)
321        state_machine_name, function_name, args = action
322        try:
323            state_machine = self._state_machines[state_machine_name]
324        except KeyError:
325            self._runtime_error(
326                    'Malformed action registered for AT command -- Unknown '
327                    'state machine. AT command: |%s|. Action: |%s|' %
328                    (command, action))
329        try:
330            function = getattr(state_machine, function_name)
331        except AttributeError:
332            self._runtime_error(
333                    'Malformed action registered for AT command -- Unkonwn '
334                    'function name. AT command: |%s|. Action: |%s|. Object '
335                    'dictionary: %s.' % (command, action, dir(state_machine)))
336
337        self._task_loop.post_task(
338                self._execute_state_machine_function, command, action, function,
339                *args)
340
341    # ##########################################################################
342    # Helper functions
343
344    def _execute_state_machine_function(self, at_command, action, function,
345                                        *args):
346        """
347        A thin wrapper to execute state_machine.function(args). Instead of
348        posting the call directly, this method is posted for better error
349        reporting in case of failure.
350
351        @param at_command: The AT command for which this function was called.
352
353        @param action: The matching wardmodem action which led to this function
354                call.
355
356        @param function: The function to call.
357
358        @param *args: Arguments to be passed to function.
359
360        """
361        try:
362            function(*args)
363        except TypeError as e:
364            self._logger.error(
365                    'Possible malformed action registered for AT command -- '
366                    'Incorrect arguments. AT command: |%s|. Action: |%s|. '
367                    'Expected function signature: %s. '
368                    'Original error raised: |%s|',
369                    at_command, action, inspect.getargspec(function), str(e))
370            # use 'raise' here to preserve the original backtrace.
371            raise
372
373
374    def _update_at_to_wm_action_map(self, raw_map):
375        """
376        Update the dictionary that maps AT commands and their arguments to the
377        action to be taken by wardmodem.
378
379        The internal map updated is
380            {at_command, {(arg1, arg2, ...), (state_machine_name,
381                                              function,
382                                              (idx1, idx2, ...))}}
383        Here,
384            - at_command [string] is the AT Command received,
385            - (arg1, arg2, ...) [tuple of string] is possibly empty, and
386              specifies the arguments that need to be matched. It may contain
387              the special symbol '*' to mean ignore that argument while
388              matching.
389            - state_machine_name [string] is name of a state machine in the
390              state machine map.
391            - function [string] is a function exported by the state machine
392              mapped to by state_machine_name
393            - (idx1, idx2, ...) [tuple of int] lists the (string) arguments that
394              should be passed on from the AT command to the called function.
395
396        @param raw_map: The raw map from AT command to function read in from the
397                configuration file. For the format of this map, see the comment
398                at the head of a configuration file.
399
400        @raises WardModemSetupException if raw_map was not well-formed, and the
401                update failed. Absolutely no guarantees about the state of the
402                map if the update fails.
403
404        """
405        for atcom in raw_map:
406            try:
407                at, args = self._parse_at_command(atcom)
408            except wme.ATTransceiverException as e:
409                self._setup_error(e.args)
410            action = self._sanitize_wardmodem_action(raw_map[atcom])
411
412            if at not in self._at_to_wm_action_map:
413                self._at_to_wm_action_map[at] = {}
414            if args in self._at_to_wm_action_map[at]:
415                self._logger.debug('Updated at_to_wm_action_map: '
416                                   '|%s(%s): [%s --> %s]|',
417                                   at, args,
418                                   str(self._at_to_wm_action_map[at][args]),
419                                   str(action))
420            else:
421                self._logger.debug('Added to at_to_wm_action_map: |%s(%s): %s|',
422                                   at, args, str(action))
423            self._at_to_wm_action_map[at][args] = action
424
425
426    def _update_wm_response_to_at_map(self, raw_map):
427        """
428        Update the dictionary that maps wardmodem responses to AT commands.
429
430        The internal map updated is of the same form as raw_map:
431          {response_function: at_response}
432        where both response_function and at_response are of type string.
433        at_resposne may contain special placeholder charachters '*'.
434
435        @param raw_map: The map read in from the configuration file.
436
437        """
438        for response_function, at_response in raw_map.iteritems():
439            if response_function in self._wm_response_to_at_map:
440                self._logger.debug(
441                        'Updated wm_response_to_at_map: |%s: [%s --> %s]|',
442                        response_function,
443                        self._wm_response_to_at_map[response_function],
444                        at_response)
445            else:
446                self._logger.debug(
447                        'Added to wm_response_to_at_map: |%s: %s|',
448                        response_function, at_response)
449            self._wm_response_to_at_map[response_function] = at_response
450
451
452    def _sanitize_wardmodem_action(self, action):
453        """
454        Test that the action specified in the AT command --> wardmodem action
455        map is sane and normalize to simplify handling later.
456
457        Currently, this only checks that the action consists of tuples of the
458        right size / type. It might make sense to make this check a lot stricter
459        so that ill-formed configuration files are caught early.
460
461        Returns the normalized form: 3-tuple with the last item being a tuple of
462        integers.
463
464        @param action: The action tuple to check.
465
466        @return action: Sanitized action tuple. Normalized form is (string,
467        string, (int*)).
468
469        @raises: WardModemSetupException if action is ill-formed.
470
471        """
472        errstr = ('Ill formed action |%s|. Action must be of the form: '
473                  '(state_machine_name, function_name, (index_tuple)) '
474                  'Here, index_tuple is a tuple of integers.' % str(action))
475        sanitized_action = []
476        if type(action) is not tuple:
477            self._setup_error(errstr)
478        if len(action) != 2 and len(action) != 3:
479            self._setup_error(errstr)
480        if type(action[0]) != str or type(action[1]) != str:
481            self._setup_error(errstr)
482        sanitized_action.append(action[0])
483        sanitized_action.append(action[1])
484        if len(action) != 3:
485            sanitized_action.append(())
486        else:
487            if type(action[2]) == tuple:
488                for idx in action[2]:
489                    if type(idx) != int:
490                        self._setup_error(errstr)
491                sanitized_action.append(action[2])
492            else:
493                if type(action[2]) != int:
494                    self._setup_error(errstr)
495                sanitized_action.append((action[2],))
496        return tuple(sanitized_action)
497
498
499    def _parse_at_command(self, atcom):
500        """
501        Parse an AT command into the command and its arguments
502
503        Examples:
504        'AT?' --> ('AT?', ())
505        'AT+XX' --> ('AT+XX', ())
506        'AT%SCF=1,2' --> ('AT%SCF=', ('1', '2'))
507        'ATX=*' --> ('ATX=', ('*',))
508
509        @param atcom: [string] the AT command to parse
510
511        @return: [(string, (string))] A tuple of the AT command proper and a
512        tuple of arguments. If no arguments are present, an empty argument
513        tuple is included.
514
515        @raises ATTransceiverError if atcom is not well-formed.
516
517        """
518        parts = atcom.split('=')
519        if len(parts) > 2:
520            self._runtime_error('Parsing error: |%s|' % atcom)
521        if len(parts) == 1:
522            return (atcom, ())
523        # Note: Include the trailing '=' in the AT commmand.
524        at = parts[0] + '='
525        if parts[1] == '':
526            # This was a command of the form 'ATXXX='.
527            # Treat this as having no arguments, instead of a single ''
528            # argument.
529            return (at, ())
530        else:
531            return (at, tuple(parts[1].split(',')))
532
533
534    def _find_wardmodem_action_for_at(self, atcom):
535        """
536        For the given AT command, find the appropriate action from wardmodem.
537        This will attempt to find a rule matching |atcom|. If that fails, and if
538        |_fallback_state_machine| exists, the default action from this machine
539        is returned.
540
541        @param atcom: The AT command to find action for. Type: str.
542
543        @return: Returns the tuple of (state_machine_name, function,
544                (arguments,)) for the corresponding action. The action to be
545                taken is roughly
546                    state_machine.function(arguments)
547                Type: (string, string, (string,))
548
549        @raises: ATTransceiverException if the at command is ill-formed or we
550                don't have a corresponding action.
551
552        """
553        try:
554            at, args = self._parse_at_command(atcom)
555        except wme.ATTransceiverException as e:
556            self._runtime_error(
557                    'Ill formed AT command received. %s' % str(e.args))
558        if at not in self._at_to_wm_action_map:
559            if self._fallback_state_machine:
560                return (self._fallback_state_machine,
561                        self._fallback_machine_function,
562                        (atcom,))
563            self._runtime_error('Unknown AT command: |%s|' % atcom)
564
565        for candidate_args in self._at_to_wm_action_map[at]:
566            candidate_action = self._at_to_wm_action_map[at][candidate_args]
567            if self._args_match(args, candidate_args):
568                # Found corresponding entry, now replace the indices of the
569                # arguments in the action with actual arguments.
570                machine, function, idxs = candidate_action
571                fargs = []
572                for idx in idxs:
573                    fargs.append(args[idx])
574                return machine, function, tuple(fargs)
575
576        if self._fallback_state_machine:
577            return (self._fallback_state_machine,
578                    self._fallback_machine_function,
579                    (atcom,))
580        self._runtime_error('Unhandled arguments: |%s|' % atcom)
581
582
583    def _args_match(self, args, matches):
584        """
585        Check whether args are captured by regexp.
586
587        @param args: A tuple of strings, the arguments to check for inclusion.
588
589        @param matches: A similar tuple, but may contain the wild-card '*'.
590
591        @return True if args is represented by regexp, False otherwise.
592
593        """
594        if len(args) != len(matches):
595            return False
596        for i in range(len(args)):
597            arg = args[i]
598            match = matches[i]
599            if match == ARG_PLACEHOLDER:
600                return True
601            if arg != match:
602                return False
603        return True
604
605    def _construct_at_response(self, raw_at, *args):
606        """
607        Replace palceholders in an AT command template with actual arguments.
608
609        @param raw_at: An AT command with '*' placeholders where arguments
610                should be provided.
611
612        @param *args: Arguments to fill in the placeholders in |raw_at|.
613
614        @return: AT command with placeholders replaced by arguments.
615
616        @raises: ATTransceiverException if the number of arguments does not
617                match the number of placeholders.
618
619        """
620        parts = raw_at.split(ARG_PLACEHOLDER)
621        if len(args) < (len(parts) - 1):
622            self._runtime_error(
623                    'Failed to construct AT response from |%s|. Expected %d '
624                    'arguments, found %d.' %
625                    (raw_at, len(parts) - 1, len(args)))
626        if len(args) > (len(parts) - 1):
627            self._logger.warning(
628                    'Number of arguments in wardmodem response greater than '
629                    'expected. Some of the arguments from %s will not be used '
630                    'in the reconstruction of %s', str(args), raw_at)
631
632        ret = []
633        for i in range(len(parts) - 1):
634            ret += parts[i]
635            ret += str(args[i])
636        ret += parts[len(parts) - 1]
637        return ''.join(ret)
638
639
640    def _verify_and_send_mm_commands(self):
641        """
642        While there are corresponding responses from wardmodem and physical
643        modem, verify that they match and respond to modem manager.
644
645        """
646        if not self._cached_wardmodem_responses:
647            return
648        elif not self._cached_modem_responses:
649            if self._modem_response_wait_task is not None:
650                return
651            self._modem_response_wait_task = (
652                    self._task_loop.post_task_after_delay(
653                            self._modem_response_timed_out,
654                            self._modem_response_timeout_milliseconds))
655        else:
656            if self._modem_response_wait_task is not None:
657                self._task_loop.cancel_posted_task(
658                        self._modem_response_wait_task)
659                self._modem_response_wait_task = None
660            self._verify_and_send_mm_command(
661                    self._cached_modem_responses.popleft(),
662                    self._cached_wardmodem_responses.popleft())
663            self._verify_and_send_mm_commands()
664
665
666    def _verify_and_send_mm_command(self, modem_response, wardmodem_response):
667        """
668        Verify that the two AT commands match and respond to modem manager.
669
670        @param modem_response: AT command response of the physical modem.
671
672        @param wardmodem_response: AT command response of wardmodem.
673
674        """
675        # TODO(pprabhu) This can not handle unsolicited commands yet.
676        # Unsolicited commands from either of the modems will push the lists out
677        # of sync.
678        if wardmodem_response != modem_response:
679            self._logger.warning('Response verification failed.')
680            self._logger.warning('modem response: |%s|', modem_response)
681            self._logger.warning('wardmodem response: |%s|', wardmodem_response)
682            self._logger.warning('wardmodem response takes precedence.')
683            self._report_verification_failure(
684                    self.VERIFICATION_FAILED_MISMATCH,
685                    modem_response,
686                    wardmodem_response)
687        self._logger.debug('Command {[] ==> mm}: |%s|' , wardmodem_response)
688        self._mm_channel.send(wardmodem_response)
689
690
691    def _modem_response_timed_out(self):
692        """
693        Callback called when we time out waiting for physical modem response for
694        some wardmodem response. Can't do much -- log physical modem failure and
695        forward wardmodem response anyway.
696
697        """
698        assert (not self._cached_modem_responses and
699                self._cached_wardmodem_responses)
700        wardmodem_response = self._cached_wardmodem_responses.popleft()
701        self._logger.warning('modem response timed out. '
702                             'Forwarding wardmodem response |%s| anyway.',
703                             wardmodem_response)
704        self._logger.debug('Command {[] ==> mm}: |%s|' , wardmodem_response)
705        self._report_verification_failure(
706                self.VERIFICATION_FAILED_TIME_OUT,
707                None,
708                wardmodem_response)
709        self._mm_channel.send(wardmodem_response)
710        self._modem_response_wait_task = None
711        self._verify_and_send_mm_commands()
712
713
714    def _report_verification_failure(self, failure, modem_response,
715                                     wardmodem_response):
716        """
717        Failure to verify the wardmodem response will call this non-public
718        method.
719
720        At present, it is only used by unittests to detect failure.
721
722        @param failure: The cause of failure. Must be one of
723                VERIFICATION_FAILED_MISMATCH or VERIFICATION_FAILED_TIME_OUT.
724
725        @param modem_response: The received modem response (if any).
726
727        @param wardmodem_response: The received wardmodem response.
728
729        """
730        pass
731
732
733    def _runtime_error(self, error_message):
734        """
735        Log the message at error level and raise ATTransceiverException.
736
737        @param error_message: The error message.
738
739        @raises: ATTransceiverException.
740
741        """
742        self._logger.error(error_message)
743        raise wme.ATTransceiverException(error_message)
744
745
746    def _setup_error(self, error_message):
747        """
748        Log the message at error level and raise WardModemSetupException.
749
750        @param error_message: The error message.
751
752        @raises: WardModemSetupException.
753
754        """
755        self._logger.error(error_message)
756        raise wme.WardModemSetupException(error_message)
757