Home | History | Annotate | Download | only in nfc
      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 
      7 import cmd
      8 import dbus
      9 import dbus.exceptions
     10 import dbus.mainloop.glib
     11 import gobject
     12 import threading
     13 
     14 from functools import wraps
     15 
     16 
     17 DBUS_ERROR = 'org.freedesktop.DBus.Error'
     18 NEARD_PATH = '/org/neard/'
     19 PROMPT = 'NFC> '
     20 
     21 class NfcClientException(Exception):
     22     """Exception class for exceptions thrown by NfcClient."""
     23 
     24 
     25 def 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 
     40 def 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 
     67 class 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 
    440 class 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 
    665 def main():
    666     """Main function."""
    667     NfcConsole().begin()
    668 
    669 
    670 if __name__ == '__main__':
    671     main()
    672