Home | History | Annotate | Download | only in cellular
      1 #!/usr/bin/env python
      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 
      8 This modem mimics a GSM (eventually LTE & CDMA) modem and allows a
      9 user to test shill and UI behaviors when a supported SIM is inserted
     10 into the device.  Invoked with the proper flags it can test that SMS
     11 messages are deliver to the UI.
     12 
     13 This program creates a virtual network interface to simulate the
     14 network interface of a modem.  It depends on modemmanager-next to
     15 set the dbus permissions properly.
     16 
     17 TODO:
     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 
     24 from optparse import OptionParser
     25 import logging
     26 import os
     27 import signal
     28 import string
     29 import subprocess
     30 import sys
     31 import time
     32 
     33 import dbus
     34 from dbus.exceptions import DBusException
     35 import dbus.mainloop.glib
     36 import dbus.service
     37 from dbus.types import Int32
     38 from dbus.types import ObjectPath
     39 from dbus.types import Struct
     40 from dbus.types import UInt32
     41 import glib
     42 import gobject
     43 import mm1
     44 
     45 
     46 # Miscellaneous delays to simulate a modem
     47 DEFAULT_CONNECT_DELAY_MS = 1500
     48 
     49 DEFAULT_CARRIER = 'att'
     50 
     51 
     52 class 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 
    101 class 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 
    171 class 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 
    201 class 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     def __init__(self, interface, base):
    209         self.interface = interface
    210         self.peer = self.interface + 'p'
    211         self.base = base
    212         self.lease_file = '/tmp/dnsmasq.%s.leases' % self.interface
    213         self.dnsmasq = None
    214 
    215     def __enter__(self):
    216         """Make usable with "with" statement."""
    217         self.CreateInterface()
    218         return self
    219 
    220     def __exit__(self, exception, value, traceback):
    221         """Make usable with "with" statement."""
    222         self.DestroyInterface()
    223         return False
    224 
    225     def CreateInterface(self):
    226         """Creates a virtual interface.
    227 
    228         Creates the virtual interface self.interface as well as a peer
    229         interface.  Runs dnsmasq on the peer interface so that a DHCP
    230         service can offer ip addresses to the virtual interface.
    231         """
    232         os.system('ip link add name %s type veth peer name %s' % (
    233             self.interface, self.peer))
    234 
    235         os.system('ifconfig %s %s.1/24' % (self.peer, self.base))
    236         os.system('ifconfig %s up' % self.peer)
    237 
    238         os.system('ifconfig %s up' % self.interface)
    239         os.system('route add -host 255.255.255.255 dev %s' % self.peer)
    240         os.close(os.open(self.lease_file, os.O_CREAT | os.O_TRUNC))
    241         self.dnsmasq = subprocess.Popen(
    242             ['/usr/local/sbin/dnsmasq',
    243              '--pid-file',
    244              '-k',
    245              '--dhcp-leasefile=%s' % self.lease_file,
    246              '--dhcp-range=%s.2,%s.254' % (self.base, self.base),
    247              '--port=0',
    248              '--interface=%s' % self.peer,
    249              '--bind-interfaces'
    250             ])
    251         # iptables rejects packets on a newly defined interface.  Fix that.
    252         os.system('iptables -I INPUT -i %s -j ACCEPT' % self.peer)
    253         os.system('iptables -I INPUT -i %s -j ACCEPT' % self.interface)
    254 
    255     def DestroyInterface(self):
    256         """Destroys the virtual interface.
    257 
    258         Stops dnsmasq and cleans up all on disk state.
    259         """
    260         if self.dnsmasq:
    261             self.dnsmasq.terminate()
    262         try:
    263             os.system('route del -host 255.255.255.255')
    264         except:
    265             pass
    266         try:
    267             os.system('ip link del %s' % self.interface)
    268         except:
    269             pass
    270         os.system('iptables -D INPUT -i %s -j ACCEPT' % self.peer)
    271         os.system('iptables -D INPUT -i %s -j ACCEPT' % self.interface)
    272         if os.path.exists(self.lease_file):
    273             os.remove(self.lease_file)
    274 
    275 
    276 class Modem(DBusObjectWithProperties):
    277     """A Modem object that implements the ModemManager DBUS API."""
    278 
    279     def __init__(self, manager, name='/Modem/0',
    280                  device='pseudomodem0',
    281                  mdn='0000001234',
    282                  meid='A100000DCE2CA0',
    283                  carrier='CrCarrier',
    284                  esn='EDD1EDD1',
    285                  sim=None):
    286         """Instantiates a Modem with some options.
    287 
    288         Args:
    289             manager: a ModemManager object.
    290             name: string, a dbus path name.
    291             device: string, the network device to use.
    292             mdn: string, the mobile directory number.
    293             meid: string, the mobile equipment id (CDMA only?).
    294             carrier: string, the name of the carrier.
    295             esn: string, the electronic serial number.
    296             sim: a SIM object.
    297         """
    298         self.state = mm1.MM_MODEM_STATE_DISABLED
    299         self.manager = manager
    300         self.name = name
    301         self.path = manager.path + name
    302         self.device = device
    303         self.mdn = mdn
    304         self.meid = meid
    305         self.carrier = carrier
    306         self.operator_name = carrier
    307         self.operator_code = '123'
    308         self.esn = esn
    309         self.registration_state = mm1.MM_MODEM_3GPP_REGISTRATION_STATE_IDLE
    310         self.sim = sim
    311         DBusObjectWithProperties.__init__(self, manager.bus, self.path)
    312         self.pseudo_interface = PseudoNetworkInterface(self.device, '192.168.7')
    313         self.smses = {}
    314 
    315     def __enter__(self):
    316         """Make usable with "with" statement."""
    317         self.pseudo_interface.__enter__()
    318         # Add the device to the manager only after the pseudo
    319         # interface has been created.
    320         self.manager.Add(self)
    321         return self
    322 
    323     def __exit__(self, exception, value, traceback):
    324         """Make usable with "with" statement."""
    325         self.manager.Remove(self)
    326         return self.pseudo_interface.__exit__(exception, value, traceback)
    327 
    328     def DiscardModem(self):
    329         """Discard this DBUS Object.
    330 
    331         Send a message that a modem has disappeared and deregister from DBUS.
    332         """
    333         logging.info('DiscardModem')
    334         self.remove_from_connection()
    335         self.manager.Remove(self)
    336 
    337     def ModemProperties(self):
    338         """Return the properties of the modem object."""
    339         properties = {
    340             # 'Sim': type='o'
    341             'ModemCapabilities': UInt32(0),
    342             'CurrentCapabilities': UInt32(0),
    343             'MaxBearers': UInt32(2),
    344             'MaxActiveBearers': UInt32(2),
    345             'Manufacturer': 'Foo Electronics',
    346             'Model': 'Super Foo Modem',
    347             'Revision': '1.0',
    348             'DeviceIdentifier': '123456789',
    349             'Device': self.device,
    350             'Driver': 'fake',
    351             'Plugin': 'Foo Plugin',
    352             'EquipmentIdentifier': self.meid,
    353             'UnlockRequired': UInt32(0),
    354             #'UnlockRetries' type='a{uu}'
    355             mm1.MM_MODEM_PROPERTY_STATE: Int32(self.state),
    356             'AccessTechnologies': UInt32(self.state),
    357             'SignalQuality': Struct([UInt32(90), True], signature='ub'),
    358             'OwnNumbers': ['6175551212'],
    359             'SupportedModes': UInt32(0),
    360             'AllowedModes': UInt32(0),
    361             'PreferredMode': UInt32(0),
    362             'SupportedBands': [UInt32(0)],
    363             'Bands': [UInt32(0)]
    364             }
    365         if self.sim:
    366             properties['Sim'] = ObjectPath(self.sim.path)
    367         return properties
    368 
    369     def InterfacesAndProperties(self):
    370         """Return all supported interfaces and their properties."""
    371         return {
    372             mm1.MODEM_INTERFACE: self.ModemProperties(),
    373             }
    374 
    375     def ChangeState(self, new_state,
    376                     why=mm1.MM_MODEM_STATE_CHANGE_REASON_UNKNOWN):
    377         logging.info('Change state from %s to %s', self.state, new_state)
    378         self.StateChanged(Int32(self.state), Int32(new_state), UInt32(why))
    379         self.PropertiesChanged(mm1.MODEM_INTERFACE,
    380                                {mm1.MM_MODEM_PROPERTY_STATE: Int32(new_state)},
    381                                [])
    382         self.state = new_state
    383 
    384     @dbus.service.method(mm1.MODEM_INTERFACE,
    385                          in_signature='b', out_signature='')
    386     def Enable(self, on, *args, **kwargs):
    387         """Enables the Modem."""
    388         logging.info('Modem: Enable %s', str(on))
    389         if on:
    390             if self.state <= mm1.MM_MODEM_STATE_ENABLING:
    391                 self.ChangeState(mm1.MM_MODEM_STATE_ENABLING)
    392             if self.state <= mm1.MM_MODEM_STATE_ENABLED:
    393                 self.ChangeState(mm1.MM_MODEM_STATE_ENABLED)
    394             if self.state <= mm1.MM_MODEM_STATE_SEARCHING:
    395                 self.ChangeState(mm1.MM_MODEM_STATE_SEARCHING)
    396             glib.timeout_add(250, self.OnRegistered)
    397         else:
    398             if self.state >= mm1.MM_MODEM_STATE_DISABLING:
    399                 self.ChangeState(mm1.MM_MODEM_STATE_DISABLING)
    400             if self.state >= mm1.MM_MODEM_STATE_DISABLED:
    401                 self.ChangeState(mm1.MM_MODEM_STATE_DISABLED)
    402                 self.ChangeRegistrationState(
    403                     mm1.MM_MODEM_3GPP_REGISTRATION_STATE_IDLE)
    404         return None
    405 
    406     def ChangeRegistrationState(self, new_state):
    407         """Updates the registration state of the modem.
    408 
    409         Updates the registration state of the modem and broadcasts a
    410         DBUS signal.
    411 
    412         Args:
    413           new_state: the new registation state of the modem.
    414         """
    415         if new_state != self.registration_state:
    416             self.registration_state = new_state
    417             self.PropertiesChanged(
    418                 mm1.MODEM_MODEM3GPP_INTERFACE,
    419                 {mm1.MM_MODEM3GPP_PROPERTY_REGISTRATION_STATE:
    420                      UInt32(new_state)},
    421                 [])
    422 
    423     def OnRegistered(self):
    424         """Called when the Modem is Registered."""
    425         if (self.state >= mm1.MM_MODEM_STATE_ENABLED and
    426             self.state <= mm1.MM_MODEM_STATE_REGISTERED):
    427             logging.info('Modem: Marking Registered')
    428             self.ChangeRegistrationState(
    429                 mm1.MM_MODEM_3GPP_REGISTRATION_STATE_HOME)
    430             self.ChangeState(mm1.MM_MODEM_STATE_REGISTERED)
    431 
    432     @dbus.service.method(mm1.MODEM_SIMPLE_INTERFACE, in_signature='',
    433                          out_signature='a{sv}')
    434     def GetStatus(self, *args, **kwargs):
    435         """Gets the general modem status.
    436 
    437         Returns:
    438             A dictionary of properties.
    439         """
    440         logging.info('Modem: GetStatus')
    441         properties = {
    442             'state': UInt32(self.state),
    443             'signal-quality': UInt32(99),
    444             'bands': self.carrier,
    445             'access-technology': UInt32(0),
    446             'm3gpp-registration-state': UInt32(self.registration_state),
    447             'm3gpp-operator-code': '123',
    448             'm3gpp-operator-name': '123',
    449             'cdma-cdma1x-registration-state': UInt32(99),
    450             'cdma-evdo-registration-state': UInt32(99),
    451             'cdma-sid': '123',
    452             'cdma-nid': '123',
    453             }
    454         if self.state >= mm1.MM_MODEM_STATE_ENABLED:
    455             properties['carrier'] = 'Test Network'
    456         return properties
    457 
    458     @dbus.service.signal(mm1.MODEM_INTERFACE, signature='iiu')
    459     def StateChanged(self, old_state, new_state, why):
    460         pass
    461 
    462     @dbus.service.method(mm1.MODEM_SIMPLE_INTERFACE, in_signature='a{sv}',
    463                          out_signature='o',
    464                          async_callbacks=('return_cb', 'raise_cb'))
    465     def Connect(self, unused_props, return_cb, raise_cb, **kwargs):
    466         """Connect the modem to the network.
    467 
    468         Args:
    469             unused_props: connection properties. See ModemManager documentation.
    470             return_cb: function to call to return result asynchronously.
    471             raise_cb: function to call to raise an error asynchronously.
    472         """
    473 
    474         def ConnectDone(new, why):
    475             logging.info('Modem: ConnectDone %s -> %s because %s',
    476                          str(self.state), str(new), str(why))
    477             if self.state == mm1.MM_MODEM_STATE_CONNECTING:
    478                 self.ChangeState(new, why)
    479             # TODO(jglasgow): implement a bearer object
    480                 bearer_path = '/Bearer/0'
    481                 return_cb(bearer_path)
    482             else:
    483                 raise_cb(mm1.ConnectionUnknownError())
    484 
    485         logging.info('Modem: Connect')
    486         if self.state != mm1.MM_MODEM_STATE_REGISTERED:
    487             logging.info(
    488                 'Modem: Connect fails on unregistered modem.  State = %s',
    489                 self.state)
    490             raise mm1.NoNetworkError()
    491         delay_ms = kwargs.get('connect_delay_ms', DEFAULT_CONNECT_DELAY_MS)
    492         time.sleep(delay_ms / 1000.0)
    493         self.ChangeState(mm1.MM_MODEM_STATE_CONNECTING)
    494         glib.timeout_add(50, lambda: ConnectDone(
    495             mm1.MM_MODEM_STATE_CONNECTED,
    496             mm1.MM_MODEM_STATE_CHANGE_REASON_USER_REQUESTED))
    497 
    498     @dbus.service.method(mm1.MODEM_SIMPLE_INTERFACE, in_signature='o',
    499                          async_callbacks=('return_cb', 'raise_cb'))
    500     def Disconnect(self, bearer, return_cb, raise_cb, **kwargs):
    501         """Disconnect the modem from the network."""
    502 
    503         def DisconnectDone(old, new, why):
    504             logging.info('Modem: DisconnectDone %s -> %s because %s',
    505                          str(old), str(new), str(why))
    506             if self.state == mm1.MM_MODEM_STATE_DISCONNECTING:
    507                 logging.info('Modem: State is DISCONNECTING, changing to %s',
    508                              str(new))
    509                 self.ChangeState(new)
    510                 return_cb()
    511             elif self.state == mm1.MM_MODEM_STATE_DISABLED:
    512                 logging.info('Modem: State is DISABLED, not changing state')
    513                 return_cb()
    514             else:
    515                 raise_cb(mm1.ConnectionUnknownError())
    516 
    517         logging.info('Modem: Disconnect')
    518         self.ChangeState(mm1.MM_MODEM_STATE_DISCONNECTING)
    519         glib.timeout_add(
    520             500,
    521             lambda: DisconnectDone(
    522                 self.state,
    523                 mm1.MM_MODEM_STATE_REGISTERED,
    524                 mm1.MM_MODEM_STATE_CHANGE_REASON_USER_REQUESTED))
    525 
    526     @dbus.service.signal(dbus.PROPERTIES_IFACE, signature='sa{sv}as')
    527     def PropertiesChanged(self, interface, changed_properties,
    528                           invalidated_properties):
    529         pass
    530 
    531     def AddSMS(self, sms):
    532         logging.info('Adding SMS %s to list', sms.path)
    533         self.smses[sms.path] = sms
    534         self.Added(self.path, True)
    535 
    536     @dbus.service.method(mm1.MODEM_MESSAGING_INTERFACE, in_signature='',
    537                          out_signature='ao')
    538     def List(self, *args, **kwargs):
    539         logging.info('Modem.Messaging: List: %s',
    540                      ', '.join(self.smses.keys()))
    541         return self.smses.keys()
    542 
    543     @dbus.service.method(mm1.MODEM_MESSAGING_INTERFACE, in_signature='o',
    544                          out_signature='')
    545     def Delete(self, sms_path, *args, **kwargs):
    546         logging.info('Modem.Messaging: Delete %s', sms_path)
    547         del self.smses[sms_path]
    548 
    549     @dbus.service.signal(mm1.MODEM_MESSAGING_INTERFACE, signature='ob')
    550     def Added(self, sms_path, complete):
    551         pass
    552 
    553 
    554 class GSMModem(Modem):
    555     """A GSMModem implements the mm1.MODEM_MODEM3GPP_INTERFACE interface."""
    556 
    557     def __init__(self, manager, imei='00112342342', **kwargs):
    558         self.imei = imei
    559         Modem.__init__(self, manager, **kwargs)
    560 
    561     @dbus.service.method(mm1.MODEM_MODEM3GPP_INTERFACE,
    562                          in_signature='s', out_signature='')
    563     def Register(self, operator_id, *args, **kwargs):
    564         """Register the modem on the network."""
    565         pass
    566 
    567     def Modem3GPPProperties(self):
    568         """Return the 3GPP Properties of the modem object."""
    569         return {
    570             'Imei': self.imei,
    571             mm1.MM_MODEM3GPP_PROPERTY_REGISTRATION_STATE:
    572                 UInt32(self.registration_state),
    573             'OperatorCode': self.operator_code,
    574             'OperatorName': self.operator_name,
    575             'EnabledFacilityLocks': UInt32(0)
    576             }
    577 
    578     def InterfacesAndProperties(self):
    579         """Return all supported interfaces and their properties."""
    580         return {
    581             mm1.MODEM_INTERFACE: self.ModemProperties(),
    582             mm1.MODEM_MODEM3GPP_INTERFACE: self.Modem3GPPProperties()
    583             }
    584 
    585     @dbus.service.method(mm1.MODEM_MODEM3GPP_INTERFACE, in_signature='',
    586                          out_signature='aa{sv}')
    587     def Scan(self, *args, **kwargs):
    588         """Scan for networks."""
    589         raise mm1.CoreUnsupportedError()
    590 
    591 
    592 class ModemManager(dbus.service.Object):
    593     """Implements the org.freedesktop.DBus.ObjectManager interface."""
    594 
    595     def __init__(self, bus, path):
    596         self.devices = []
    597         self.bus = bus
    598         self.path = path
    599         dbus.service.Object.__init__(self, bus, path)
    600 
    601     def Add(self, device):
    602         """Adds a modem device to the list of devices that are managed."""
    603         logging.info('ModemManager: add %s', device.name)
    604         self.devices.append(device)
    605         interfaces = device.InterfacesAndProperties()
    606         logging.info('Add: %s', interfaces)
    607         self.InterfacesAdded(device.path, interfaces)
    608 
    609     def Remove(self, device):
    610         """Removes a modem device from the list of managed devices."""
    611         logging.info('ModemManager: remove %s', device.name)
    612         self.devices.remove(device)
    613         interfaces = device.InterfacesAndProperties().keys()
    614         self.InterfacesRemoved(device.path, interfaces)
    615 
    616     @dbus.service.method(mm1.OFDOM, out_signature='a{oa{sa{sv}}}')
    617     def GetManagedObjects(self):
    618         """Returns the list of managed objects and their properties."""
    619         results = {}
    620         for device in self.devices:
    621             results[device.path] = device.InterfacesAndProperties()
    622         logging.info('GetManagedObjects: %s', ', '.join(results.keys()))
    623         return results
    624 
    625     @dbus.service.signal(mm1.OFDOM, signature='oa{sa{sv}}')
    626     def InterfacesAdded(self, object_path, interfaces_and_properties):
    627         pass
    628 
    629     @dbus.service.signal(mm1.OFDOM, signature='oas')
    630     def InterfacesRemoved(self, object_path, interfaces):
    631         pass
    632 
    633 
    634 def main():
    635     usage = """
    636 Run pseudo_modem to simulate a GSM modem using the modemmanager-next
    637 DBUS interfaces.  This can be used for the following:
    638   - to simpilify the verification process of UI features that use of
    639     overseas SIM cards
    640   - to test shill on a virtual machine without a physical modem
    641   - to test that Chrome property displays SMS messages
    642 
    643 To use on a test image you use test_that to run
    644 network_3GModemControl which will cause pseudo_modem.py to be
    645 installed in /usr/local/autotests/cros/cellular.  Then stop
    646 modemmanager and start the pseudo modem with the commands:
    647 
    648   stop modemmanager
    649   /usr/local/autotest/cros/cellular/pseudo_modem.py
    650 
    651 When done, use Control-C to stop the process and restart modem manager:
    652   start modemmanager
    653 
    654 Additional help documentation is available by invoking pseudo_modem.py
    655 --help.
    656 
    657 SMS testing can be accomnplished by supplying the -s flag to simulate
    658 the receipt of a number of SMS messages.  The message text can be
    659 specified with the --text option on the command line, or read from a
    660 file by using the --file option.  If the messages are located in a
    661 file, then each line corresponds to a single SMS message.
    662 
    663 Chrome should display the SMS messages as soon as a user logs in to
    664 the Chromebook, or if the user is already logged in, then shortly
    665 after the pseudo modem is recognized by shill.
    666 """
    667     parser = OptionParser(usage=usage)
    668     parser.add_option('-c', '--carrier', dest='carrier_name',
    669                       metavar='<carrier name>',
    670                       help='<carrier name> := %s' % ' | '.join(
    671                           SIM.CARRIERS.keys()))
    672     parser.add_option('-s', '--smscount', dest='sms_count',
    673                       default=0,
    674                       metavar='<smscount>',
    675                       help='<smscount> := integer')
    676     parser.add_option('-l', '--logfile', dest='logfile',
    677                       default='',
    678                       metavar='<filename>',
    679                       help='<filename> := filename for logging output')
    680     parser.add_option('-t', '--text', dest='sms_text',
    681                       default=None,
    682                       metavar='<text>',
    683                       help='<text> := text for sms messages')
    684     parser.add_option('-f', '--file', dest='filename',
    685                       default=None,
    686                       metavar='<filename>',
    687                       help='<filename> := file with text for sms messages')
    688 
    689     (options, args) = parser.parse_args()
    690 
    691     kwargs = {}
    692     if options.logfile:
    693         kwargs['filename'] = options.logfile
    694     logging.basicConfig(format='%(asctime)-15s %(message)s',
    695                         level=logging.DEBUG,
    696                         **kwargs)
    697 
    698     if not options.carrier_name:
    699         options.carrier_name = DEFAULT_CARRIER
    700 
    701     dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    702     bus = dbus.SystemBus()
    703     name = dbus.service.BusName(mm1.MODEM_MANAGER_INTERFACE, bus)
    704     manager = ModemManager(bus, mm1.OMM)
    705     sim_card = SIM.FromCarrier(string.lower(options.carrier_name),
    706                                manager)
    707     with GSMModem(manager, sim=sim_card) as modem:
    708         if options.filename:
    709             f = open(options.filename, 'r')
    710             for index, line in enumerate(f.readlines()):
    711                 line = line.strip()
    712                 if line:
    713                     sms = SMS(manager, name='/SMS/%s' % index, text=line)
    714                     modem.AddSMS(sms)
    715         else:
    716             for index in xrange(int(options.sms_count)):
    717                 sms = SMS(manager, name='/SMS/%s' % index,
    718                           text=options.sms_text)
    719                 modem.AddSMS(sms)
    720 
    721         mainloop = gobject.MainLoop()
    722 
    723         def SignalHandler(signum, frame):
    724             logging.info('Signal handler called with signal: %s', signum)
    725             mainloop.quit()
    726 
    727         signal.signal(signal.SIGTERM, SignalHandler)
    728 
    729         mainloop.run()
    730 
    731 if __name__ == '__main__':
    732     main()
    733