2013年9月11日 星期三

python gtk+ glade and dbus work together

這篇的篇名下得有點奇怪,我也不知如何命名之,主要是想要表達整合這幾個技術:
  • 用python語言
  • 使用gtk+來做出圖形界面
  • 利用glade工具來編排圖形界面的佈局,而不是用程式來寫
  • 結合dbus來控制圖形界面
之前有看到python dbus的範例程式:參見Python DBus教學精要[1]example-service.py其程式主要架構為:
class CLASSNAME(dbus.service.Object)
  @dbus.service.method("INTERFACE NAME")
  def METHOD_NAME(self):
    # do something here

if __name__ == '__main__':
  # request dbus public name, instantiate object for dbus
  # gobject mainloop run
注意到我們必須建立GObject的事件處理迴圈,因為這是在寫DBus服務,而服務是不會跑完馬上停止的,而是必須一直在等待是否有客戶端的要求進來。

另外也看到可以用python配合glade來寫gtk+的圖形界面: 參見Python Gtk glade開發GUI程式[2]。其中python的程式為:
from gi.repository import Gtk

class Handler:
    def onDeleteWindow(self, *args):
        Gtk.main_quit(*args)

    def onButtonPressed(self, button):
        print "Hello World!"

builder = Gtk.Builder()
builder.add_from_file("builder_example.glade")
builder.connect_signals(Handler())

window = builder.get_object("window1")
window.show_all()

Gtk.main()
同樣,圖形界面也是程式跑起來後,必須一直等待使用者的輸入事件發生。所以其實和服務的程式寫法一樣,都必須有個事件迴圈。注意這裡的事件迴圈使用的是Gtk.main(),這下讓我想將兩者合在一起時,真不知道要使用那一個才是,或者要兩者一起用,或是要用其他的方式做呢?
這個問題的答案在這一篇文how-export-methods-with-dbus-in-a-extended-class-in-python-inherited-methods[3]裡找到了一個可行的答案。其實此文不是在討論這個問題,只是他的範例程式也正好給了我們答案。他使用了Gtk.main()。但是也下了DBusGMainLoop()。
我將他的程式稍加修改:
#! /usr/bin/env python

# interface imports
from gi.repository import Gtk

# dbus imports  
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop

# Main window class
class Window(dbus.service.Object):
 def onDeleteWindow(self, *args):
  Gtk.main_quit(*args)
  
 def onButtonPressed(self, button):
  label3 = self.builder.get_object("label3") 
  label3.set_text("o............o")
  print "Hello World!"
  
 def on_dialog_pressed(self, button):
  self.show_dial()
  
 def __init__(self, gladeFilePath, name):
  self.builder = Gtk.Builder()
  self.builder.add_from_file(gladeFilePath)
  self.builder.connect_signals(self)
  self.window = self.builder.get_object("window1")
  self.dialog = self.builder.get_object("dialog1")
  self.name = name
  self.busName = dbus.service.BusName('com.example.MyTest', bus=dbus.SessionBus())
  dbus.service.Object.__init__(self, self.busName, '/com/example/MyInterface/' + self.name)
  
 @dbus.service.method('com.example.MyInterface.Window')
 def show(self):
  self.window.show_all()
 
 @dbus.service.method('com.example.MyInterface.Window')
 def show_dial(self):
  label2 = self.builder.get_object("label2") 
  label2.set_text("ok!.............")
  self.dialog.show()
  response = self.dialog.run()
  print response
  #do stuff
  self.dialog.hide()
  
 @dbus.service.method('com.example.MyInterface.Window')
 def hide(self):
  self.window.hide()
  
 @dbus.service.method('com.example.MyInterface.Window')
 def destroy(self):
  Gtk.main_quit()
  
 @dbus.service.method('com.example.MyInterface.Window')
 def update(self, data):
  print(update)

# Child window class
class WindowOne(Window):
    def __init__(self, gladeFilePath):
        Window.__init__(self, gladeFilePath, "WindowOne")

    @dbus.service.method('com.example.MyInterface.WindowOne')
    def update(self, data):
  # reimplementation of top class 'update' method
        print "One update"


if __name__ == "__main__":
    DBusGMainLoop(set_as_default=True)

    gladeFilePath = "builder_example2.glade"
    windowOne = WindowOne(gladeFilePath)

    Gtk.main()
Python程式的好處之一就是程式區塊不使用{}或者begin,end關鍵字來表示,而是使用縮格來表示,如此一來程式會短一點,也比較容易閱讀。但是我在改這支程式時也被縮格給玩了好久。可能是我程式由網頁直接複製貼上編輯器的關係,也許有一些看不到的HTML碼也被貼上了,所以程式一跑,就一直跟我抱怨縮格沒對到。以下是已測過可正常執行的程式碼:
其中的builder_exampler2.glade是使用glade工具做出來的,如Fig.1, Fig.2所示,我們拉了一個視窗(window)和一個對話框(dialog)。
Fig.1
Fig.2
$ ./glade-dbus.py
 
