• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python2
2
3# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""An implementation of the ModemManager1 DBUS interface.
7
8This modem mimics a GSM (eventually LTE & CDMA) modem and allows a
9user to test shill and UI behaviors when a supported SIM is inserted
10into the device.  Invoked with the proper flags it can test that SMS
11messages are deliver to the UI.
12
13This program creates a virtual network interface to simulate the
14network interface of a modem.  It depends on modemmanager-next to
15set the dbus permissions properly.
16
17TODO:
18   * Use more appropriate values for many of the properties
19   * Support all ModemManager1 interfaces
20   * implement LTE modems
21   * implement CDMA modems
22"""
23
24from optparse import OptionParser
25import logging
26import os
27import signal
28import string
29import subprocess
30import sys
31import time
32
33import dbus
34from dbus.exceptions import DBusException
35import dbus.mainloop.glib
36import dbus.service
37from dbus.types import Int32
38from dbus.types import ObjectPath
39from dbus.types import Struct
40from dbus.types import UInt32
41import glib
42import gobject
43import mm1
44
45
46# Miscellaneous delays to simulate a modem
47DEFAULT_CONNECT_DELAY_MS = 1500
48
49DEFAULT_CARRIER = 'att'
50
51
52class DBusObjectWithProperties(dbus.service.Object):
53    """Implements the org.freedesktop.DBus.Properties interface.
54
55    Implements the org.freedesktop.DBus.Properties interface, specifically
56    the Get and GetAll methods.  Class which inherit from this class must
57    implement the InterfacesAndProperties function which will return a
58    dictionary of all interfaces and the properties defined on those interfaces.
59    """
60
61    def __init__(self, bus, path):
62        dbus.service.Object.__init__(self, bus, path)
63
64    @dbus.service.method(dbus.PROPERTIES_IFACE,
65                         in_signature='ss', out_signature='v')
66    def Get(self, interface, property_name, *args, **kwargs):
67        """Returns: The value of property_name on interface."""
68        logging.info('%s: Get %s, %s', self.path, interface, property_name)
69        interfaces = self.InterfacesAndProperties()
70        properties = interfaces.get(interface, None)
71        if property_name in properties:
72            return properties[property_name]
73        raise dbus.exceptions.DBusException(
74            mm1.MODEM_MANAGER_INTERFACE + '.UnknownProperty',
75            'Property %s not defined for interface %s' %
76            (property_name, interface))
77
78    @dbus.service.method(dbus.PROPERTIES_IFACE,
79                         in_signature='s', out_signature='a{sv}')
80    def GetAll(self, interface, *args, **kwargs):
81        """Returns: A dictionary. The properties on interface."""
82        logging.info('%s: GetAll %s', self.path, interface)
83        interfaces = self.InterfacesAndProperties()
84        properties = interfaces.get(interface, None)
85        if properties is not None:
86            return properties
87        raise dbus.exceptions.DBusException(
88            mm1.MODEM_MANAGER_INTERFACE + '.UnknownInterface',
89            'Object does not implement the %s interface' % interface)
90
91    def InterfacesAndProperties(self):
92        """Subclasses must implement this function.
93
94        Returns:
95            A dictionary of interfaces where the values are dictionaries
96            of dbus properties.
97        """
98        pass
99
100
101class SIM(DBusObjectWithProperties):
102    """SIM Object.
103
104       Mock SIM Card and the typical information it might contain.
105       SIM cards of different carriers can be created by providing
106       the MCC, MNC, operator name, imsi, and msin.  SIM objects are
107       passed to the Modem during Modem initialization.
108    """
109
110    DEFAULT_MCC = '310'
111    DEFAULT_MNC = '090'
112    DEFAULT_OPERATOR = 'AT&T'
113    DEFAULT_MSIN = '1234567890'
114    DEFAULT_IMSI = '888999111'
115    MCC_LIST = {
116        'us': '310',
117        'de': '262',
118        'es': '214',
119        'fr': '208',
120        'gb': '234',
121        'it': '222',
122        'nl': '204',
123    }
124    CARRIERS = {
125        'att': ('us', '090', 'AT&T'),
126        'tmobile': ('us', '026', 'T-Mobile'),
127        'simyo': ('de', '03', 'simyo'),
128        'movistar': ('es', '07', 'Movistar'),
129        'sfr': ('fr', '10', 'SFR'),
130        'three': ('gb', '20', '3'),
131        'threeita': ('it', '99', '3ITA'),
132        'kpn': ('nl', '08', 'KPN')
133        }
134
135    def __init__(self,
136                 manager,
137                 mcc_country='us',
138                 mnc=DEFAULT_MNC,
139                 operator_name=DEFAULT_OPERATOR,
140                 msin=DEFAULT_MSIN,
141                 imsi=None,
142                 mcc=None,
143                 name='/Sim/0'):
144        self.manager = manager
145        self.name = name
146        self.path = manager.path + name
147        self.mcc = mcc or SIM.MCC_LIST.get(mcc_country, '000')
148        self.mnc = mnc
149        self.operator_name = operator_name
150        self.msin = msin
151        self.imsi = imsi or (self.mcc + self.mnc + SIM.DEFAULT_IMSI)
152        DBusObjectWithProperties.__init__(self, manager.bus, self.path)
153
154    @staticmethod
155    def FromCarrier(carrier, manager):
156        """Creates a SIM card object for a given carrier."""
157        args = SIM.CARRIERS.get(carrier, [])
158        return SIM(manager, *args)
159
160    def Properties(self):
161        return {
162            'SimIdentifier': self.msin,
163            'Imsi': self.imsi,
164            'OperatorIdentifier': self.mcc + self.mnc,
165            'OperatorName': self.operator_name
166            }
167
168    def InterfacesAndProperties(self):
169        return {mm1.SIM_INTERFACE: self.Properties()}
170
171class SMS(DBusObjectWithProperties):
172    """SMS Object.
173
174       Mock SMS message.
175    """
176
177    def __init__(self, manager, name='/SMS/0', text='test',
178                 number='123', timestamp='12:00', smsc=''):
179        self.manager = manager
180        self.name = name
181        self.path = manager.path + name
182        self.text = text or 'test sms at %s' % name
183        self.number = number
184        self.timestamp = timestamp
185        self.smsc = smsc
186        DBusObjectWithProperties.__init__(self, manager.bus, self.path)
187
188    def Properties(self):
189        # TODO(jglasgow): State, Validity, Class, Storage are also defined
190        return {
191            'Text': self.text,
192            'Number': self.number,
193            'Timestamp': self.timestamp,
194            'SMSC': self.smsc
195            }
196
197    def InterfacesAndProperties(self):
198        return {mm1.SMS_INTERFACE: self.Properties()}
199
200
201class PseudoNetworkInterface(object):
202    """A Pseudo network interface.
203
204    This uses a pair of network interfaces and dnsmasq to simulate the
205    network device normally associated with a modem.
206    """
207
208    # Any interface that shill manages will get its own routing
209    # table. Routes added to the main routing table with RTPROT_BOOT (the
210    # default proto value) will be sent to the corresponding interface's
211    # routing table. We want to prevent that in this case, so we use
212    # proto 5, as shill currently ignores proto values greater than 4.
213    ROUTE_PROTO = 'proto 5'
214
215    def __init__(self, interface, base):
216        self.interface = interface
217        self.peer = self.interface + 'p'
218        self.base = base
219        self.lease_file = '/tmp/dnsmasq.%s.leases' % self.interface
220        self.dnsmasq = None
221
222    def __enter__(self):
223        """Make usable with "with" statement."""
224        self.CreateInterface()
225        return self
226
227    def __exit__(self, exception, value, traceback):
228        """Make usable with "with" statement."""
229        self.DestroyInterface()
230        return False
231
232    def CreateInterface(self):
233        """Creates a virtual interface.
234
235        Creates the virtual interface self.interface as well as a peer
236        interface.  Runs dnsmasq on the peer interface so that a DHCP
237        service can offer ip addresses to the virtual interface.
238        """
239        os.system('ip link add name %s type veth peer name %s' % (
240            self.interface, self.peer))
241
242        os.system('ifconfig %s %s.1/24' % (self.peer, self.base))
243        os.system('ifconfig %s up' % self.peer)
244
245        os.system('ifconfig %s up' % self.interface)
246        os.system('ip route add 255.255.255.255 dev %s %s' %
247                  (self.peer, self.ROUTE_PROTO))
248        os.close(os.open(self.lease_file, os.O_CREAT | os.O_TRUNC))
249        self.dnsmasq = subprocess.Popen(
250            ['/usr/local/sbin/dnsmasq',
251             '--pid-file',
252             '-k',
253             '--dhcp-leasefile=%s' % self.lease_file,
254             '--dhcp-range=%s.2,%s.254' % (self.base, self.base),
255             '--port=0',
256             '--interface=%s' % self.peer,
257             '--bind-interfaces'
258            ])
259        # iptables default policy is to reject packets. Add ACCEPT as the
260        # target for the virtual and peer interfaces. Note that this currently
261        # only accepts v4 traffic.
262        os.system('iptables -I INPUT -i %s -j ACCEPT' % self.peer)
263        os.system('iptables -I INPUT -i %s -j ACCEPT' % self.interface)
264
265    def DestroyInterface(self):
266        """Destroys the virtual interface.
267
268        Stops dnsmasq and cleans up all on disk state.
269        """
270        if self.dnsmasq:
271            self.dnsmasq.terminate()
272        try:
273            os.system('ip route del 255.255.255.255 %s' % self.ROUTE_PROTO)
274        except:
275            pass
276        try:
277            os.system('ip link del %s' % self.interface)
278        except:
279            pass
280        os.system('iptables -D INPUT -i %s -j ACCEPT' % self.peer)
281        os.system('iptables -D INPUT -i %s -j ACCEPT' % self.interface)
282        if os.path.exists(self.lease_file):
283            os.remove(self.lease_file)
284
285
286class Modem(DBusObjectWithProperties):
287    """A Modem object that implements the ModemManager DBUS API."""
288
289    def __init__(self, manager, name='/Modem/0',
290                 device='pseudomodem0',
291                 mdn='0000001234',
292                 meid='A100000DCE2CA0',
293                 carrier='CrCarrier',
294                 esn='EDD1EDD1',
295                 sim=None):
296        """Instantiates a Modem with some options.
297
298        Args:
299            manager: a ModemManager object.
300            name: string, a dbus path name.
301            device: string, the network device to use.
302            mdn: string, the mobile directory number.
303            meid: string, the mobile equipment id (CDMA only?).
304            carrier: string, the name of the carrier.
305            esn: string, the electronic serial number.
306            sim: a SIM object.
307        """
308        self.state = mm1.MM_MODEM_STATE_DISABLED
309        self.manager = manager
310        self.name = name
311        self.path = manager.path + name
312        self.device = device
313        self.mdn = mdn
314        self.meid = meid
315        self.carrier = carrier
316        self.operator_name = carrier
317        self.operator_code = '123'
318        self.esn = esn
319        self.registration_state = mm1.MM_MODEM_3GPP_REGISTRATION_STATE_IDLE
320        self.sim = sim
321        DBusObjectWithProperties.__init__(self, manager.bus, self.path)
322        self.pseudo_interface = PseudoNetworkInterface(self.device, '192.168.7')
323        self.smses = {}
324
325    def __enter__(self):
326        """Make usable with "with" statement."""
327        self.pseudo_interface.__enter__()
328        # Add the device to the manager only after the pseudo
329        # interface has been created.
330        self.manager.Add(self)
331        return self
332
333    def __exit__(self, exception, value, traceback):
334        """Make usable with "with" statement."""
335        self.manager.Remove(self)
336        return self.pseudo_interface.__exit__(exception, value, traceback)
337
338    def DiscardModem(self):
339        """Discard this DBUS Object.
340
341        Send a message that a modem has disappeared and deregister from DBUS.
342        """
343        logging.info('DiscardModem')
344        self.remove_from_connection()
345        self.manager.Remove(self)
346
347    def ModemProperties(self):
348        """Return the properties of the modem object."""
349        properties = {
350            # 'Sim': type='o'
351            'ModemCapabilities': UInt32(0),
352            'CurrentCapabilities': UInt32(0),
353            'MaxBearers': UInt32(2),
354            'MaxActiveBearers': UInt32(2),
355            'Manufacturer': 'Foo Electronics',
356            'Model': 'Super Foo Modem',
357            'Revision': '1.0',
358            'DeviceIdentifier': '123456789',
359            'Device': self.device,
360            'Driver': 'fake',
361            'Plugin': 'Foo Plugin',
362            'EquipmentIdentifier': self.meid,
363            'UnlockRequired': UInt32(0),
364            #'UnlockRetries' type='a{uu}'
365            mm1.MM_MODEM_PROPERTY_STATE: Int32(self.state),
366            'AccessTechnologies': UInt32(self.state),
367            'SignalQuality': Struct([UInt32(90), True], signature='ub'),
368            'OwnNumbers': ['6175551212'],
369            'SupportedModes': UInt32(0),
370            'AllowedModes': UInt32(0),
371            'PreferredMode': UInt32(0),
372            'SupportedBands': [UInt32(0)],
373            'Bands': [UInt32(0)]
374            }
375        if self.sim:
376            properties['Sim'] = ObjectPath(self.sim.path)
377        return properties
378
379    def InterfacesAndProperties(self):
380        """Return all supported interfaces and their properties."""
381        return {
382            mm1.MODEM_INTERFACE: self.ModemProperties(),
383            }
384
385    def ChangeState(self, new_state,
386                    why=mm1.MM_MODEM_STATE_CHANGE_REASON_UNKNOWN):
387        logging.info('Change state from %s to %s', self.state, new_state)
388        self.StateChanged(Int32(self.state), Int32(new_state), UInt32(why))
389        self.PropertiesChanged(mm1.MODEM_INTERFACE,
390                               {mm1.MM_MODEM_PROPERTY_STATE: Int32(new_state)},
391                               [])
392        self.state = new_state
393
394    @dbus.service.method(mm1.MODEM_INTERFACE,
395                         in_signature='b', out_signature='')
396    def Enable(self, on, *args, **kwargs):
397        """Enables the Modem."""
398        logging.info('Modem: Enable %s', str(on))
399        if on:
400            if self.state <= mm1.MM_MODEM_STATE_ENABLING:
401                self.ChangeState(mm1.MM_MODEM_STATE_ENABLING)
402            if self.state <= mm1.MM_MODEM_STATE_ENABLED:
403                self.ChangeState(mm1.MM_MODEM_STATE_ENABLED)
404            if self.state <= mm1.MM_MODEM_STATE_SEARCHING:
405                self.ChangeState(mm1.MM_MODEM_STATE_SEARCHING)
406            glib.timeout_add(250, self.OnRegistered)
407        else:
408            if self.state >= mm1.MM_MODEM_STATE_DISABLING:
409                self.ChangeState(mm1.MM_MODEM_STATE_DISABLING)
410            if self.state >= mm1.MM_MODEM_STATE_DISABLED:
411                self.ChangeState(mm1.MM_MODEM_STATE_DISABLED)
412                self.ChangeRegistrationState(
413                    mm1.MM_MODEM_3GPP_REGISTRATION_STATE_IDLE)
414        return None
415
416    def ChangeRegistrationState(self, new_state):
417        """Updates the registration state of the modem.
418
419        Updates the registration state of the modem and broadcasts a
420        DBUS signal.
421
422        Args:
423          new_state: the new registation state of the modem.
424        """
425        if new_state != self.registration_state:
426            self.registration_state = new_state
427            self.PropertiesChanged(
428                mm1.MODEM_MODEM3GPP_INTERFACE,
429                {mm1.MM_MODEM3GPP_PROPERTY_REGISTRATION_STATE:
430                     UInt32(new_state)},
431                [])
432
433    def OnRegistered(self):
434        """Called when the Modem is Registered."""
435        if (self.state >= mm1.MM_MODEM_STATE_ENABLED and
436            self.state <= mm1.MM_MODEM_STATE_REGISTERED):
437            logging.info('Modem: Marking Registered')
438            self.ChangeRegistrationState(
439                mm1.MM_MODEM_3GPP_REGISTRATION_STATE_HOME)
440            self.ChangeState(mm1.MM_MODEM_STATE_REGISTERED)
441
442    @dbus.service.method(mm1.MODEM_SIMPLE_INTERFACE, in_signature='',
443                         out_signature='a{sv}')
444    def GetStatus(self, *args, **kwargs):
445        """Gets the general modem status.
446
447        Returns:
448            A dictionary of properties.
449        """
450        logging.info('Modem: GetStatus')
451        properties = {
452            'state': UInt32(self.state),
453            'signal-quality': UInt32(99),
454            'bands': self.carrier,
455            'access-technology': UInt32(0),
456            'm3gpp-registration-state': UInt32(self.registration_state),
457            'm3gpp-operator-code': '123',
458            'm3gpp-operator-name': '123',
459            'cdma-cdma1x-registration-state': UInt32(99),
460            'cdma-evdo-registration-state': UInt32(99),
461            'cdma-sid': '123',
462            'cdma-nid': '123',
463            }
464        if self.state >= mm1.MM_MODEM_STATE_ENABLED:
465            properties['carrier'] = 'Test Network'
466        return properties
467
468    @dbus.service.signal(mm1.MODEM_INTERFACE, signature='iiu')
469    def StateChanged(self, old_state, new_state, why):
470        pass
471
472    @dbus.service.method(mm1.MODEM_SIMPLE_INTERFACE, in_signature='a{sv}',
473                         out_signature='o',
474                         async_callbacks=('return_cb', 'raise_cb'))
475    def Connect(self, unused_props, return_cb, raise_cb, **kwargs):
476        """Connect the modem to the network.
477
478        Args:
479            unused_props: connection properties. See ModemManager documentation.
480            return_cb: function to call to return result asynchronously.
481            raise_cb: function to call to raise an error asynchronously.
482        """
483
484        def ConnectDone(new, why):
485            logging.info('Modem: ConnectDone %s -> %s because %s',
486                         str(self.state), str(new), str(why))
487            if self.state == mm1.MM_MODEM_STATE_CONNECTING:
488                self.ChangeState(new, why)
489            # TODO(jglasgow): implement a bearer object
490                bearer_path = '/Bearer/0'
491                return_cb(bearer_path)
492            else:
493                raise_cb(mm1.ConnectionUnknownError())
494
495        logging.info('Modem: Connect')
496        if self.state != mm1.MM_MODEM_STATE_REGISTERED:
497            logging.info(
498                'Modem: Connect fails on unregistered modem.  State = %s',
499                self.state)
500            raise mm1.NoNetworkError()
501        delay_ms = kwargs.get('connect_delay_ms', DEFAULT_CONNECT_DELAY_MS)
502        time.sleep(delay_ms / 1000.0)
503        self.ChangeState(mm1.MM_MODEM_STATE_CONNECTING)
504        glib.timeout_add(50, lambda: ConnectDone(
505            mm1.MM_MODEM_STATE_CONNECTED,
506            mm1.MM_MODEM_STATE_CHANGE_REASON_USER_REQUESTED))
507
508    @dbus.service.method(mm1.MODEM_SIMPLE_INTERFACE, in_signature='o',
509                         async_callbacks=('return_cb', 'raise_cb'))
510    def Disconnect(self, bearer, return_cb, raise_cb, **kwargs):
511        """Disconnect the modem from the network."""
512
513        def DisconnectDone(old, new, why):
514            logging.info('Modem: DisconnectDone %s -> %s because %s',
515                         str(old), str(new), str(why))
516            if self.state == mm1.MM_MODEM_STATE_DISCONNECTING:
517                logging.info('Modem: State is DISCONNECTING, changing to %s',
518                             str(new))
519                self.ChangeState(new)
520                return_cb()
521            elif self.state == mm1.MM_MODEM_STATE_DISABLED:
522                logging.info('Modem: State is DISABLED, not changing state')
523                return_cb()
524            else:
525                raise_cb(mm1.ConnectionUnknownError())
526
527        logging.info('Modem: Disconnect')
528        self.ChangeState(mm1.MM_MODEM_STATE_DISCONNECTING)
529        glib.timeout_add(
530            500,
531            lambda: DisconnectDone(
532                self.state,
533                mm1.MM_MODEM_STATE_REGISTERED,
534                mm1.MM_MODEM_STATE_CHANGE_REASON_USER_REQUESTED))
535
536    @dbus.service.signal(dbus.PROPERTIES_IFACE, signature='sa{sv}as')
537    def PropertiesChanged(self, interface, changed_properties,
538                          invalidated_properties):
539        pass
540
541    def AddSMS(self, sms):
542        logging.info('Adding SMS %s to list', sms.path)
543        self.smses[sms.path] = sms
544        self.Added(self.path, True)
545
546    @dbus.service.method(mm1.MODEM_MESSAGING_INTERFACE, in_signature='',
547                         out_signature='ao')
548    def List(self, *args, **kwargs):
549        logging.info('Modem.Messaging: List: %s',
550                     ', '.join(self.smses.keys()))
551        return self.smses.keys()
552
553    @dbus.service.method(mm1.MODEM_MESSAGING_INTERFACE, in_signature='o',
554                         out_signature='')
555    def Delete(self, sms_path, *args, **kwargs):
556        logging.info('Modem.Messaging: Delete %s', sms_path)
557        del self.smses[sms_path]
558
559    @dbus.service.signal(mm1.MODEM_MESSAGING_INTERFACE, signature='ob')
560    def Added(self, sms_path, complete):
561        pass
562
563
564class GSMModem(Modem):
565    """A GSMModem implements the mm1.MODEM_MODEM3GPP_INTERFACE interface."""
566
567    def __init__(self, manager, imei='00112342342', **kwargs):
568        self.imei = imei
569        Modem.__init__(self, manager, **kwargs)
570
571    @dbus.service.method(mm1.MODEM_MODEM3GPP_INTERFACE,
572                         in_signature='s', out_signature='')
573    def Register(self, operator_id, *args, **kwargs):
574        """Register the modem on the network."""
575        pass
576
577    def Modem3GPPProperties(self):
578        """Return the 3GPP Properties of the modem object."""
579        return {
580            'Imei': self.imei,
581            mm1.MM_MODEM3GPP_PROPERTY_REGISTRATION_STATE:
582                UInt32(self.registration_state),
583            'OperatorCode': self.operator_code,
584            'OperatorName': self.operator_name,
585            'EnabledFacilityLocks': UInt32(0)
586            }
587
588    def InterfacesAndProperties(self):
589        """Return all supported interfaces and their properties."""
590        return {
591            mm1.MODEM_INTERFACE: self.ModemProperties(),
592            mm1.MODEM_MODEM3GPP_INTERFACE: self.Modem3GPPProperties()
593            }
594
595    @dbus.service.method(mm1.MODEM_MODEM3GPP_INTERFACE, in_signature='',
596                         out_signature='aa{sv}')
597    def Scan(self, *args, **kwargs):
598        """Scan for networks."""
599        raise mm1.CoreUnsupportedError()
600
601
602class ModemManager(dbus.service.Object):
603    """Implements the org.freedesktop.DBus.ObjectManager interface."""
604
605    def __init__(self, bus, path):
606        self.devices = []
607        self.bus = bus
608        self.path = path
609        dbus.service.Object.__init__(self, bus, path)
610
611    def Add(self, device):
612        """Adds a modem device to the list of devices that are managed."""
613        logging.info('ModemManager: add %s', device.name)
614        self.devices.append(device)
615        interfaces = device.InterfacesAndProperties()
616        logging.info('Add: %s', interfaces)
617        self.InterfacesAdded(device.path, interfaces)
618
619    def Remove(self, device):
620        """Removes a modem device from the list of managed devices."""
621        logging.info('ModemManager: remove %s', device.name)
622        self.devices.remove(device)
623        interfaces = device.InterfacesAndProperties().keys()
624        self.InterfacesRemoved(device.path, interfaces)
625
626    @dbus.service.method(mm1.OFDOM, out_signature='a{oa{sa{sv}}}')
627    def GetManagedObjects(self):
628        """Returns the list of managed objects and their properties."""
629        results = {}
630        for device in self.devices:
631            results[device.path] = device.InterfacesAndProperties()
632        logging.info('GetManagedObjects: %s', ', '.join(results.keys()))
633        return results
634
635    @dbus.service.signal(mm1.OFDOM, signature='oa{sa{sv}}')
636    def InterfacesAdded(self, object_path, interfaces_and_properties):
637        pass
638
639    @dbus.service.signal(mm1.OFDOM, signature='oas')
640    def InterfacesRemoved(self, object_path, interfaces):
641        pass
642
643
644def main():
645    usage = """
646Run pseudo_modem to simulate a GSM modem using the modemmanager-next
647DBUS interfaces.  This can be used for the following:
648  - to simpilify the verification process of UI features that use of
649    overseas SIM cards
650  - to test shill on a virtual machine without a physical modem
651  - to test that Chrome property displays SMS messages
652
653To use on a test image you use test_that to run
654network_3GModemControl which will cause pseudo_modem.py to be
655installed in /usr/local/autotests/cros/cellular.  Then stop
656modemmanager and start the pseudo modem with the commands:
657
658  stop modemmanager
659  /usr/local/autotest/cros/cellular/pseudo_modem.py
660
661When done, use Control-C to stop the process and restart modem manager:
662  start modemmanager
663
664Additional help documentation is available by invoking pseudo_modem.py
665--help.
666
667SMS testing can be accomnplished by supplying the -s flag to simulate
668the receipt of a number of SMS messages.  The message text can be
669specified with the --text option on the command line, or read from a
670file by using the --file option.  If the messages are located in a
671file, then each line corresponds to a single SMS message.
672
673Chrome should display the SMS messages as soon as a user logs in to
674the Chromebook, or if the user is already logged in, then shortly
675after the pseudo modem is recognized by shill.
676"""
677    parser = OptionParser(usage=usage)
678    parser.add_option('-c', '--carrier', dest='carrier_name',
679                      metavar='<carrier name>',
680                      help='<carrier name> := %s' % ' | '.join(
681                          SIM.CARRIERS.keys()))
682    parser.add_option('-s', '--smscount', dest='sms_count',
683                      default=0,
684                      metavar='<smscount>',
685                      help='<smscount> := integer')
686    parser.add_option('-l', '--logfile', dest='logfile',
687                      default='',
688                      metavar='<filename>',
689                      help='<filename> := filename for logging output')
690    parser.add_option('-t', '--text', dest='sms_text',
691                      default=None,
692                      metavar='<text>',
693                      help='<text> := text for sms messages')
694    parser.add_option('-f', '--file', dest='filename',
695                      default=None,
696                      metavar='<filename>',
697                      help='<filename> := file with text for sms messages')
698
699    (options, args) = parser.parse_args()
700
701    kwargs = {}
702    if options.logfile:
703        kwargs['filename'] = options.logfile
704    logging.basicConfig(format='%(asctime)-15s %(message)s',
705                        level=logging.DEBUG,
706                        **kwargs)
707
708    if not options.carrier_name:
709        options.carrier_name = DEFAULT_CARRIER
710
711    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
712    bus = dbus.SystemBus()
713    name = dbus.service.BusName(mm1.MODEM_MANAGER_INTERFACE, bus)
714    manager = ModemManager(bus, mm1.OMM)
715    sim_card = SIM.FromCarrier(string.lower(options.carrier_name),
716                               manager)
717    with GSMModem(manager, sim=sim_card) as modem:
718        if options.filename:
719            f = open(options.filename, 'r')
720            for index, line in enumerate(f.readlines()):
721                line = line.strip()
722                if line:
723                    sms = SMS(manager, name='/SMS/%s' % index, text=line)
724                    modem.AddSMS(sms)
725        else:
726            for index in xrange(int(options.sms_count)):
727                sms = SMS(manager, name='/SMS/%s' % index,
728                          text=options.sms_text)
729                modem.AddSMS(sms)
730
731        mainloop = gobject.MainLoop()
732
733        def SignalHandler(signum, frame):
734            logging.info('Signal handler called with signal: %s', signum)
735            mainloop.quit()
736
737        signal.signal(signal.SIGTERM, SignalHandler)
738
739        mainloop.run()
740
741if __name__ == '__main__':
742    main()
743