Home | History | Annotate | Download | only in wardmodem
      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 
      5 import fcntl
      6 import glib
      7 import logging
      8 import os
      9 import re
     10 import termios
     11 import tty
     12 
     13 import task_loop
     14 
     15 class ATChannel(object):
     16     """
     17     Send a single AT command in either direction asynchronously.
     18 
     19     This class represents the AT command channel. The program can
     20       (1) Request *one* AT command to be sent on the channel.
     21       (2) Get notified of a received AT command.
     22 
     23     """
     24 
     25     CHANNEL_READ_CHUNK_SIZE = 128
     26 
     27     GLIB_CB_CONDITION_STR = {
     28         glib.IO_IN: 'glib.IO_IN',
     29         glib.IO_OUT: 'glib.IO_OUT',
     30         glib.IO_PRI: 'glib.IO_PRI',
     31         glib.IO_ERR: 'glib.IO_ERR',
     32         glib.IO_HUP: 'glib.IO_HUP'
     33     }
     34 
     35     # And exception with error code 11 is raised when a write to some file
     36     # descriptor fails because the channel is full.
     37     IO_ERROR_CHANNEL_FULL = 11
     38 
     39     def __init__(self, receiver_callback, channel, channel_name='',
     40                  at_prefix='', at_suffix='\r\n'):
     41         """
     42         @param receiver_callback: The callback function to be called when an AT
     43                 command is received over the channel. The signature of the
     44                 callback must be
     45 
     46                 def receiver_callback(self, command)
     47 
     48         @param channel: The file descriptor for channel, as returned by e.g.
     49                 os.open().
     50 
     51         @param channel_name: [Optional] Name of the channel to be used for
     52                 logging.
     53 
     54         @param at_prefix: AT commands sent out on this channel will be prefixed
     55                 with |at_prefix|. Default ''.
     56 
     57         @param at_suffix: AT commands sent out on this channel will be
     58                 terminated with |at_suffix|. Default '\r\n'.
     59 
     60         @raises IOError if some file operation on |channel| fails.
     61 
     62         """
     63         super(ATChannel, self).__init__()
     64         assert receiver_callback and channel
     65 
     66         self._receiver_callback = receiver_callback
     67         self._channel = channel
     68         self._channel_name = channel_name
     69         self._at_prefix = at_prefix
     70         self._at_suffix = at_suffix
     71 
     72         self._logger = logging.getLogger(__name__)
     73         self._task_loop = task_loop.get_instance()
     74         self._received_command = ''  # Used to store partially received command.
     75 
     76         flags = fcntl.fcntl(self._channel, fcntl.F_GETFL)
     77         flags = flags | os.O_RDWR | os.O_NONBLOCK
     78         fcntl.fcntl(self._channel, fcntl.F_SETFL, flags)
     79         try:
     80             tty.setraw(self._channel, tty.TCSANOW)
     81         except termios.error as ttyerror:
     82             raise IOError(ttyerror.args)
     83 
     84         # glib does not raise errors, merely prints to stderr.
     85         # If we've come so far, assume channel is well behaved.
     86         self._channel_cb_handler = glib.io_add_watch(
     87                 self._channel,
     88                 glib.IO_IN | glib.IO_PRI | glib.IO_ERR | glib.IO_HUP,
     89                 self._handle_channel_cb,
     90                 priority=glib.PRIORITY_HIGH)
     91 
     92 
     93     @property
     94     def at_prefix(self):
     95         """ The string used to prefix AT commands sent on the channel. """
     96         return self._at_prefix
     97 
     98 
     99     @at_prefix.setter
    100     def at_prefix(self, value):
    101         """
    102         Set the string to use to prefix AT commands.
    103 
    104         This can vary by the modem being used.
    105 
    106         @param value: The string prefix.
    107 
    108         """
    109         self._logger.debug('AT command prefix set to: |%s|', value)
    110         self._at_prefix = value
    111 
    112 
    113     @property
    114     def at_suffix(self):
    115         """ The string used to terminate AT commands sent on the channel. """
    116         return self._at_suffix
    117 
    118 
    119     @at_suffix.setter
    120     def at_suffix(self, value):
    121         """
    122         Set the string to use to terminate AT commands.
    123 
    124         This can vary by the modem being used.
    125 
    126         @param value: The string terminator.
    127 
    128         """
    129         self._logger.debug('AT command suffix set to: |%s|', value)
    130         self._at_suffix = value
    131 
    132 
    133     def __del__(self):
    134         glib.source_remove(self._channel_cb_handler)
    135 
    136 
    137     def send(self, at_command):
    138         """
    139         Send an AT command on the channel.
    140 
    141         @param at_command: The AT command to send.
    142 
    143         @return: True if send was successful, False if send failed because the
    144                 channel was full.
    145 
    146         @raises: OSError if send failed for any reason other than that the
    147                 channel was full.
    148 
    149         """
    150         at_command = self._prepare_for_send(at_command)
    151         try:
    152             os.write(self._channel, at_command)
    153         except OSError as write_error:
    154             if write_error.args[0] == self.IO_ERROR_CHANNEL_FULL:
    155                 self._logger.warning('%s Send Failed: |%s|',
    156                                      self._channel_name, repr(at_command))
    157                 return False
    158             raise write_error
    159 
    160         self._logger.debug('%s Sent: |%s|',
    161                            self._channel_name, repr(at_command))
    162         return True
    163 
    164 
    165     def _process_received_command(self):
    166         """
    167         Process a command from the channel once it has been fully received.
    168 
    169         """
    170         self._logger.debug('%s Received: |%s|',
    171                            self._channel_name, repr(self._received_command))
    172         self._task_loop.post_task(self._receiver_callback,
    173                                   self._received_command)
    174 
    175 
    176     def _handle_channel_cb(self, channel, cb_condition):
    177         """
    178         Callback used by the channel when there is any data to read.
    179 
    180         @param channel: The channel which issued the signal.
    181 
    182         @param cb_condition: one of glib.IO_* conditions that caused the signal.
    183 
    184         @return: True, so as to continue watching the channel for further
    185                 signals.
    186 
    187         """
    188         if channel != self._channel:
    189             self._logger.warning('%s Signal received on unknown channel. '
    190                                  'Expected: |%d|, obtained |%d|. Ignoring.',
    191                                  self._channel_name, self._channel, channel)
    192             return True
    193         if cb_condition == glib.IO_IN or cb_condition == glib.IO_PRI:
    194             self._read_channel()
    195             return True
    196         self._logger.warning('%s Unexpected cb condition %s received. Ignored.',
    197                              self._channel_name,
    198                              self.GLIB_CB_CONDITION_STR[cb_condition])
    199         return True
    200 
    201 
    202     def _read_channel(self):
    203         """
    204         Read data from channel when the channel indicates available data.
    205 
    206         """
    207         incoming_list = []
    208         try:
    209             while True:
    210                 s = os.read(self._channel, self.CHANNEL_READ_CHUNK_SIZE)
    211                 if not s:
    212                     break
    213                 incoming_list.append(s)
    214         except OSError as read_error:
    215             if not read_error.args[0] == self.IO_ERROR_CHANNEL_FULL:
    216                 raise read_error
    217         if not incoming_list:
    218             return
    219         incoming = ''.join(incoming_list)
    220         if not incoming:
    221             return
    222 
    223         # TODO(pprabhu) Currently, we split incoming AT commands on '\r' or
    224         # '\n'. It may be that some modems that expect the terminator sequence
    225         # to be '\r\n' send spurious '\r's on the channel. If so, we must ignore
    226         # spurious '\r' or '\n'.
    227 
    228         # (1) replace ; by \rAT.
    229         # ';' can be used to string together AT commands.
    230         # So
    231         #  AT1;2
    232         # is the same as sending two commands:
    233         #  AT1
    234         #  AT2
    235         incoming = re.sub(';', '\rAT', incoming)
    236 
    237         # (2) Replace any occurence of a terminator with '\r\r'.
    238         # This ensures that splitting at the terminator actually gives us an
    239         # empty part. viz --
    240         #  'some_string\nother_string' --> 'some_string\r\rother_string'
    241         #  --> ['some_string', '', 'other_string']
    242         # We use the empty string generated to detect completed commands.
    243         incoming = re.sub('\r|\n|;', '\r\r', incoming)
    244 
    245         # (3) Split into AT commands.
    246         parts = re.split('\r', incoming)
    247         for part in parts:
    248             if (not part) and self._received_command:
    249                 self._process_received_command()
    250                 self._received_command = ''
    251             elif part:
    252                 self._received_command = self._received_command + part
    253 
    254 
    255     def _prepare_for_send(self, command):
    256         """
    257         Sanitize AT command before sending on channel.
    258 
    259         @param command: The command to sanitize.
    260 
    261         @reutrn: The sanitized command.
    262 
    263         """
    264         command = command.strip()
    265         assert command.find('\r') == -1
    266         assert command.find('\n') == -1
    267         command = self.at_prefix + command + self.at_suffix
    268         return command
    269