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