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