linux-bluetooth.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* contribution: bthid command-line HID utility
@ 2010-02-10 19:41 Shawn Rutledge
  0 siblings, 0 replies; only message in thread
From: Shawn Rutledge @ 2010-02-10 19:41 UTC (permalink / raw)
  To: linux-bluetooth

[-- Attachment #1: Type: text/plain, Size: 1751 bytes --]

It's always been a bit cumbersome IMO to discover, pair and connect to
a bluetooth HID device, and more so now that one needs to use dbus to
do it.  So I wrote the attached python script to make it easier.  I
would like to see it distributed as part of bluez.  Please let me know
if there is some other process for such contributions.

It does discovery and then uses urwid to present a curses menu in the
console with the discovered devices.  You can simply select one and it
will try to pair if necessary (prompting for the pin if necessary) and
then connect.  So anytime your HID device is no longer connected or
you wish to switch the computer to which it's connected (which I do
frequently), you can just put it in discoverable mode and run bthid
and not worry about whether it was already paired or not.  At least
that's the idea, although plenty of things can still go wrong
(permission trouble, .service files not installed, HCI device not
working, cryptic error messages etc.)  Maybe some of those can be
improved over time.  Of course I didn't have to invent any new
techniques here, it's just a mashup of an existing script with a
curses UI, but still... IMO the regular user needs something like this
at least as a fallback to make HID useful, in the absence of a better
GUI utility like the KDE one or some such.  Since it works over ssh,
it's better than a GUI for fixing machines that don't have wired HID
devices (e.g. I use a wireless keyboard for my MythTV box, and every
time I change the batteries I have to re-connect it).

I'm using it with bluez 4.39 at the moment, but have tested some other
versions on other machines (e.g. with arch), and I imagine it will
work the same as long as the relevant dbus interfaces are the same.

