#!/usr/bin/env python # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import cmd import dbus import dbus.exceptions import dbus.mainloop.glib import gobject import threading from functools import wraps DBUS_ERROR = 'org.freedesktop.DBus.Error' NEARD_PATH = '/org/neard/' PROMPT = 'NFC> ' class NfcClientException(Exception): """Exception class for exceptions thrown by NfcClient.""" def print_message(message, newlines=2): """ Prints the given message with extra wrapping newline characters. @param message: Message to print. @param newlines: Integer, specifying the number of '\n' characters that should be padded at the beginning and end of |message| before being passed to "print". """ padding = newlines * '\n' message = padding + message + padding print message def handle_errors(func): """ Decorator for handling exceptions that are commonly raised by many of the methods in NfcClient. @param func: The function this decorator is wrapping. """ @wraps(func) def _error_handler(*args): try: return func(*args) except dbus.exceptions.DBusException as e: if e.get_dbus_name() == DBUS_ERROR + '.ServiceUnknown': print_message('neard may have crashed or disappeared. ' 'Check if neard is running and run "initialize" ' 'from this shell.') return if e.get_dbus_name() == DBUS_ERROR + '.UnknownObject': print_message('Could not find object.') return print_message(str(e)) except Exception as e: print_message(str(e)) return _error_handler class NfcClient(object): """ neard D-Bus client """ NEARD_SERVICE_NAME = 'org.neard' IMANAGER = NEARD_SERVICE_NAME + '.Manager' IADAPTER = NEARD_SERVICE_NAME + '.Adapter' ITAG = NEARD_SERVICE_NAME + '.Tag' IRECORD = NEARD_SERVICE_NAME + '.Record' IDEVICE = NEARD_SERVICE_NAME + '.Device' def __init__(self): self._mainloop = None self._mainloop_thread = None self._adapters = {} self._adapter_property_handler_matches = {} def begin(self): """ Starts the D-Bus client. """ # Here we run a GLib MainLoop in its own thread, so that the client can # listen to D-Bus signals while keeping the console interactive. self._dbusmainloop = dbus.mainloop.glib.DBusGMainLoop( set_as_default=True) dbus.mainloop.glib.threads_init() gobject.threads_init() def _mainloop_thread_func(): self._mainloop = gobject.MainLoop() context = self._mainloop.get_context() self._run_loop = True while self._run_loop: context.iteration(True) self._mainloop_thread = threading.Thread(None, _mainloop_thread_func) self._mainloop_thread.start() self._bus = dbus.SystemBus() self.setup_manager() def end(self): """ Stops the D-Bus client. """ self._run_loop = False self._mainloop.quit() self._mainloop_thread.join() def restart(self): """Reinitializes the NFC client.""" self.setup_manager() @handle_errors def _get_manager_proxy(self): return dbus.Interface( self._bus.get_object(self.NEARD_SERVICE_NAME, '/'), self.IMANAGER) @handle_errors def _get_adapter_proxy(self, adapter): return dbus.Interface( self._bus.get_object(self.NEARD_SERVICE_NAME, adapter), self.IADAPTER) def _get_cached_adapter_proxy(self, adapter): adapter_proxy = self._adapters.get(adapter, None) if not adapter_proxy: raise NfcClientException('Adapter "' + adapter + '" not found.') return adapter_proxy @handle_errors def _get_tag_proxy(self, tag): return dbus.Interface( self._bus.get_object(self.NEARD_SERVICE_NAME, tag), self.ITAG) @handle_errors def _get_device_proxy(self, device): return dbus.Interface( self._bus.get_object(self.NEARD_SERVICE_NAME, device), self.IDEVICE) @handle_errors def _get_record_proxy(self, record): return dbus.Interface( self._bus.get_object(self.NEARD_SERVICE_NAME, record), self.IRECORD) @handle_errors def _get_adapter_properties(self, adapter): adapter_proxy = self._get_cached_adapter_proxy(adapter) return adapter_proxy.GetProperties() def _get_adapters(self): props = self._manager.GetProperties() return props.get('Adapters', None) def setup_manager(self): """ Creates a manager proxy and subscribes to adapter signals. This method will also initialize proxies for adapters if any are available. """ # Create the manager proxy. self._adapters.clear() self._manager = self._get_manager_proxy() if not self._manager: print_message('Failed to create a proxy to the Manager interface.') return # Listen to the adapter added and removed signals. self._manager.connect_to_signal( 'AdapterAdded', lambda adapter: self.register_adapter(str(adapter))) self._manager.connect_to_signal( 'AdapterRemoved', lambda adapter: self.unregister_adapter(str(adapter))) # See if there are any adapters and create proxies for each. adapters = self._get_adapters() if adapters: for adapter in adapters: self.register_adapter(adapter) def register_adapter(self, adapter): """ Registers an adapter proxy with the given object path and subscribes to adapter signals. @param adapter: string, containing the adapter's D-Bus object path. """ print_message('Added adapter: ' + adapter) adapter_proxy = self._get_adapter_proxy(adapter) self._adapters[adapter] = adapter_proxy # Tag found/lost currently don't get fired. Monitor property changes # instead. if self._adapter_property_handler_matches.get(adapter, None) is None: self._adapter_property_handler_matches[adapter] = ( adapter_proxy.connect_to_signal( 'PropertyChanged', (lambda name, value: self._adapter_property_changed_signal( adapter, name, value)))) def unregister_adapter(self, adapter): """ Removes the adapter proxy for the given object path from the internal cache of adapters. @param adapter: string, containing the adapter's D-Bus object path. """ print_message('Removed adapter: ' + adapter) match = self._adapter_property_handler_matches.get(adapter, None) if match is not None: match.remove() self._adapter_property_handler_matches.pop(adapter) self._adapters.pop(adapter) def _adapter_property_changed_signal(self, adapter, name, value): if name == 'Tags' or name == 'Devices': print_message('Found ' + name + ': ' + self._dbus_array_to_string(value)) @handle_errors def show_adapters(self): """ Prints the D-Bus object paths of all adapters that are available. """ adapters = self._get_adapters() if not adapters: print_message('No adapters found.') return for adapter in adapters: print_message(' ' + str(adapter), newlines=0) print def _dbus_array_to_string(self, array): string = '[ ' for value in array: string += ' ' + str(value) + ', ' string += ' ]' return string def print_adapter_status(self, adapter): """ Prints the properties of the given adapter. @param adapter: string, containing the adapter's D-Bus object path. """ props = self._get_adapter_properties(adapter) if not props: return print_message('Status ' + adapter + ': ', newlines=0) for key, value in props.iteritems(): if type(value) == dbus.Array: value = self._dbus_array_to_string(value) else: value = str(value) print_message(' ' + key + ' = ' + value, newlines=0) print @handle_errors def set_powered(self, adapter, powered): """ Enables or disables the adapter. @param adapter: string, containing the adapter's D-Bus object path. @param powered: boolean that dictates whether the adapter will be enabled or disabled. """ adapter_proxy = self._get_cached_adapter_proxy(adapter) if not adapter_proxy: return adapter_proxy.SetProperty('Powered', powered) @handle_errors def start_polling(self, adapter): """ Starts polling for nearby tags and devices in "Initiator" mode. @param adapter: string, containing the adapter's D-Bus object path. """ adapter_proxy = self._get_cached_adapter_proxy(adapter) adapter_proxy.StartPollLoop('Initiator') print_message('Started polling.') @handle_errors def stop_polling(self, adapter): """ Stops polling for nearby tags and devices. @param adapter: string, containing the adapter's D-Bus object path. """ adapter_proxy = self._get_cached_adapter_proxy(adapter) adapter_proxy.StopPollLoop() self._polling_stopped = True print_message('Stopped polling.') @handle_errors def show_tag_data(self, tag): """ Prints the properties of the given tag, as well as the contents of any records associated with it. @param tag: string, containing the tag's D-Bus object path. """ tag_proxy = self._get_tag_proxy(tag) if not tag_proxy: print_message('Tag "' + tag + '" not found.') return props = tag_proxy.GetProperties() print_message('Tag ' + tag + ': ', newlines=1) for key, value in props.iteritems(): if key != 'Records': print_message(' ' + key + ' = ' + str(value), newlines=0) records = props['Records'] if not records: return print_message('Records: ', newlines=1) for record in records: self.show_record_data(str(record)) print @handle_errors def show_device_data(self, device): """ Prints the properties of the given device, as well as the contents of any records associated with it. @param device: string, containing the device's D-Bus object path. """ device_proxy = self._get_device_proxy(device) if not device_proxy: print_message('Device "' + device + '" not found.') return records = device_proxy.GetProperties()['Records'] if not records: print_message('No records on device.') return print_message('Records: ', newlines=1) for record in records: self.show_record_data(str(record)) print @handle_errors def show_record_data(self, record): """ Prints the contents of the given record. @param record: string, containing the record's D-Bus object path. """ record_proxy = self._get_record_proxy(record) if not record_proxy: print_message('Record "' + record + '" not found.') return props = record_proxy.GetProperties() print_message('Record ' + record + ': ', newlines=1) for key, value in props.iteritems(): print ' ' + key + ' = ' + value print def _create_record_data(self, record_type, params): if record_type == 'Text': possible_keys = [ 'Encoding', 'Language', 'Representation' ] tag_data = { 'Type': 'Text' } elif record_type == 'URI': possible_keys = [ 'URI' ] tag_data = { 'Type': 'URI' } else: print_message('Writing record type "' + record_type + '" currently not supported.') return None for key, value in params.iteritems(): if key in possible_keys: tag_data[key] = value return tag_data @handle_errors def write_tag(self, tag, record_type, params): """ Writes an NDEF record to the given tag. @param tag: string, containing the tag's D-Bus object path. @param record_type: The type of the record, e.g. Text or URI. @param params: dictionary, containing the parameters of the NDEF. """ tag_data = self._create_record_data(record_type, params) if not tag_data: return tag_proxy = self._get_tag_proxy(tag) if not tag_proxy: print_message('Tag "' + tag + '" not found.') return tag_proxy.Write(tag_data) print_message('Tag written!') @handle_errors def push_to_device(self, device, record_type, params): """ Pushes an NDEF record to the given device. @param device: string, containing the device's D-Bus object path. @param record_type: The type of the record, e.g. Text or URI. @param params: dictionary, containing the parameters of the NDEF. """ record_data = self._create_record_data(record_type, params) if not record_data: return device_proxy = self._get_device_proxy(device) if not device_proxy: print_message('Device "' + device + '" not found.') return device_proxy.Push(record_data) print_message('NDEF pushed to device!') class NfcConsole(cmd.Cmd): """ Interactive console to interact with the NFC daemon. """ def __init__(self): cmd.Cmd.__init__(self) self.prompt = PROMPT def begin(self): """ Starts the interactive shell. """ print_message('NFC console! Run "help" for a list of commands.', newlines=1) self._nfc_client = NfcClient() self._nfc_client.begin() self.cmdloop() def can_exit(self): """Override""" return True def do_initialize(self, args): """Handles "initialize".""" if args: print_message('Command "initialize" expects no arguments.') return self._nfc_client.restart() def help_initialize(self): """Prints the help message for "initialize".""" print_message('Initializes the neard D-Bus client. This can be ' 'run many times to restart the client in case of ' 'neard failures or crashes.') def do_adapters(self, args): """Handles "adapters".""" if args: print_message('Command "adapters" expects no arguments.') return self._nfc_client.show_adapters() def help_adapters(self): """Prints the help message for "adapters".""" print_message('Displays the D-Bus object paths of the available ' 'adapter objects.') def do_adapter_status(self, args): """Handles "adapter_status".""" args = args.strip().split(' ') if len(args) != 1 or not args[0]: print_message('Usage: adapter_status ') return self._nfc_client.print_adapter_status(NEARD_PATH + args[0]) def help_adapter_status(self): """Prints the help message for "adapter_status".""" print_message('Returns the properties of the given NFC adapter.\n\n' ' Ex: "adapter_status nfc0"') def do_enable_adapter(self, args): """Handles "enable_adapter".""" args = args.strip().split(' ') if len(args) != 1 or not args[0]: print_message('Usage: enable_adapter ') return self._nfc_client.set_powered(NEARD_PATH + args[0], True) def help_enable_adapter(self): """Prints the help message for "enable_adapter".""" print_message('Powers up the adapter. Ex: "enable_adapter nfc0"') def do_disable_adapter(self, args): """Handles "disable_adapter".""" args = args.strip().split(' ') if len(args) != 1 or not args[0]: print_message('Usage: disable_adapter ') return self._nfc_client.set_powered(NEARD_PATH + args[0], False) def help_disable_adapter(self): """Prints the help message for "disable_adapter".""" print_message('Powers down the adapter. Ex: "disable_adapter nfc0"') def do_start_poll(self, args): """Handles "start_poll".""" args = args.strip().split(' ') if len(args) != 1 or not args[0]: print_message('Usage: start_poll ') return self._nfc_client.start_polling(NEARD_PATH + args[0]) def help_start_poll(self): """Prints the help message for "start_poll".""" print_message('Initiates a poll loop.\n\n Ex: "start_poll nfc0"') def do_stop_poll(self, args): """Handles "stop_poll".""" args = args.split(' ') if len(args) != 1 or not args[0]: print_message('Usage: stop_poll ') return self._nfc_client.stop_polling(NEARD_PATH + args[0]) def help_stop_poll(self): """Prints the help message for "stop_poll".""" print_message('Stops a poll loop.\n\n Ex: "stop_poll nfc0"') def do_read_tag(self, args): """Handles "read_tag".""" args = args.strip().split(' ') if len(args) != 1 or not args[0]: print_message('Usage read_tag ') return self._nfc_client.show_tag_data(NEARD_PATH + args[0]) def help_read_tag(self): """Prints the help message for "read_tag".""" print_message('Reads the contents of a tag. Ex: read_tag nfc0/tag0') def _parse_record_args(self, record_type, args): if record_type == 'Text': if len(args) < 5: print_message('Usage: write_tag Text ' ' ') return None if args[2] not in [ 'UTF-8', 'UTF-16' ]: print_message('Encoding must be one of "UTF-8" or "UTF-16".') return None return { 'Encoding': args[2], 'Language': args[3], 'Representation': ' '.join(args[4:]) } if record_type == 'URI': if len(args) != 3: print_message('Usage: write_tag URI ') return None return { 'URI': args[2] } print_message('Only types "Text" and "URI" are supported by this ' 'script.') return None def do_write_tag(self, args): """Handles "write_tag".""" args = args.strip().split(' ') if len(args) < 3: print_message('Usage: write_tag [params]') return record_type = args[1] params = self._parse_record_args(record_type, args) if not params: return self._nfc_client.write_tag(NEARD_PATH + args[0], record_type, params) def help_write_tag(self): """Prints the help message for "write_tag".""" print_message('Writes the given data to a tag. Usage:\n' ' write_tag Text ' '\n write_tag URI ') def do_read_device(self, args): """Handles "read_device".""" args = args.strip().split(' ') if len(args) != 1 or not args[0]: print_message('Usage read_device ') return self._nfc_client.show_device_data(NEARD_PATH + args[0]) def help_read_device(self): """Prints the help message for "read_device".""" print_message('Reads the contents of a device. Ex: read_device ' 'nfc0/device0') def do_push_to_device(self, args): """Handles "push_to_device".""" args = args.strip().split(' ') if len(args) < 3: print_message('Usage: push_to_device [params]') return record_type = args[1] params = self._parse_record_args(record_type, args) if not params: return self._nfc_client.push_to_device(NEARD_PATH + args[0], record_type, params) def help_push_to_device(self): """Prints the help message for "push_to_device".""" print_message('Pushes the given data to a device. Usage:\n' ' push_to_device Text ' '\n push_to_device URI ') def do_exit(self, args): """ Handles the 'exit' command. @param args: Arguments to the command. Unused. """ if args: print_message('Command "exit" expects no arguments.') return resp = raw_input('Are you sure? (yes/no): ') if resp == 'yes': print_message('Goodbye!') self._nfc_client.end() return True if resp != 'no': print_message('Did not understand: ' + resp) return False def help_exit(self): """Handles the 'help exit' command.""" print_message('Exits the console.') do_EOF = do_exit help_EOF = help_exit def main(): """Main function.""" NfcConsole().begin() if __name__ == '__main__': main()