執行程式後,什麼東西都不會出現!
此時請先打開d-feet,切到Session Bus,為了快一點找到我們寫的服務,在篩選器輸入『com.example』就會看到我們的服務,如Fig.3所示。
Fig.3
請找到show()方法,直接按兩下後,會出現一個視窗,直接按『執行』按鍵就會看到出現了一個視窗,沒錯,這就是我們用glade設計的那一個視窗;如果要隱藏視窗,同樣可以在d-feet找到hide()方法並執行之就可以了。這表示,我們寫的程式既是一個圖形界面的程式,也是一個DBus的服務,而且使用者可以透過叫用DBus的方法(包括使用d-feet, dbus-send指令等)來操作圖形界面。
Fig.4
例如,當Fig.4的視窗出現時,我可以按一下其中的「dialog」按鍵來叫出一個對話框(Fig.5),我也可以在d-feet中找到show_dial()方法,執行它來叫出這個對話框。或者也可以透過執行以下dbus-send指令來做到同樣的動作。
$ dbus-send --print-reply --dest=com.example.MyTest /com/example/MyInterface/WindowOne com.example.MyInterface.Window.show_dial

method return sender=:1.233 -> dest=:1.240 reply_serial=2
Fig.5
最後提到一個技巧,我想要一個比較有彈性的對話框,每次叫出它之前可以改變它的提示文字,就像Fig.5所示,它的藍色文字其實是程式動態填上的。使用的程式寫法就是利用builder.get_object()取得對其Label的參考,再由此參考的set_text()方法來填入我們想要的文字。

檢視改寫的程式後,發現一個Class要做太多事(dbus, gtk+),所以再修改為以下版本:
#! /usr/bin/env python

# interface imports
from gi.repository import Gtk

# dbus imports  
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop

# dbus class
class DBus(dbus.service.Object):
 def __init__(self, window, name):
  self.window = window
  self.name = name
  self.busName = dbus.service.BusName('com.example.MyTest', bus=dbus.SessionBus())
  dbus.service.Object.__init__(self, self.busName, '/com/example/MyInterface/' + self.name)
  
 @dbus.service.method('com.example.MyTest')
 def show(self):
  self.window.show_all()
  
 @dbus.service.method('com.example.MyTest')
 def hide(self):
  self.window.hide()
  
# Main window class
class Window:
 def onDeleteWindow(self, *args):
  Gtk.main_quit(*args)
  
 def onButtonPressed(self, button):
  label3 = self.builder.get_object("label3") 
  label3.set_text("o............o")
  print "Hello World!"
  
 def on_dialog_pressed(self, button):
  label2 = self.builder.get_object("label2") 
  label2.set_text("ok!.............")
  self.dialog.show()
  response = self.dialog.run()
  print response
  #do stuff
  self.dialog.hide()
  
 def __init__(self, gladeFilePath, name):
  self.builder = Gtk.Builder()
  self.builder.add_from_file(gladeFilePath)
  self.builder.connect_signals(self)
  self.window = self.builder.get_object("window1")
  self.dialog = self.builder.get_object("dialog1")

if __name__ == "__main__":
    DBusGMainLoop(set_as_default=True)

    gladeFilePath = "builder_example2.glade"
    windowOne = Window(gladeFilePath, "Test")
    dbus = DBus(windowOne.window, "Test")

    Gtk.main() 
這個版本把原本的單一Class分為兩個Class,一個專司dbus的工作,另一個專司gtk+的工作。負責dbus的Class在需要用到GUI操作時,再去叫用另一個專司gtk+的Class來工作就可以了。這樣程式看來比原來的又簡潔了一些。
有一個特別的應用,可以把這個技術用上,就是配對藍牙設備。Bluez的source code解開後,在頂層目錄有一個/test的目錄,裡面有一支叫做simple-agent的python程式,大家都知道如果要在命令列配對藍牙設備,其中一個方法就是去用這支程式。使用方法:
$ simple-agent hci0 00:07:61:xx:xx:xx
RequestPinCode (/org/bluez/2345/hci0/dev_00_07_61_xx_xx_xx)
Enter PIN Code: 1234
Release
New device (/org/bluez/2345/hci0/dev_00_07_61_xx_xx_xx)
Ok, 這是命令列指令,如果我們想將其改寫成GUI界面的程式呢? 先研究一下simple-agent吧!我們發現其實它有一個Class叫做Agent,和這裡的範例程式的Class DBus或者原先的Class Window很像,都是會公開至DBus。如Fig.6所示。只是在我們的範例中,我們透過程式公開出來的方法,讓使用者可以由dbus來操作GUI,但是在simple-agent裡,其Class Agent是用來給bluez叫用的。所以我們可以簡單的改寫原來的範例程式,就可以達到目的:
Fig.6
#! /usr/bin/env python
# pair.py
# interface imports
from gi.repository import Gtk

