xref: /aosp_15_r20/external/autotest/client/cros/bluetooth/adv_monitor_helper.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright 2020 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
6"""Advertisement Monitor Test Application."""
7
8import dbus
9import dbus.mainloop.glib
10import dbus.service
11# AU tests use ToT client code, but ToT -3 client version.
12try:
13    from gi.repository import GObject
14except ImportError:
15    import gobject as GObject
16import logging
17
18from multiprocessing import Process, Pipe
19from threading import Thread
20
21DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
22DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
23
24BLUEZ_SERVICE_NAME = 'org.bluez'
25
26ADV_MONITOR_MANAGER_IFACE = 'org.bluez.AdvertisementMonitorManager1'
27ADV_MONITOR_IFACE = 'org.bluez.AdvertisementMonitor1'
28ADV_MONITOR_APP_BASE_PATH = '/org/bluez/adv_monitor_app'
29
30
31class AdvMonitor(dbus.service.Object):
32    """A monitor object.
33
34    This class exposes a dbus monitor object along with its properties
35    and methods.
36
37    More information can be found at BlueZ documentation:
38    doc/advertisement-monitor-api.txt
39
40    """
41
42    # Refer doc/advertisement-monitor-api.txt for more info about unset values.
43    UNSET_RSSI = 127
44    UNSET_TIMEOUT = 0
45    UNSET_SAMPLING_PERIOD = 256
46
47    # Indexes of the Monitor object parameters in a monitor data list.
48    MONITOR_TYPE = 0
49    RSSI_FILTER = 1
50    PATTERNS = 2
51
52    # Indexes of the RSSI filter parameters in a monitor data list.
53    RSSI_H_THRESH = 0
54    RSSI_H_TIMEOUT = 1
55    RSSI_L_THRESH = 2
56    RSSI_L_TIMEOUT = 3
57    SAMPLING_PERIOD = 4
58
59    # Indexes of the Patterns filter parameters in a monitor data list.
60    PATTERN_START_POS = 0
61    PATTERN_AD_TYPE = 1
62    PATTERN_DATA = 2
63
64    def __init__(self, bus, app_path, monitor_id, monitor_data):
65        """Construction of a Monitor object.
66
67        @param bus: a dbus system bus.
68        @param app_path: application path.
69        @param monitor_id: unique monitor id.
70
71        """
72        self.path = app_path + '/monitor' + str(monitor_id)
73        self.bus = bus
74
75        self.events = dict()
76        self.events['Activate'] = 0
77        self.events['Release'] = 0
78        self.events['DeviceFound'] = 0
79        self.events['DeviceLost'] = 0
80
81        self.target_devices = []
82
83        self._set_type(monitor_data[self.MONITOR_TYPE])
84        self._set_rssi(monitor_data[self.RSSI_FILTER])
85        self._set_patterns(monitor_data[self.PATTERNS])
86
87        super(AdvMonitor, self).__init__(self.bus, self.path)
88
89
90    def get_path(self):
91        """Get the dbus object path of the monitor.
92
93        @returns: the monitor object path.
94
95        """
96        return dbus.ObjectPath(self.path)
97
98
99    def get_properties(self):
100        """Get the properties dictionary of the monitor.
101
102        @returns: the monitor properties dictionary.
103
104        """
105        properties = dict()
106        properties['Type'] = dbus.String(self.monitor_type)
107        if self.rssi_h_thresh != self.UNSET_RSSI:
108            properties['RSSIHighThreshold'] = dbus.Int16(self.rssi_h_thresh)
109        if self.rssi_h_timeout != self.UNSET_TIMEOUT:
110            properties['RSSIHighTimeout'] = dbus.UInt16(self.rssi_h_timeout)
111        if self.rssi_l_thresh != self.UNSET_RSSI:
112            properties['RSSILowThreshold'] = dbus.Int16(self.rssi_l_thresh)
113        if self.rssi_l_timeout != self.UNSET_TIMEOUT:
114            properties['RSSILowTimeout'] = dbus.UInt16(self.rssi_l_timeout)
115        if self.sampling_period != self.UNSET_SAMPLING_PERIOD:
116            properties['RSSISamplingPeriod'] = dbus.UInt16(self.sampling_period)
117        properties['Patterns'] = dbus.Array(self.patterns, signature='(yyay)')
118        return {ADV_MONITOR_IFACE: properties}
119
120
121    def _set_type(self, monitor_type):
122        """Set the monitor type.
123
124        @param monitor_type: the type of a monitor.
125
126        """
127        self.monitor_type = monitor_type
128
129
130    def _set_rssi(self, rssi):
131        """Set the RSSI filter values.
132
133        @param rssi: the list of rssi threshold and timeout values.
134
135        """
136        self.rssi_h_thresh = rssi[self.RSSI_H_THRESH]
137        self.rssi_h_timeout = rssi[self.RSSI_H_TIMEOUT]
138        self.rssi_l_thresh = rssi[self.RSSI_L_THRESH]
139        self.rssi_l_timeout = rssi[self.RSSI_L_TIMEOUT]
140        self.sampling_period = rssi[self.SAMPLING_PERIOD]
141
142
143    def _set_patterns(self, patterns):
144        """Set the content filter patterns.
145
146        @param patterns: the list of start position, ad type and patterns.
147
148        """
149        self.patterns = []
150        for pattern in patterns:
151            start_pos = dbus.Byte(pattern[self.PATTERN_START_POS])
152            ad_type = dbus.Byte(pattern[self.PATTERN_AD_TYPE])
153            ad_data = []
154            for byte in pattern[self.PATTERN_DATA]:
155                ad_data.append(dbus.Byte(byte))
156            adv_pattern = dbus.Struct((start_pos, ad_type, ad_data),
157                                      signature='yyay')
158            self.patterns.append(adv_pattern)
159
160
161    def remove_monitor(self):
162        """Remove the monitor object.
163
164        Invoke the dbus method to remove current monitor object from the
165        connection.
166
167        """
168        self.remove_from_connection()
169
170
171    def _update_event_count(self, event):
172        """Update the event count.
173
174        @param event: name of the event.
175
176        """
177        self.events[event] += 1
178
179
180    def get_event_count(self, event):
181        """Read the event count.
182
183        @param event: name of the specific event or 'All' for all events.
184
185        @returns: count of the specific event or dict of counts of all events.
186
187        """
188        if event == 'All':
189            return self.events
190
191        return self.events.get(event)
192
193
194    def reset_event_count(self, event):
195        """Reset the event count.
196
197        @param event: name of the specific event or 'All' for all events.
198
199        @returns: True on success, False otherwise.
200
201        """
202        if event == 'All':
203            for event_key in self.events:
204                self.events[event_key] = 0
205            return True
206
207        if event in self.events:
208            self.events[event] = 0
209            return True
210
211        return False
212
213
214    def set_target_devices(self, devices):
215        """Set the target devices to the given monitor.
216
217        DeviceFound and DeviceLost will only be counted if it is triggered by a
218        target device.
219
220        @param devices: a list of devices in dbus object path
221
222        """
223        self.target_devices = devices
224
225    @dbus.service.method(DBUS_PROP_IFACE,
226                         in_signature='s',
227                         out_signature='a{sv}')
228    def GetAll(self, interface):
229        """Get the properties dictionary of the monitor.
230
231        @param interface: the bluetooth dbus interface.
232
233        @returns: the monitor properties dictionary.
234
235        """
236        logging.info('%s: %s GetAll', self.path, interface)
237
238        if interface != ADV_MONITOR_IFACE:
239            logging.error('%s: GetAll: Invalid arg %s', self.path, interface)
240            return {}
241
242        return self.get_properties()[ADV_MONITOR_IFACE]
243
244
245    @dbus.service.method(ADV_MONITOR_IFACE,
246                         in_signature='',
247                         out_signature='')
248    def Activate(self):
249        """The method callback at Activate."""
250        logging.info('%s: Monitor Activated!', self.path)
251        self._update_event_count('Activate')
252
253
254    @dbus.service.method(ADV_MONITOR_IFACE,
255                         in_signature='',
256                         out_signature='')
257    def Release(self):
258        """The method callback at Release."""
259        logging.info('%s: Monitor Released!', self.path)
260        self._update_event_count('Release')
261
262
263    @dbus.service.method(ADV_MONITOR_IFACE,
264                         in_signature='o',
265                         out_signature='')
266    def DeviceFound(self, device):
267        """The method callback at DeviceFound.
268
269        @param device: the dbus object path of the found device.
270
271        """
272        logging.info('%s: %s Device Found!', self.path, device)
273        if device in self.target_devices:
274            self._update_event_count('DeviceFound')
275        else:
276            logging.debug('Found an uninteresting device: %s', device)
277
278
279    @dbus.service.method(ADV_MONITOR_IFACE,
280                         in_signature='o',
281                         out_signature='')
282    def DeviceLost(self, device):
283        """The method callback at DeviceLost.
284
285        @param device: the dbus object path of the lost device.
286
287        """
288        logging.info('%s: %s Device Lost!', self.path, device)
289        if device in self.target_devices:
290            self._update_event_count('DeviceLost')
291        else:
292            logging.debug('Lost an uninteresting device: %s', device)
293
294
295class AdvMonitorApp(dbus.service.Object):
296    """The test application.
297
298    This class implements a test application to manage monitor objects.
299
300    """
301
302    def __init__(self, bus, dbus_mainloop, advmon_manager, app_id):
303        """Construction of a test application object.
304
305        @param bus: a dbus system bus.
306        @param dbus_mainloop: an instance of mainloop.
307        @param advmon_manager: AdvertisementMonitorManager1 interface on
308                               the adapter.
309        @param app_id: application id (to create application path).
310
311        """
312        self.bus = bus
313        self.mainloop = dbus_mainloop
314        self.advmon_mgr = advmon_manager
315        self.app_path = ADV_MONITOR_APP_BASE_PATH + str(app_id)
316
317        self.monitors = dict()
318
319        super(AdvMonitorApp, self).__init__(self.bus, self.app_path)
320
321
322    def get_app_path(self):
323        """Get the dbus object path of the application.
324
325        @returns: the application path.
326
327        """
328        return dbus.ObjectPath(self.app_path)
329
330
331    def add_monitor(self, monitor_data):
332        """Create a monitor object.
333
334        @param monitor_data: the list containing monitor type, RSSI filter
335                             values and patterns.
336
337        @returns: monitor id, once the monitor is created.
338
339        """
340        monitor_id = 0
341        while monitor_id in self.monitors:
342            monitor_id += 1
343
344        monitor = AdvMonitor(self.bus, self.app_path, monitor_id, monitor_data)
345
346        # Emit the InterfacesAdded signal once the Monitor object is created.
347        self.InterfacesAdded(monitor.get_path(), monitor.get_properties())
348
349        self.monitors[monitor_id] = monitor
350
351        return monitor_id
352
353
354    def remove_monitor(self, monitor_id):
355        """Remove a monitor object based on the given monitor id.
356
357        @param monitor_id: the monitor id.
358
359        @returns: True on success, False otherwise.
360
361        """
362        if monitor_id not in self.monitors:
363            return False
364
365        monitor = self.monitors[monitor_id]
366
367        # Emit the InterfacesRemoved signal before removing the Monitor object.
368        self.InterfacesRemoved(monitor.get_path(),
369                               list(monitor.get_properties().keys()))
370
371        monitor.remove_monitor()
372
373        self.monitors.pop(monitor_id)
374
375        return True
376
377
378    def get_event_count(self, monitor_id, event):
379        """Read the count of a particular event on the given monitor.
380
381        @param monitor_id: the monitor id.
382        @param event: name of the specific event or 'All' for all events.
383
384        @returns: count of the specific event or dict of counts of all events.
385
386        """
387        if monitor_id not in self.monitors:
388            return None
389
390        return self.monitors[monitor_id].get_event_count(event)
391
392
393    def reset_event_count(self, monitor_id, event):
394        """Reset the count of a particular event on the given monitor.
395
396        @param monitor_id: the monitor id.
397        @param event: name of the specific event or 'All' for all events.
398
399        @returns: True on success, False otherwise.
400
401        """
402        if monitor_id not in self.monitors:
403            return False
404
405        return self.monitors[monitor_id].reset_event_count(event)
406
407
408    def set_target_devices(self, monitor_id, devices):
409        """Set the target devices to the given monitor.
410
411        DeviceFound and DeviceLost will only be counted if it is triggered by a
412        target device.
413
414        @param monitor_id: the monitor id.
415        @param devices: a list of devices in dbus object path
416
417        @returns: True on success, False otherwise.
418        """
419        if monitor_id not in self.monitors:
420            return False
421
422        self.monitors[monitor_id].set_target_devices(devices)
423        return True
424
425    def _mainloop_thread(self):
426        """Run the dbus mainloop thread.
427
428        Callback methods on the monitor objects get invoked only when the
429        dbus mainloop is running. This thread starts when app is registered
430        and stops when app is unregistered.
431
432        """
433        self.mainloop.run() # blocks until mainloop.quit() is called
434
435
436    def register_app(self):
437        """Register an advertisement monitor app.
438
439        @returns: True on success, False otherwise.
440
441        """
442        if self.mainloop.is_running():
443            self.mainloop.quit()
444
445        self.register_successful = False
446
447        def register_cb():
448            """Handler when RegisterMonitor succeeded."""
449            logging.info('%s: RegisterMonitor successful!', self.app_path)
450            self.register_successful = True
451            self.mainloop.quit()
452
453        def register_error_cb(error):
454            """Handler when RegisterMonitor failed."""
455            logging.error('%s: RegisterMonitor failed: %s', self.app_path,
456                                                            str(error))
457            self.register_successful = False
458            self.mainloop.quit()
459
460        self.advmon_mgr.RegisterMonitor(self.get_app_path(),
461                                        reply_handler=register_cb,
462                                        error_handler=register_error_cb)
463        self.mainloop.run() # blocks until mainloop.quit() is called
464
465        # Start a background thread to run mainloop.run(). This is required for
466        # the bluetoothd to be able to invoke methods on the monitor object.
467        # Mark this thread as a daemon to make sure that the thread is killed
468        # in case the parent process dies unexpectedly.
469        t = Thread(target=self._mainloop_thread)
470        t.daemon = True
471        t.start()
472
473        return self.register_successful
474
475
476    def unregister_app(self):
477        """Unregister an advertisement monitor app.
478
479        @returns: True on success, False otherwise.
480
481        """
482        if self.mainloop.is_running():
483            self.mainloop.quit()
484
485        self.unregister_successful = False
486
487        def unregister_cb():
488            """Handler when UnregisterMonitor succeeded."""
489            logging.info('%s: UnregisterMonitor successful!', self.app_path)
490            self.unregister_successful = True
491            self.mainloop.quit()
492
493        def unregister_error_cb(error):
494            """Handler when UnregisterMonitor failed."""
495            logging.error('%s: UnregisterMonitor failed: %s', self.app_path,
496                                                              str(error))
497            self.unregister_successful = False
498            self.mainloop.quit()
499
500        self.advmon_mgr.UnregisterMonitor(self.get_app_path(),
501                                          reply_handler=unregister_cb,
502                                          error_handler=unregister_error_cb)
503        self.mainloop.run() # blocks until mainloop.quit() is called
504
505        return self.unregister_successful
506
507
508    @dbus.service.method(DBUS_OM_IFACE, out_signature='a{oa{sa{sv}}}')
509    def GetManagedObjects(self):
510        """Get the list of managed monitor objects.
511
512        @returns: the list of managed objects and their properties.
513
514        """
515        logging.info('%s: GetManagedObjects', self.app_path)
516
517        objects = dict()
518        for monitor_id in self.monitors:
519            monitor = self.monitors[monitor_id]
520            objects[monitor.get_path()] = monitor.get_properties()
521
522        return objects
523
524
525    @dbus.service.signal(DBUS_OM_IFACE, signature='oa{sa{sv}}')
526    def InterfacesAdded(self, object_path, interfaces_and_properties):
527        """Emit the InterfacesAdded signal for a given monitor object.
528
529        Invoking this method emits the InterfacesAdded signal,
530        nothing needs to be done here.
531
532        @param object_path: the dbus object path of a monitor.
533        @param interfaces_and_properties: the monitor properties dictionary.
534
535        """
536        return
537
538
539    @dbus.service.signal(DBUS_OM_IFACE, signature='oas')
540    def InterfacesRemoved(self, object_path, interfaces):
541        """Emit the InterfacesRemoved signal for a given monitor object.
542
543        Invoking this method emits the InterfacesRemoved signal,
544        nothing needs to be done here.
545
546        @param object_path: the dbus object path of a monitor.
547        @param interfaces: the list of monitor interfaces.
548
549        """
550        return
551
552
553class AdvMonitorAppMgr():
554    """The app manager for Advertisement Monitor Test Apps.
555
556    This class manages instances of multiple advertisement monitor test
557    applications.
558
559    """
560
561    # List of commands used by AdvMonitor AppMgr, AppMgr-helper process and
562    # AdvMonitor Test Application for communication between each other.
563    CMD_EXIT_HELPER = 0
564    CMD_CREATE_APP = 1
565    CMD_EXIT_APP = 2
566    CMD_KILL_APP = 3
567    CMD_REGISTER_APP = 4
568    CMD_UNREGISTER_APP = 5
569    CMD_ADD_MONITOR = 6
570    CMD_REMOVE_MONITOR = 7
571    CMD_GET_EVENT_COUNT = 8
572    CMD_RESET_EVENT_COUNT = 9
573    CMD_SET_TARGET_DEVICES = 10
574
575    def __init__(self):
576        """Construction of applications manager object."""
577
578        # Due to a limitation of python, it is not possible to fork a new
579        # process once any dbus connections are established. So, create a
580        # helper process before making any dbus connections. This helper
581        # process can be used to create more processes on demand.
582        parent_conn, child_conn = Pipe()
583        p = Process(target=self._appmgr_helper, args=(child_conn,))
584        p.start()
585
586        self._helper_proc = p
587        self._helper_conn = parent_conn
588        self.apps = []
589
590
591    def _appmgr_helper(self, appmgr_conn):
592        """AppMgr helper process.
593
594        This process is used to create new instances of the AdvMonitor Test
595        Application on demand and acts as a communication bridge between the
596        AppMgr and test applications.
597
598        @param appmgr_conn: an object of AppMgr connection pipe.
599
600        """
601        app_conns = dict()
602
603        done = False
604        while not done:
605            cmd, app_id, data = appmgr_conn.recv()
606            ret = None
607
608            if cmd == self.CMD_EXIT_HELPER:
609                # Terminate any outstanding test application instances before
610                # exiting the helper process.
611                for app_id in app_conns:
612                    p, app_conn = app_conns[app_id]
613                    if p.is_alive():
614                        # Try to exit the app gracefully first, terminate if it
615                        # doesn't work.
616                        app_conn.send((self.CMD_EXIT_APP, None))
617                        if not app_conn.recv() or p.is_alive():
618                            p.terminate()
619                            p.join() # wait for test app to terminate
620                done = True
621                ret = True
622
623            elif cmd == self.CMD_CREATE_APP:
624                if app_id not in app_conns:
625                    parent_conn, child_conn = Pipe()
626                    p = Process(target=self._testapp_main,
627                                args=(child_conn, app_id,))
628                    p.start()
629
630                    app_conns[app_id] = (p, parent_conn)
631                    ret = app_id
632
633            elif cmd == self.CMD_KILL_APP:
634                if app_id in app_conns:
635                    p, _ = app_conns[app_id]
636                    if p.is_alive():
637                        p.terminate()
638                        p.join() # wait for test app to terminate
639
640                    app_conns.pop(app_id)
641                    ret = not p.is_alive()
642
643            else:
644                if app_id in app_conns:
645                    p, app_conn = app_conns[app_id]
646
647                    app_conn.send((cmd, data))
648                    ret = app_conn.recv()
649
650                    if cmd == self.CMD_EXIT_APP:
651                        p.join() # wait for test app to terminate
652
653                        app_conns.pop(app_id)
654                        ret = not p.is_alive()
655
656            appmgr_conn.send(ret)
657
658
659    def _testapp_main(self, helper_conn, app_id):
660        """AdvMonitor Test Application Process.
661
662        This process acts as a client application for AdvMonitor tests and used
663        to host AdvMonitor dbus objects.
664
665        @param helper_conn: an object of AppMgr-helper process connection pipe.
666        @param app_id: the app id of this test app process.
667
668        """
669        # Initialize threads in GObject/dbus-glib before creating local threads.
670        GObject.threads_init()
671        dbus.mainloop.glib.threads_init()
672
673        # Arrange for the GObject main loop to be the default.
674        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
675
676        def get_advmon_mgr(bus):
677            """Finds the AdvMonitor Manager object exported by bluetoothd."""
678            remote_om = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, '/'),
679                                       DBUS_OM_IFACE)
680            objects = remote_om.GetManagedObjects()
681
682            for o, props in objects.items():
683                if ADV_MONITOR_MANAGER_IFACE in props:
684                    return dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, o),
685                                          ADV_MONITOR_MANAGER_IFACE)
686            return None
687
688        bus = dbus.SystemBus()
689        mainloop = GObject.MainLoop()
690        advmon_mgr = get_advmon_mgr(bus)
691
692        app = AdvMonitorApp(bus, mainloop, advmon_mgr, app_id)
693
694        done = False
695        while not done:
696            cmd, data = helper_conn.recv()
697            ret = None
698
699            if cmd == self.CMD_EXIT_APP:
700                done = True
701                ret = True
702
703            elif cmd == self.CMD_REGISTER_APP:
704                ret = app.register_app()
705
706            elif cmd == self.CMD_UNREGISTER_APP:
707                ret = app.unregister_app()
708
709            elif cmd == self.CMD_ADD_MONITOR:
710                ret = app.add_monitor(data)
711
712            elif cmd == self.CMD_REMOVE_MONITOR:
713                ret = app.remove_monitor(data)
714
715            elif cmd == self.CMD_GET_EVENT_COUNT:
716                ret = app.get_event_count(*data)
717
718            elif cmd == self.CMD_RESET_EVENT_COUNT:
719                ret = app.reset_event_count(*data)
720
721            elif cmd == self.CMD_SET_TARGET_DEVICES:
722                ret = app.set_target_devices(*data)
723
724            helper_conn.send(ret)
725
726
727    def _send_to_helper(self, cmd, app_id=None, data=None):
728        """Sends commands to the helper process.
729
730        @param cmd: command number from the above set of CMD_* commands.
731        @param app_id: the app id.
732        @param data: the command data.
733
734        @returns: outcome of the command returned by the helper process.
735
736        """
737        if not self._helper_proc.is_alive():
738            return None
739
740        self._helper_conn.send((cmd, app_id, data))
741        return self._helper_conn.recv()
742
743
744    def create_app(self):
745        """Create an advertisement monitor app.
746
747        @returns: app id, once the app is created.
748
749        """
750        app_id = 0
751        while app_id in self.apps:
752            app_id += 1
753
754        self.apps.append(app_id)
755
756        return self._send_to_helper(self.CMD_CREATE_APP, app_id)
757
758
759    def exit_app(self, app_id):
760        """Exit an advertisement monitor app.
761
762        @param app_id: the app id.
763
764        @returns: True on success, False otherwise.
765
766        """
767        if app_id not in self.apps:
768            return False
769
770        self.apps.remove(app_id)
771
772        return self._send_to_helper(self.CMD_EXIT_APP, app_id)
773
774
775    def kill_app(self, app_id):
776        """Kill an advertisement monitor app by sending SIGKILL.
777
778        @param app_id: the app id.
779
780        @returns: True on success, False otherwise.
781
782        """
783        if app_id not in self.apps:
784            return False
785
786        self.apps.remove(app_id)
787
788        return self._send_to_helper(self.CMD_KILL_APP, app_id)
789
790
791    def register_app(self, app_id):
792        """Register an advertisement monitor app.
793
794        @param app_id: the app id.
795
796        @returns: True on success, False otherwise.
797
798        """
799        if app_id not in self.apps:
800            return False
801
802        return self._send_to_helper(self.CMD_REGISTER_APP, app_id)
803
804
805    def unregister_app(self, app_id):
806        """Unregister an advertisement monitor app.
807
808        @param app_id: the app id.
809
810        @returns: True on success, False otherwise.
811
812        """
813        if app_id not in self.apps:
814            return False
815
816        return self._send_to_helper(self.CMD_UNREGISTER_APP, app_id)
817
818
819    def add_monitor(self, app_id, monitor_data):
820        """Create a monitor object.
821
822        @param app_id: the app id.
823        @param monitor_data: the list containing monitor type, RSSI filter
824                             values and patterns.
825
826        @returns: monitor id, once the monitor is created, None otherwise.
827
828        """
829        if app_id not in self.apps:
830            return None
831
832        return self._send_to_helper(self.CMD_ADD_MONITOR, app_id, monitor_data)
833
834
835    def remove_monitor(self, app_id, monitor_id):
836        """Remove a monitor object based on the given monitor id.
837
838        @param app_id: the app id.
839        @param monitor_id: the monitor id.
840
841        @returns: True on success, False otherwise.
842
843        """
844        if app_id not in self.apps:
845            return False
846
847        return self._send_to_helper(self.CMD_REMOVE_MONITOR, app_id, monitor_id)
848
849
850    def get_event_count(self, app_id, monitor_id, event):
851        """Read the count of a particular event on the given monitor.
852
853        @param app_id: the app id.
854        @param monitor_id: the monitor id.
855        @param event: name of the specific event or 'All' for all events.
856
857        @returns: count of the specific event or dict of counts of all events.
858
859        """
860        if app_id not in self.apps:
861            return None
862
863        return self._send_to_helper(self.CMD_GET_EVENT_COUNT, app_id,
864                                    (monitor_id, event))
865
866
867    def reset_event_count(self, app_id, monitor_id, event):
868        """Reset the count of a particular event on the given monitor.
869
870        @param app_id: the app id.
871        @param monitor_id: the monitor id.
872        @param event: name of the specific event or 'All' for all events.
873
874        @returns: True on success, False otherwise.
875
876        """
877        if app_id not in self.apps:
878            return False
879
880        return self._send_to_helper(self.CMD_RESET_EVENT_COUNT, app_id,
881                                    (monitor_id, event))
882
883
884    def set_target_devices(self, app_id, monitor_id, devices):
885        """Set the target devices to the given monitor.
886
887        DeviceFound and DeviceLost will only be counted if it is triggered by a
888        target device.
889
890        @param app_id: the app id.
891        @param monitor_id: the monitor id.
892        @param devices: a list of devices in dbus object path
893
894        @returns: True on success, False otherwise.
895        """
896        if app_id not in self.apps:
897            return False
898
899        self._send_to_helper(self.CMD_SET_TARGET_DEVICES, app_id,
900                             (monitor_id, devices))
901        return True
902
903    def destroy(self):
904        """Clean up the helper process and test app processes."""
905
906        self._send_to_helper(self.CMD_EXIT_HELPER)
907
908        if self._helper_proc.is_alive():
909            self._helper_proc.terminate()
910            self._helper_proc.join() # wait for helper process to terminate
911
912        return not self._helper_proc.is_alive()
913