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