blob: 8c1c910836f2ea83cfedfe70d76d7c66fa745d3c [file] [log] [blame]
Arman Uguray77486dc2013-10-09 21:40:40 -07001#!/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 if len(value) == 0:
237 self.start_polling(adapter)
238
239 @handle_errors
240 def show_adapters(self):
241 """
242 Prints the D-Bus object paths of all adapters that are available.
243
244 """
245 adapters = self._get_adapters()
246 if not adapters:
247 print_message('No adapters found.')
248 return
249 for adapter in adapters:
250 print_message(' ' + str(adapter), newlines=0)
251 print
252
253 def _dbus_array_to_string(self, array):
254 string = '[ '
255 for value in array:
256 string += ' ' + str(value) + ', '
257 string += ' ]'
258 return string
259
260 def print_adapter_status(self, adapter):
261 """
262 Prints the properties of the given adapter.
263
264 @param adapter: string, containing the adapter's D-Bus object path.
265
266 """
267 props = self._get_adapter_properties(adapter)
268 if not props:
269 return
270 print_message('Status ' + adapter + ': ', newlines=0)
271 for key, value in props.iteritems():
272 if type(value) == dbus.Array:
273 value = self._dbus_array_to_string(value)
274 else:
275 value = str(value)
276 print_message(' ' + key + ' = ' + value, newlines=0)
277 print
278
279 @handle_errors
280 def set_powered(self, adapter, powered):
281 """
282 Enables or disables the adapter.
283
284 @param adapter: string, containing the adapter's D-Bus object path.
285 @param powered: boolean that dictates whether the adapter will be
286 enabled or disabled.
287
288 """
289 adapter_proxy = self._get_cached_adapter_proxy(adapter)
290 if not adapter_proxy:
291 return
292 adapter_proxy.SetProperty('Powered', powered)
293
294 @handle_errors
295 def start_polling(self, adapter):
296 """
297 Starts polling for nearby tags and devices in "Initiator" mode.
298
299 @param adapter: string, containing the adapter's D-Bus object path.
300
301 """
302 adapter_proxy = self._get_cached_adapter_proxy(adapter)
303 adapter_proxy.StartPollLoop('Initiator')
304 print_message('Started polling.')
305
306 @handle_errors
307 def stop_polling(self, adapter):
308 """
309 Stops polling for nearby tags and devices.
310
311 @param adapter: string, containing the adapter's D-Bus object path.
312
313 """
314 adapter_proxy = self._get_cached_adapter_proxy(adapter)
315 adapter_proxy.StopPollLoop()
316 self._polling_stopped = True
317 print_message('Stopped polling.')
318
319 @handle_errors
320 def show_tag_data(self, tag):
321 """
322 Prints the properties of the given tag, as well as the contents of any
323 records associated with it.
324
325 @param tag: string, containing the tag's D-Bus object path.
326
327 """
328 tag_proxy = self._get_tag_proxy(tag)
329 if not tag_proxy:
330 print_message('Tag "' + tag + '" not found.')
331 return
332 props = tag_proxy.GetProperties()
333 print_message('Tag ' + tag + ': ', newlines=1)
334 for key, value in props.iteritems():
335 if key != 'Records':
336 print_message(' ' + key + ' = ' + str(value), newlines=0)
337 records = props['Records']
338 if not records:
339 return
340 print_message('Records: ', newlines=1)
341 for record in records:
342 self.show_record_data(str(record))
343 print
344
345 @handle_errors
346 def show_device_data(self, device):
347 """
348 Prints the properties of the given device, as well as the contents of
349 any records associated with it.
350
351 @param device: string, containing the device's D-Bus object path.
352
353 """
354 device_proxy = self._get_device_proxy(device)
355 if not device_proxy:
356 print_message('Device "' + device + '" not found.')
357 return
358 records = device_proxy.GetProperties()['Records']
359 if not records:
360 print_message('No records on device.')
361 return
362 print_message('Records: ', newlines=1)
363 for record in records:
364 self.show_record_data(str(record))
365 print
366
367 @handle_errors
368 def show_record_data(self, record):
369 """
370 Prints the contents of the given record.
371
372 @param record: string, containing the record's D-Bus object path.
373
374 """
375 record_proxy = self._get_record_proxy(record)
376 if not record_proxy:
377 print_message('Record "' + record + '" not found.')
378 return
379 props = record_proxy.GetProperties()
380 print_message('Record ' + record + ': ', newlines=1)
381 for key, value in props.iteritems():
382 print ' ' + key + ' = ' + value
383 print
384
385 def _create_record_data(self, record_type, params):
386 if record_type == 'Text':
387 possible_keys = [ 'Encoding', 'Language', 'Representation' ]
388 tag_data = { 'Type': 'Text' }
389 elif record_type == 'URI':
390 possible_keys = [ 'URI' ]
391 tag_data = { 'Type': 'URI' }
392 else:
393 print_message('Writing record type "' + record_type +
394 '" currently not supported.')
395 return None
396 for key, value in params.iteritems():
397 if key in possible_keys:
398 tag_data[key] = value
399 return tag_data
400
401 @handle_errors
402 def write_tag(self, tag, record_type, params):
403 """
404 Writes an NDEF record to the given tag.
405
406 @param tag: string, containing the tag's D-Bus object path.
407 @param record_type: The type of the record, e.g. Text or URI.
408 @param params: dictionary, containing the parameters of the NDEF.
409
410 """
411 tag_data = self._create_record_data(record_type, params)
412 if not tag_data:
413 return
414 tag_proxy = self._get_tag_proxy(tag)
415 if not tag_proxy:
416 print_message('Tag "' + tag + '" not found.')
417 return
418 tag_proxy.Write(tag_data)
419 print_message('Tag written!')
420
421 @handle_errors
422 def push_to_device(self, device, record_type, params):
423 """
424 Pushes an NDEF record to the given device.
425
426 @param device: string, containing the device's D-Bus object path.
427 @param record_type: The type of the record, e.g. Text or URI.
428 @param params: dictionary, containing the parameters of the NDEF.
429
430 """
431 record_data = self._create_record_data(record_type, params)
432 if not record_data:
433 return
434 device_proxy = self._get_device_proxy(device)
435 if not device_proxy:
436 print_message('Device "' + device + '" not found.')
437 return
438 device_proxy.Push(record_data)
439 print_message('NDEF pushed to device!')
440
441
442class NfcConsole(cmd.Cmd):
443 """
444 Interactive console to interact with the NFC daemon.
445
446 """
447 def __init__(self):
448 cmd.Cmd.__init__(self)
449 self.prompt = PROMPT
450
451 def begin(self):
452 """
453 Starts the interactive shell.
454
455 """
456 print_message('NFC console! Run "help" for a list of commands.',
457 newlines=1)
458 self._nfc_client = NfcClient()
459 self._nfc_client.begin()
460 self.cmdloop()
461
462 def can_exit(self):
463 """Override"""
464 return True
465
466 def do_initialize(self, args):
467 """Handles "initialize"."""
468 if args:
469 print_message('Command "initialize" expects no arguments.')
470 return
471 self._nfc_client.restart()
472
473 def help_initialize(self):
474 """Prints the help message for "initialize"."""
475 print_message('Initializes the neard D-Bus client. This can be '
476 'run many times to restart the client in case of '
477 'neard failures or crashes.')
478
479 def do_adapters(self, args):
480 """Handles "adapters"."""
481 if args:
482 print_message('Command "adapters" expects no arguments.')
483 return
484 self._nfc_client.show_adapters()
485
486 def help_adapters(self):
487 """Prints the help message for "adapters"."""
488 print_message('Displays the D-Bus object paths of the available '
489 'adapter objects.')
490
491 def do_adapter_status(self, args):
492 """Handles "adapter_status"."""
493 args = args.strip().split(' ')
494 if len(args) != 1 or not args[0]:
495 print_message('Usage: adapter_status <adapter>')
496 return
497 self._nfc_client.print_adapter_status(NEARD_PATH + args[0])
498
499 def help_adapter_status(self):
500 """Prints the help message for "adapter_status"."""
501 print_message('Returns the properties of the given NFC adapter.\n\n'
502 ' Ex: "adapter_status nfc0"')
503
504 def do_enable_adapter(self, args):
505 """Handles "enable_adapter"."""
506 args = args.strip().split(' ')
507 if len(args) != 1 or not args[0]:
508 print_message('Usage: enable_adapter <adapter>')
509 return
510 self._nfc_client.set_powered(NEARD_PATH + args[0], True)
511
512 def help_enable_adapter(self):
513 """Prints the help message for "enable_adapter"."""
514 print_message('Powers up the adapter. Ex: "enable_adapter nfc0"')
515
516 def do_disable_adapter(self, args):
517 """Handles "disable_adapter"."""
518 args = args.strip().split(' ')
519 if len(args) != 1 or not args[0]:
520 print_message('Usage: disable_adapter <adapter>')
521 return
522 self._nfc_client.set_powered(NEARD_PATH + args[0], False)
523
524 def help_disable_adapter(self):
525 """Prints the help message for "disable_adapter"."""
526 print_message('Powers down the adapter. Ex: "disable_adapter nfc0"')
527
528 def do_start_poll(self, args):
529 """Handles "start_poll"."""
530 args = args.strip().split(' ')
531 if len(args) != 1 or not args[0]:
532 print_message('Usage: start_poll <adapter>')
533 return
534 self._nfc_client.start_polling(NEARD_PATH + args[0])
535
536 def help_start_poll(self):
537 """Prints the help message for "start_poll"."""
538 print_message('Initiates a poll loop.\n\n Ex: "start_poll nfc0"')
539
540 def do_stop_poll(self, args):
541 """Handles "stop_poll"."""
542 args = args.split(' ')
543 if len(args) != 1 or not args[0]:
544 print_message('Usage: stop_poll <adapter>')
545 return
546 self._nfc_client.stop_polling(NEARD_PATH + args[0])
547
548 def help_stop_poll(self):
549 """Prints the help message for "stop_poll"."""
550 print_message('Stops a poll loop.\n\n Ex: "stop_poll nfc0"')
551
552 def do_read_tag(self, args):
553 """Handles "read_tag"."""
554 args = args.strip().split(' ')
555 if len(args) != 1 or not args[0]:
556 print_message('Usage read_tag <tag>')
557 return
558 self._nfc_client.show_tag_data(NEARD_PATH + args[0])
559
560 def help_read_tag(self):
561 """Prints the help message for "read_tag"."""
562 print_message('Reads the contents of a tag. Ex: read_tag nfc0/tag0')
563
564 def _parse_record_args(self, record_type, args):
565 if record_type == 'Text':
566 if len(args) < 5:
567 print_message('Usage: write_tag <tag> Text <encoding> '
568 '<language> <representation>')
569 return None
570 if args[2] not in [ 'UTF-8', 'UTF-16' ]:
571 print_message('Encoding must be one of "UTF-8" or "UTF-16".')
572 return None
573 return {
574 'Encoding': args[2],
575 'Language': args[3],
576 'Representation': ' '.join(args[4:])
577 }
578 if record_type == 'URI':
579 if len(args) != 3:
580 print_message('Usage: write_tag <tag> URI <uri>')
581 return None
582 return {
583 'URI': args[2]
584 }
585 print_message('Only types "Text" and "URI" are supported by this '
586 'script.')
587 return None
588
589 def do_write_tag(self, args):
590 """Handles "write_tag"."""
591 args = args.strip().split(' ')
592 if len(args) < 3:
593 print_message('Usage: write_tag <tag> [params]')
594 return
595 record_type = args[1]
596 params = self._parse_record_args(record_type, args)
597 if not params:
598 return
599 self._nfc_client.write_tag(NEARD_PATH + args[0],
600 record_type, params)
601
602 def help_write_tag(self):
603 """Prints the help message for "write_tag"."""
604 print_message('Writes the given data to a tag. Usage:\n'
605 ' write_tag <tag> Text <encoding> <language> '
606 '<representation>\n write_tag <tag> URI <uri>')
607
608 def do_read_device(self, args):
609 """Handles "read_device"."""
610 args = args.strip().split(' ')
611 if len(args) != 1 or not args[0]:
612 print_message('Usage read_device <device>')
613 return
614 self._nfc_client.show_device_data(NEARD_PATH + args[0])
615
616 def help_read_device(self):
617 """Prints the help message for "read_device"."""
618 print_message('Reads the contents of a device. Ex: read_device '
619 'nfc0/device0')
620
621 def do_push_to_device(self, args):
622 """Handles "push_to_device"."""
623 args = args.strip().split(' ')
624 if len(args) < 3:
625 print_message('Usage: push_to_device <device> [params]')
626 return
627 record_type = args[1]
628 params = self._parse_record_args(record_type, args)
629 if not params:
630 return
631 self._nfc_client.push_to_device(NEARD_PATH + args[0],
632 record_type, params)
633
634 def help_push_to_device(self):
635 """Prints the help message for "push_to_device"."""
636 print_message('Pushes the given data to a device. Usage:\n'
637 ' push_to_device <device> Text <encoding> <language> '
638 '<representation>\n push_to_device <device> URI <uri>')
639
640 def do_exit(self, args):
641 """
642 Handles the 'exit' command.
643
644 @param args: Arguments to the command. Unused.
645
646 """
647 if args:
648 print_message('Command "exit" expects no arguments.')
649 return
650 resp = raw_input('Are you sure? (yes/no): ')
651 if resp == 'yes':
652 print_message('Goodbye!')
653 self._nfc_client.end()
654 return True
655 if resp != 'no':
656 print_message('Did not understand: ' + resp)
657 return False
658
659 def help_exit(self):
660 """Handles the 'help exit' command."""
661 print_message('Exits the console.')
662
663 do_EOF = do_exit
664 help_EOF = help_exit
665
666
667def main():
668 """Main function."""
669 NfcConsole().begin()
670
671
672if __name__ == '__main__':
673 main()