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