1#!/usr/bin/env python
2
3# Copyright (c) 2013 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
7import cmd
8import dbus
9import dbus.exceptions
10import dbus.mainloop.glib
11import gobject
12import threading
13
14from functools import wraps
15
16
17DBUS_ERROR = 'org.freedesktop.DBus.Error'
18NEARD_PATH = '/org/neard/'
19PROMPT = 'NFC> '
20
21class NfcClientException(Exception):
22    """Exception class for exceptions thrown by NfcClient."""
23
24
25def print_message(message, newlines=2):
26    """
27    Prints the given message with extra wrapping newline characters.
28
29    @param message: Message to print.
30    @param newlines: Integer, specifying the number of '\n' characters that
31            should be padded at the beginning and end of |message| before
32            being passed to "print".
33
34    """
35    padding = newlines * '\n'
36    message = padding + message + padding
37    print message
38
39
40def handle_errors(func):
41    """
42    Decorator for handling exceptions that are commonly raised by many of the
43    methods in NfcClient.
44
45    @param func: The function this decorator is wrapping.
46
47    """
48    @wraps(func)
49    def _error_handler(*args):
50        try:
51            return func(*args)
52        except dbus.exceptions.DBusException as e:
53            if e.get_dbus_name() == DBUS_ERROR + '.ServiceUnknown':
54                print_message('neard may have crashed or disappeared. '
55                              'Check if neard is running and run "initialize" '
56                              'from this shell.')
57                return
58            if e.get_dbus_name() == DBUS_ERROR + '.UnknownObject':
59                print_message('Could not find object.')
60                return
61            print_message(str(e))
62        except Exception as e:
63            print_message(str(e))
64    return _error_handler
65
66
67class NfcClient(object):
68    """
69    neard D-Bus client
70
71    """
72    NEARD_SERVICE_NAME = 'org.neard'
73    IMANAGER = NEARD_SERVICE_NAME + '.Manager'
74    IADAPTER = NEARD_SERVICE_NAME + '.Adapter'
75    ITAG = NEARD_SERVICE_NAME + '.Tag'
76    IRECORD = NEARD_SERVICE_NAME + '.Record'
77    IDEVICE = NEARD_SERVICE_NAME + '.Device'
78
79    def __init__(self):
80        self._mainloop = None
81        self._mainloop_thread = None
82        self._adapters = {}
83        self._adapter_property_handler_matches = {}
84
85    def begin(self):
86        """
87        Starts the D-Bus client.
88
89        """
90        # Here we run a GLib MainLoop in its own thread, so that the client can
91        # listen to D-Bus signals while keeping the console interactive.
92        self._dbusmainloop = dbus.mainloop.glib.DBusGMainLoop(
93                set_as_default=True)
94        dbus.mainloop.glib.threads_init()
95        gobject.threads_init()
96
97        def _mainloop_thread_func():
98            self._mainloop = gobject.MainLoop()
99            context = self._mainloop.get_context()
100            self._run_loop = True
101            while self._run_loop:
102                context.iteration(True)
103        self._mainloop_thread = threading.Thread(None, _mainloop_thread_func)
104        self._mainloop_thread.start()
105
106        self._bus = dbus.SystemBus()
107        self.setup_manager()
108
109    def end(self):
110        """
111        Stops the D-Bus client.
112
113        """
114        self._run_loop = False
115        self._mainloop.quit()
116        self._mainloop_thread.join()
117
118    def restart(self):
119        """Reinitializes the NFC client."""
120        self.setup_manager()
121
122    @handle_errors
123    def _get_manager_proxy(self):
124        return dbus.Interface(
125                self._bus.get_object(self.NEARD_SERVICE_NAME, '/'),
126                self.IMANAGER)
127
128    @handle_errors
129    def _get_adapter_proxy(self, adapter):
130        return dbus.Interface(
131                self._bus.get_object(self.NEARD_SERVICE_NAME, adapter),
132                self.IADAPTER)
133
134    def _get_cached_adapter_proxy(self, adapter):
135        adapter_proxy = self._adapters.get(adapter, None)
136        if not adapter_proxy:
137            raise NfcClientException('Adapter "' + adapter + '" not found.')
138        return adapter_proxy
139
140
141    @handle_errors
142    def _get_tag_proxy(self, tag):
143        return dbus.Interface(
144                self._bus.get_object(self.NEARD_SERVICE_NAME, tag),
145                self.ITAG)
146
147    @handle_errors
148    def _get_device_proxy(self, device):
149        return dbus.Interface(
150                self._bus.get_object(self.NEARD_SERVICE_NAME, device),
151                self.IDEVICE)
152
153    @handle_errors
154    def _get_record_proxy(self, record):
155        return dbus.Interface(
156                self._bus.get_object(self.NEARD_SERVICE_NAME, record),
157                self.IRECORD)
158
159    @handle_errors
160    def _get_adapter_properties(self, adapter):
161        adapter_proxy = self._get_cached_adapter_proxy(adapter)
162        return adapter_proxy.GetProperties()
163
164    def _get_adapters(self):
165        props = self._manager.GetProperties()
166        return props.get('Adapters', None)
167
168    def setup_manager(self):
169        """
170        Creates a manager proxy and subscribes to adapter signals. This method
171        will also initialize proxies for adapters if any are available.
172
173        """
174        # Create the manager proxy.
175        self._adapters.clear()
176        self._manager = self._get_manager_proxy()
177        if not self._manager:
178            print_message('Failed to create a proxy to the Manager interface.')
179            return
180
181        # Listen to the adapter added and removed signals.
182        self._manager.connect_to_signal(
183                'AdapterAdded',
184                lambda adapter: self.register_adapter(str(adapter)))
185        self._manager.connect_to_signal(
186                'AdapterRemoved',
187                lambda adapter: self.unregister_adapter(str(adapter)))
188
189        # See if there are any adapters and create proxies for each.
190        adapters = self._get_adapters()
191        if adapters:
192            for adapter in adapters:
193                self.register_adapter(adapter)
194
195    def register_adapter(self, adapter):
196        """
197        Registers an adapter proxy with the given object path and subscribes to
198        adapter signals.
199
200        @param adapter: string, containing the adapter's D-Bus object path.
201
202        """
203        print_message('Added adapter: ' + adapter)
204        adapter_proxy = self._get_adapter_proxy(adapter)
205        self._adapters[adapter] = adapter_proxy
206
207        # Tag found/lost currently don't get fired. Monitor property changes
208        # instead.
209        if self._adapter_property_handler_matches.get(adapter, None) is None:
210            self._adapter_property_handler_matches[adapter] = (
211                    adapter_proxy.connect_to_signal(
212                            'PropertyChanged',
213                            (lambda name, value:
214                                    self._adapter_property_changed_signal(
215                                            adapter, name, value))))
216
217    def unregister_adapter(self, adapter):
218        """
219        Removes the adapter proxy for the given object path from the internal
220        cache of adapters.
221
222        @param adapter: string, containing the adapter's D-Bus object path.
223
224        """
225        print_message('Removed adapter: ' + adapter)
226        match = self._adapter_property_handler_matches.get(adapter, None)
227        if match is not None:
228            match.remove()
229            self._adapter_property_handler_matches.pop(adapter)
230        self._adapters.pop(adapter)
231
232    def _adapter_property_changed_signal(self, adapter, name, value):
233        if name == 'Tags' or name == 'Devices':
234            print_message('Found ' + name + ': ' +
235                          self._dbus_array_to_string(value))
236
237    @handle_errors
238    def show_adapters(self):
239        """
240        Prints the D-Bus object paths of all adapters that are available.
241
242        """
243        adapters = self._get_adapters()
244        if not adapters:
245            print_message('No adapters found.')
246            return
247        for adapter in adapters:
248            print_message('  ' + str(adapter), newlines=0)
249        print
250
251    def _dbus_array_to_string(self, array):
252        string = '[ '
253        for value in array:
254            string += ' ' + str(value) + ', '
255        string += ' ]'
256        return string
257
258    def print_adapter_status(self, adapter):
259        """
260        Prints the properties of the given adapter.
261
262        @param adapter: string, containing the adapter's D-Bus object path.
263
264        """
265        props = self._get_adapter_properties(adapter)
266        if not props:
267            return
268        print_message('Status ' + adapter + ': ', newlines=0)
269        for key, value in props.iteritems():
270            if type(value) == dbus.Array:
271                value = self._dbus_array_to_string(value)
272            else:
273                value = str(value)
274            print_message('  ' + key + ' = ' + value, newlines=0)
275        print
276
277    @handle_errors
278    def set_powered(self, adapter, powered):
279        """
280        Enables or disables the adapter.
281
282        @param adapter: string, containing the adapter's D-Bus object path.
283        @param powered: boolean that dictates whether the adapter will be
284                enabled or disabled.
285
286        """
287        adapter_proxy = self._get_cached_adapter_proxy(adapter)
288        if not adapter_proxy:
289            return
290        adapter_proxy.SetProperty('Powered', powered)
291
292    @handle_errors
293    def start_polling(self, adapter):
294        """
295        Starts polling for nearby tags and devices in "Initiator" mode.
296
297        @param adapter: string, containing the adapter's D-Bus object path.
298
299        """
300        adapter_proxy = self._get_cached_adapter_proxy(adapter)
301        adapter_proxy.StartPollLoop('Initiator')
302        print_message('Started polling.')
303
304    @handle_errors
305    def stop_polling(self, adapter):
306        """
307        Stops polling for nearby tags and devices.
308
309        @param adapter: string, containing the adapter's D-Bus object path.
310
311        """
312        adapter_proxy = self._get_cached_adapter_proxy(adapter)
313        adapter_proxy.StopPollLoop()
314        self._polling_stopped = True
315        print_message('Stopped polling.')
316
317    @handle_errors
318    def show_tag_data(self, tag):
319        """
320        Prints the properties of the given tag, as well as the contents of any
321        records associated with it.
322
323        @param tag: string, containing the tag's D-Bus object path.
324
325        """
326        tag_proxy = self._get_tag_proxy(tag)
327        if not tag_proxy:
328            print_message('Tag "' + tag + '" not found.')
329            return
330        props = tag_proxy.GetProperties()
331        print_message('Tag ' + tag + ': ', newlines=1)
332        for key, value in props.iteritems():
333            if key != 'Records':
334                print_message('  ' + key + ' = ' + str(value), newlines=0)
335        records = props['Records']
336        if not records:
337            return
338        print_message('Records: ', newlines=1)
339        for record in records:
340            self.show_record_data(str(record))
341        print
342
343    @handle_errors
344    def show_device_data(self, device):
345        """
346        Prints the properties of the given device, as well as the contents of
347        any records associated with it.
348
349        @param device: string, containing the device's D-Bus object path.
350
351        """
352        device_proxy = self._get_device_proxy(device)
353        if not device_proxy:
354            print_message('Device "' + device + '" not found.')
355            return
356        records = device_proxy.GetProperties()['Records']
357        if not records:
358            print_message('No records on device.')
359            return
360        print_message('Records: ', newlines=1)
361        for record in records:
362            self.show_record_data(str(record))
363        print
364
365    @handle_errors
366    def show_record_data(self, record):
367        """
368        Prints the contents of the given record.
369
370        @param record: string, containing the record's D-Bus object path.
371
372        """
373        record_proxy = self._get_record_proxy(record)
374        if not record_proxy:
375            print_message('Record "' + record + '" not found.')
376            return
377        props = record_proxy.GetProperties()
378        print_message('Record ' + record + ': ', newlines=1)
379        for key, value in props.iteritems():
380            print '  ' + key + ' = ' + value
381        print
382
383    def _create_record_data(self, record_type, params):
384        if record_type == 'Text':
385            possible_keys = [ 'Encoding', 'Language', 'Representation' ]
386            tag_data = { 'Type': 'Text' }
387        elif record_type == 'URI':
388            possible_keys = [ 'URI' ]
389            tag_data = { 'Type': 'URI' }
390        else:
391            print_message('Writing record type "' + record_type +
392                          '" currently not supported.')
393            return None
394        for key, value in params.iteritems():
395            if key in possible_keys:
396                tag_data[key] = value
397        return tag_data
398
399    @handle_errors
400    def write_tag(self, tag, record_type, params):
401        """
402        Writes an NDEF record to the given tag.
403
404        @param tag: string, containing the tag's D-Bus object path.
405        @param record_type: The type of the record, e.g. Text or URI.
406        @param params: dictionary, containing the parameters of the NDEF.
407
408        """
409        tag_data = self._create_record_data(record_type, params)
410        if not tag_data:
411            return
412        tag_proxy = self._get_tag_proxy(tag)
413        if not tag_proxy:
414            print_message('Tag "' + tag + '" not found.')
415            return
416        tag_proxy.Write(tag_data)
417        print_message('Tag written!')
418
419    @handle_errors
420    def push_to_device(self, device, record_type, params):
421        """
422        Pushes an NDEF record to the given device.
423
424        @param device: string, containing the device's D-Bus object path.
425        @param record_type: The type of the record, e.g. Text or URI.
426        @param params: dictionary, containing the parameters of the NDEF.
427
428        """
429        record_data = self._create_record_data(record_type, params)
430        if not record_data:
431            return
432        device_proxy = self._get_device_proxy(device)
433        if not device_proxy:
434            print_message('Device "' + device + '" not found.')
435            return
436        device_proxy.Push(record_data)
437        print_message('NDEF pushed to device!')
438
439
440class NfcConsole(cmd.Cmd):
441    """
442    Interactive console to interact with the NFC daemon.
443
444    """
445    def __init__(self):
446        cmd.Cmd.__init__(self)
447        self.prompt = PROMPT
448
449    def begin(self):
450        """
451        Starts the interactive shell.
452
453        """
454        print_message('NFC console! Run "help" for a list of commands.',
455                      newlines=1)
456        self._nfc_client = NfcClient()
457        self._nfc_client.begin()
458        self.cmdloop()
459
460    def can_exit(self):
461        """Override"""
462        return True
463
464    def do_initialize(self, args):
465        """Handles "initialize"."""
466        if args:
467            print_message('Command "initialize" expects no arguments.')
468            return
469        self._nfc_client.restart()
470
471    def help_initialize(self):
472        """Prints the help message for "initialize"."""
473        print_message('Initializes the neard D-Bus client. This can be '
474                      'run many times to restart the client in case of '
475                      'neard failures or crashes.')
476
477    def do_adapters(self, args):
478        """Handles "adapters"."""
479        if args:
480            print_message('Command "adapters" expects no arguments.')
481            return
482        self._nfc_client.show_adapters()
483
484    def help_adapters(self):
485        """Prints the help message for "adapters"."""
486        print_message('Displays the D-Bus object paths of the available '
487                      'adapter objects.')
488
489    def do_adapter_status(self, args):
490        """Handles "adapter_status"."""
491        args = args.strip().split(' ')
492        if len(args) != 1 or not args[0]:
493            print_message('Usage: adapter_status <adapter>')
494            return
495        self._nfc_client.print_adapter_status(NEARD_PATH + args[0])
496
497    def help_adapter_status(self):
498        """Prints the help message for "adapter_status"."""
499        print_message('Returns the properties of the given NFC adapter.\n\n'
500                      '    Ex: "adapter_status nfc0"')
501
502    def do_enable_adapter(self, args):
503        """Handles "enable_adapter"."""
504        args = args.strip().split(' ')
505        if len(args) != 1 or not args[0]:
506            print_message('Usage: enable_adapter <adapter>')
507            return
508        self._nfc_client.set_powered(NEARD_PATH + args[0], True)
509
510    def help_enable_adapter(self):
511        """Prints the help message for "enable_adapter"."""
512        print_message('Powers up the adapter. Ex: "enable_adapter nfc0"')
513
514    def do_disable_adapter(self, args):
515        """Handles "disable_adapter"."""
516        args = args.strip().split(' ')
517        if len(args) != 1 or not args[0]:
518            print_message('Usage: disable_adapter <adapter>')
519            return
520        self._nfc_client.set_powered(NEARD_PATH + args[0], False)
521
522    def help_disable_adapter(self):
523        """Prints the help message for "disable_adapter"."""
524        print_message('Powers down the adapter. Ex: "disable_adapter nfc0"')
525
526    def do_start_poll(self, args):
527        """Handles "start_poll"."""
528        args = args.strip().split(' ')
529        if len(args) != 1 or not args[0]:
530            print_message('Usage: start_poll <adapter>')
531            return
532        self._nfc_client.start_polling(NEARD_PATH + args[0])
533
534    def help_start_poll(self):
535        """Prints the help message for "start_poll"."""
536        print_message('Initiates a poll loop.\n\n    Ex: "start_poll nfc0"')
537
538    def do_stop_poll(self, args):
539        """Handles "stop_poll"."""
540        args = args.split(' ')
541        if len(args) != 1 or not args[0]:
542            print_message('Usage: stop_poll <adapter>')
543            return
544        self._nfc_client.stop_polling(NEARD_PATH + args[0])
545
546    def help_stop_poll(self):
547        """Prints the help message for "stop_poll"."""
548        print_message('Stops a poll loop.\n\n    Ex: "stop_poll nfc0"')
549
550    def do_read_tag(self, args):
551        """Handles "read_tag"."""
552        args = args.strip().split(' ')
553        if len(args) != 1 or not args[0]:
554            print_message('Usage read_tag <tag>')
555            return
556        self._nfc_client.show_tag_data(NEARD_PATH + args[0])
557
558    def help_read_tag(self):
559        """Prints the help message for "read_tag"."""
560        print_message('Reads the contents of a tag.  Ex: read_tag nfc0/tag0')
561
562    def _parse_record_args(self, record_type, args):
563        if record_type == 'Text':
564            if len(args) < 5:
565                print_message('Usage: write_tag <tag> Text <encoding> '
566                              '<language> <representation>')
567                return None
568            if args[2] not in [ 'UTF-8', 'UTF-16' ]:
569                print_message('Encoding must be one of "UTF-8" or "UTF-16".')
570                return None
571            return {
572                'Encoding': args[2],
573                'Language': args[3],
574                'Representation': ' '.join(args[4:])
575            }
576        if record_type == 'URI':
577            if len(args) != 3:
578                print_message('Usage: write_tag <tag> URI <uri>')
579                return None
580            return {
581                'URI': args[2]
582            }
583        print_message('Only types "Text" and "URI" are supported by this '
584                      'script.')
585        return None
586
587    def do_write_tag(self, args):
588        """Handles "write_tag"."""
589        args = args.strip().split(' ')
590        if len(args) < 3:
591            print_message('Usage: write_tag <tag> [params]')
592            return
593        record_type = args[1]
594        params = self._parse_record_args(record_type, args)
595        if not params:
596            return
597        self._nfc_client.write_tag(NEARD_PATH + args[0],
598                                   record_type, params)
599
600    def help_write_tag(self):
601        """Prints the help message for "write_tag"."""
602        print_message('Writes the given data to a tag. Usage:\n'
603                      '  write_tag <tag> Text <encoding> <language> '
604                      '<representation>\n  write_tag <tag> URI <uri>')
605
606    def do_read_device(self, args):
607        """Handles "read_device"."""
608        args = args.strip().split(' ')
609        if len(args) != 1 or not args[0]:
610            print_message('Usage read_device <device>')
611            return
612        self._nfc_client.show_device_data(NEARD_PATH + args[0])
613
614    def help_read_device(self):
615        """Prints the help message for "read_device"."""
616        print_message('Reads the contents of a device.  Ex: read_device '
617                      'nfc0/device0')
618
619    def do_push_to_device(self, args):
620        """Handles "push_to_device"."""
621        args = args.strip().split(' ')
622        if len(args) < 3:
623            print_message('Usage: push_to_device <device> [params]')
624            return
625        record_type = args[1]
626        params = self._parse_record_args(record_type, args)
627        if not params:
628            return
629        self._nfc_client.push_to_device(NEARD_PATH + args[0],
630                                        record_type, params)
631
632    def help_push_to_device(self):
633        """Prints the help message for "push_to_device"."""
634        print_message('Pushes the given data to a device. Usage:\n'
635                      '  push_to_device <device> Text <encoding> <language> '
636                      '<representation>\n  push_to_device <device> URI <uri>')
637
638    def do_exit(self, args):
639        """
640        Handles the 'exit' command.
641
642        @param args: Arguments to the command. Unused.
643
644        """
645        if args:
646            print_message('Command "exit" expects no arguments.')
647            return
648        resp = raw_input('Are you sure? (yes/no): ')
649        if resp == 'yes':
650            print_message('Goodbye!')
651            self._nfc_client.end()
652            return True
653        if resp != 'no':
654            print_message('Did not understand: ' + resp)
655        return False
656
657    def help_exit(self):
658        """Handles the 'help exit' command."""
659        print_message('Exits the console.')
660
661    do_EOF = do_exit
662    help_EOF = help_exit
663
664
665def main():
666    """Main function."""
667    NfcConsole().begin()
668
669
670if __name__ == '__main__':
671    main()
672