# dbus imports  
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop

class Rejected(dbus.DBusException):
 _dbus_error_name = "org.bluez.Error.Rejected"
 
class Agent(dbus.service.Object):
 exit_on_release = True

 def __init__(self, window, bus, path):
  self.window = window
  self.path = path
  self.bus = bus
  dbus.service.Object.__init__(self, self.bus, self.path)

 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:
   Gtk.main_quit()

 @dbus.service.method("org.bluez.Agent",
     in_signature="os", out_signature="")
 def Authorize(self, device, uuid):
  print "Authorize (%s, %s)" % (device, uuid)
  authorize = raw_input("Authorize connection (yes/no): ")
  if (authorize == "yes"):
   return
  raise Rejected("Connection rejected by user")

 @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):
  prompt_text = "RequestConfirmation (%s, %d)" % (device, passkey)
  confirm = self.window.dialog_result(prompt_text)
  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)
  authorize = raw_input("Authorize mode change (yes/no): ")
  if (authorize == "yes"):
   return
  raise Rejected("Mode change by user")

 @dbus.service.method("org.bluez.Agent",
     in_signature="", out_signature="")
 def Cancel(self):
  print "Cancel"
  
# Main window class
class Window:
 def onDeleteWindow(self, *args):
  Gtk.main_quit(*args)
  
 def onButtonPressed(self, button):
  my_entry = self.builder.get_object("entry1")
  t1 = my_entry.get_text()
  self.adapter.CreatePairedDevice(t1, self.path, self.capability,
     reply_handler=create_device_reply,
     error_handler=create_device_error)
  
 def dialog_result(self, prompt_text):
  #label1 = self.builder.get_object("label1") 
  #label1.set_text(device)
  label2 = self.builder.get_object("label2") 
  label2.set_text(prompt_text)
  self.dialog.show()
  response = self.dialog.run()
  #do stuff
  self.dialog.hide()
  if (response == 1):
   return 'yes'
  return 'no'
  
 def __init__(self, gladeFilePath, name, adapter, path, capability):
  self.builder = Gtk.Builder()
  self.builder.add_from_file(gladeFilePath)
  self.builder.connect_signals(self)
  self.window = self.builder.get_object("window1")
  self.dialog = self.builder.get_object("dialog1")
  self.adapter = adapter
  self.path = path
  self.capability = capability
  self.window.show_all()

def create_device_reply(device):
 print "New device (%s)" % (device)

def create_device_error(error):
 print "Creating device failed: %s" % (error)

if __name__ == "__main__":
    DBusGMainLoop(set_as_default=True)
    
    bus = dbus.SystemBus()
    manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.bluez.Manager")
    capability = "DisplayYesNo"
    path = manager.DefaultAdapter()
    adapter = dbus.Interface(bus.get_object("org.bluez", path), "org.bluez.Adapter")
    path = "/test/agent"
    
    gladeFilePath = "pair.glade"
    windowOne = Window(gladeFilePath, "Test", adapter, path, capability)
    
    agent = Agent(windowOne, bus, path)
    agent.set_exit_on_release(False)

    Gtk.main()
Fig.7
Fig.8
程式一執行,會出現Fig.8的視窗,在此我們可以輸入想要配對的藍牙設備的MAC位址,然後按Go Pair按紐。如果你的藍牙設備是比較新的,就會出現如Fig.7所示的對話框。告訴你現在要配對的裝置應該會顯示某個數字(passkey),如果裝置上的數字和此相同,請按Yes, 否則按No。用這個程序就可以完成藍牙設備的配對工作。因為我手上的設備是比較新的,所以我只改寫這個部份的程式,如果你手上是比較舊的設備,例如5,6年前的手機,那麼就不會是這個程序(passkey),而可能是要求輸入PIN碼,此部份程式我沒有修改,請自行依照我的修改方式進行程式改寫就可以了。

參考來源:
[1] 石頭成, Python DBus教學精要, 石頭閒語, 2011/4/14
[2] 小白和小叮叮, Python Gtk glade開發GUI程式, 2013/5/14
[3] Msum, How export methods with dbus in a extended class in python, inherited methods?, stackoverflow, 2012/12/7
張貼留言