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 fcntl 6import glib 7import logging 8import os 9import re 10import termios 11import tty 12 13import task_loop 14 15class 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