[-- Attachment #2: bthid --]
[-- Type: application/octet-stream, Size: 9562 bytes --]

#!/usr/bin/python

import sys

import urwid
import urwid.raw_display

import gobject

import dbus
import dbus.service
import dbus.mainloop.glib

# globals
mainloop = gobject.MainLoop()
deviceMap = {}	# name-to-properties
bus = ''
manager = ''
adapter = ''
selectedDeviceAddr = ''
selectedDevicePath = ''

class DialogExit(Exception):
	pass


class DialogDisplay:
	palette = [
		('body','black','light gray', 'standout'),
		('border','black','dark blue'),
		('shadow','white','black'),
		('selectable','black', 'dark cyan'),
		('focus','white','dark blue','bold'),
		('focustext','light gray','dark blue'),
		]

	def __init__(self, text, height, width, body=None):
		width = int(width)
		if width <= 0:
			width = ('relative', 80)
		height = int(height)
		if height <= 0:
			height = ('relative', 80)

		self.body = body
		if body is None:
			# fill space with nothing
			body = urwid.Filler(urwid.Divider(),'top')

		self.frame = urwid.Frame( body, focus_part='footer')
		if text is not None:
			self.frame.header = urwid.Pile( [urwid.Text(text),
				urwid.Divider()] )
		w = self.frame

		# pad area around listbox
		w = urwid.Padding(w, ('fixed left',2), ('fixed right',2))
		w = urwid.Filler(w, ('fixed top',1), ('fixed bottom',1))
		w = urwid.AttrWrap(w, 'body')

		# "shadow" effect
		w = urwid.Columns( [w,('fixed', 2, urwid.AttrWrap(
			urwid.Filler(urwid.Text(('border','  ')), "top")
			,'shadow'))])
		w = urwid.Frame( w, footer =
			urwid.AttrWrap(urwid.Text(('border','  ')),'shadow'))

		# outermost border area
		w = urwid.Padding(w, 'center', width )
		w = urwid.Filler(w, 'middle', height )
		w = urwid.AttrWrap( w, 'border' )

		self.view = w

	def set_body(self, body):
		self.frame.set_body(body)

	def add_buttons(self, buttons):
		l = []
		for name, exitcode in buttons:
			b = urwid.Button( name, self.button_press )
			b.exitcode = exitcode
			b = urwid.AttrWrap( b, 'selectable','focus' )
			l.append( b )
		self.buttons = urwid.GridFlow(l, 10, 3, 1, 'center')
		self.frame.footer = urwid.Pile( [ urwid.Divider(),
			self.buttons ], focus_item = 1)

	def button_press(self, button):
		raise DialogExit(button.exitcode)

	def main(self):
		self.ui = urwid.raw_display.Screen()
		self.ui.register_palette( self.palette )
		return self.ui.run_wrapper( self.run )

	def run(self):
		self.ui.set_mouse_tracking()
		size = self.ui.get_cols_rows()
		try:
			while True:
				canvas = self.view.render( size, focus=True )
				self.ui.draw_screen( size, canvas )
				keys = None
				while not keys:
					keys = self.ui.get_input()
				for k in keys:
					if urwid.is_mouse_event(k):
						event, button, col, row = k
						self.view.mouse_event( size,
							event, button, col, row,
							focus=True)
					if k == 'window resize':
						size = self.ui.get_cols_rows()
					k = self.view.keypress( size, k )

					if k:
						self.unhandled_key( size, k)
		except DialogExit, e:
			return self.on_exit( e.args[0] )

	def on_exit(self, exitcode):
		return exitcode, ""

	def unhandled_key(self, size, key):
		pass

class ListDialogDisplay(DialogDisplay):
	def __init__(self, text, height, width, items, has_default):
		l = []
		self.items = []
		for tag in items:
			w = MenuItem(tag)
			self.items.append(w)

			w = urwid.AttrWrap(w, 'selectable','focus')
			l.append(w)

		lb = urwid.ListBox(l)
		lb = urwid.AttrWrap( lb, "selectable" )
		DialogDisplay.__init__(self, text, height, width, lb )

		self.frame.set_focus('body')

	def unhandled_key(self, size, k):
		if k in ('up','page up'):
			self.frame.set_focus('body')
		if k in ('down','page down'):
			self.frame.set_focus('footer')
		if k == 'enter':
			# pass enter to the "ok" button
			self.frame.set_focus('footer')
			self.buttons.set_focus(0)
			self.view.keypress( size, k )

	def on_exit(self, exitcode):
		"""Print the tag of the item selected."""
		if exitcode != 0:
			return exitcode, ""
		s = ""
		for i in self.items:
			if i.get_state():
				s = i.get_label()
				break
		return exitcode, s

	def append(self, tag, val):
		l = []
		self.items.append(tag)
		for item in self.items:
			w = MenuItem(tag)
			self.items.append(w)
			w = urwid.Columns( [('fixed', 12, w),
				urwid.Text(item)], 2 )
			w = urwid.AttrWrap(w, 'selectable','focus')
			l.append(w)

		lb = urwid.ListBox(l)
		lb = urwid.AttrWrap( lb, "selectable" )
		set_body(lb)

class MenuItem(urwid.Text):
	"""A custom widget for the --menu option"""
	def __init__(self, label):
		urwid.Text.__init__(self, label)
		self.state = False
	def selectable(self):
		return True
	def keypress(self,size,key):
		if key == "enter":
			self.state = True
			raise DialogExit, 0
		return key
	def mouse_event(self,size,event,button,col,row,focus):
		if event=='mouse release':
			self.state = True
			raise DialogExit, 0
		return False
	def get_state(self):
		return self.state
	def get_label(self):
		text, attr = self.get_text()
		return text

class Rejected(dbus.DBusException):
        _dbus_error_name = "org.bluez.Error.Rejected"

class Agent(dbus.service.Object):
	exit_on_release = True

	def set_exit_on_release(self, exit_on_release):
			self.exit_on_release = exit_on_release

	@dbus.service.method("org.bluez.Agent",
									in_signature="", out_signature="")
	def Release(self):
			print "Release"
			if self.exit_on_release:
					mainloop.quit()

	@dbus.service.method("org.bluez.Agent",
									in_signature="os", out_signature="")
	def Authorize(self, device, uuid):
			print "Authorize (%s, %s)" % (device, uuid)

	@dbus.service.method("org.bluez.Agent",
									in_signature="o", out_signature="s")
	def RequestPinCode(self, device):
			print "RequestPinCode (%s)" % (device)
			return raw_input("Enter PIN Code: ")

	@dbus.service.method("org.bluez.Agent",
									in_signature="o", out_signature="u")
	def RequestPasskey(self, device):
			print "RequestPasskey (%s)" % (device)
			passkey = raw_input("Enter passkey: ")
			return dbus.UInt32(passkey)

	@dbus.service.method("org.bluez.Agent",
									in_signature="ou", out_signature="")
	def DisplayPasskey(self, device, passkey):
			print "DisplayPasskey (%s, %d)" % (device, passkey)

	@dbus.service.method("org.bluez.Agent",
									in_signature="ou", out_signature="")
	def RequestConfirmation(self, device, passkey):
			print "RequestConfirmation (%s, %d)" % (device, passkey)
			confirm = raw_input("Confirm passkey (yes/no): ")
			if (confirm == "yes"):
					return
			raise Rejected("Passkey doesn't match")

	@dbus.service.method("org.bluez.Agent",
									in_signature="s", out_signature="")
	def ConfirmModeChange(self, mode):
			print "ConfirmModeChange (%s)" % (mode)

	@dbus.service.method("org.bluez.Agent",
									in_signature="", out_signature="")
	def Cancel(self):
			print "Cancel"

def create_device_reply(device):
	global selectedDevicePath
	selectedDevicePath = device
	#~ print "New device (%s)" % (device)
	mainloop.quit()

def create_device_error(error):
	global selectedDeviceAddr, selectedDevicePath, adapter
	print error
	mainloop.quit()
	selectedDevicePath = adapter.FindDevice(selectedDeviceAddr)

def pair_if_necessary(btaddr):
	global bus
	agentpath = "/test/agent"
	#~ print "constructing agent with conn", bus
	agent = Agent(bus, agentpath)
	agent.set_exit_on_release(False)
	adapter.CreatePairedDevice(btaddr, agentpath, "DisplayYesNo",
							reply_handler=create_device_reply,
							error_handler=create_device_error)
	mainloop.run()

def show_usage():
	"""
	Display a helpful usage message.
	"""
	sys.stdout.write(
		__doc__)

def device_found(address, properties):
		icon = str(properties['Icon'])
		if (icon.startswith('input')):
			name = str(properties['Name'])
			#~ print address, ':', name
			properties['Address'] = address
			deviceMap[name] = properties
			#~ deviceListDialog.append(, str(properties["Name"]))
			#~ print "[ " + address + " ]"

			#~ for key in properties.keys():
				#~ value = properties[key]
				#~ if (key == "Class"):
					#~ print "    %s = 0x%06x" % (key, value)
				#~ else:
					#~ print "    %s = %s" % (key, value)

def property_changed(name, value):
        if (name == "Discovering" and not value):
                mainloop.quit()

def main():
	global bus, manager, adapter, deviceMap, selectedDeviceAddr, selectedDevicePath
	dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
	bus = dbus.SystemBus()
	manager = dbus.Interface(bus.get_object("org.bluez", "/"),
													"org.bluez.Manager")
	path = manager.DefaultAdapter()
	#~ print path
	adapter = dbus.Interface(bus.get_object("org.bluez", path),
													"org.bluez.Adapter")

	bus.add_signal_receiver(device_found,
					dbus_interface = "org.bluez.Adapter",
									signal_name = "DeviceFound")

	bus.add_signal_receiver(property_changed,
					dbus_interface = "org.bluez.Adapter",
									signal_name = "PropertyChanged")

	print "discovering input devices (~ 10 sec)..."
	adapter.StartDiscovery()
	mainloop.run()

	deviceListDialog = ListDialogDisplay("Select input device to connect (or control-C):",
		15, 54, deviceMap.keys(), False)
	exitcode, selection = deviceListDialog.main()
	selectedDeviceAddr = deviceMap[selection]['Address']
	#~ print selection, selectedDeviceAddr
	pair_if_necessary(selectedDeviceAddr)

	if selectedDevicePath:
		#~ print "new device", selectedDevicePath
		# Set it to trusted
		device = dbus.Interface(bus.get_object("org.bluez", selectedDevicePath), "org.bluez.Device")
		device.SetProperty("Trusted", dbus.Boolean(1))
		# Connect to it
		bus.call_blocking("org.bluez", selectedDevicePath, "org.bluez.Input", "Connect", "", ())

if __name__=="__main__":
	main()

[-- Attachment #3: screenshot1.png --]
[-- Type: image/png, Size: 19168 bytes --]

^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2010-02-10 19:41 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2010-02-10 19:41 contribution: bthid command-line HID utility Shawn Rutledge

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).