在做智能家居的產(chǎn)品的時候,設(shè)備上跑的是個linux系統(tǒng),里面有一些C/C++的和Python的程序,互相之間需要來回傳一些數(shù)據(jù),發(fā)一些信號。發(fā)現(xiàn)dbus這個東西可以用,Python有pydbus,但是在使用過程中,發(fā)現(xiàn)官方的文檔確實不是那么詳實,所以記錄一些經(jīng)驗。
python這邊的庫本來用的是dbus-python,結(jié)果后來發(fā)現(xiàn)deprecated了。改成pydbus,發(fā)現(xiàn)簡潔了很多,不過文檔就更簡略了。
prerequisite:
非需要一個 python3-gi, 關(guān)鍵是這個包在pypi里沒有,沒法pip安裝,所以很煩人,比如在虛擬環(huán)境里,就得折騰。如果是python2,那debian系里的包應(yīng)該叫python-gi。
還需要dbus,估計還需要dbus-x11
python 這邊我也推薦使用pydbus。
要建立一個dbus服務(wù),并提供method可以被其他進程調(diào)用,就下面這個例子就行:
https://github.com/LEW21/pydbus/blob/master/examples/clientserver/server.py
interface的名字起成和busname一樣也沒啥事,反正簡單需求。
from gi.repository import GLib
這句就是為啥需要那個python3-gi了。
需要它的loop
loop = GLib.MainLoop()
并且在最后讓loop無限循環(huán)下去:
loop.run()
bus = SessionBus()
bus.publish("net.lew21.pydbus.ClientServerExample", MyDBUSService())
這兩句是聲明一個Session Bus的連接,然后把自己定義的這個服務(wù)發(fā)表出去,名字就是那一串字符。
服務(wù)類的定義很簡單,method主要靠docstring的注釋。里面最主要的無非就是args的類型
arg type='s' name='response' direction='out'
out 是表示這是method的返回值,s就是字符串類型,
arg type='s' name='a' direction='in'
in就是method的調(diào)用參數(shù),s是字符串,參數(shù)名是a,
def EchoString(self, s):
return s
這個method就實現(xiàn)了這個相應(yīng)的聲明。
這個時候你如果能把這個文件運行起來,那么一個發(fā)表在dbus上的服務(wù)就開始工作了。
要調(diào)用這個dbus服務(wù),可以在另外一個進程里,也連到dbus上,然后通過dbus根據(jù)名字獲得相應(yīng)的proxy對象,然后用proxy對象來調(diào)用函數(shù)。
bus = SessionBus()
obj = bus.get('com.pi.mic')
result = obj.EchoString('Hello')
然而。。這是個sessionbus,就是說這個是每個session里的,也只能跟同一個session里的進程通訊。而我的需求是系統(tǒng)里跑的服務(wù),跟用戶登不登錄有沒有界面沒關(guān)系。。所以我要用SystemBus。
當然也很簡單,就是把上面的SessionBus換成SystemBus就行了。
然而。。當你運行的時候,就會發(fā)現(xiàn)報權(quán)限錯誤,無法擁有那個bus名字。。
這是因為策略的問題,所以我們需要一個策略文件
https://github.com/LEW21/pydbus/blob/master/examples/polkit/dbus.conf
allow own="net.lew21.pydbus.PolkitExample"
這一句就是允許擁有這個bus名字。但是例子里這個配置是寫在user="root" 里的,所以只有root能運行。如果其他用戶運行,還是會出現(xiàn)這個錯誤:GLib.Error: g-dbus-error-quark: GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: Connection ":x.xx" is not allowed to own the service "x.x.x" due to security policies in the configuration file
如果要讓其他用戶能運行,可以把這一句放到context="default" 里
后面兩句:
allow send_destination="net.lew21.pydbus.PolkitExample"
allow receive_sender="net.lew21.pydbus.PolkitExample"
就是允許收發(fā)消息了。
這個文件要放到/etc/dbus-1/system.d/ ?目錄下面。
現(xiàn)在,手動執(zhí)行那個python腳本是可以啟動服務(wù)了,但是要想把它變成開機自動啟動的,還需要加systemd的配置。
https://stackoverflow.com/questions/31702465/how-to-define-a-d-bus-activated-systemd-service
按這篇答案里的方法把兩個配置文件寫好,應(yīng)該就可以開機啟動了。
有個問題要注意,調(diào)用的函數(shù)如果執(zhí)行時間長,調(diào)用者會block在那等返回。如果不想這樣,可以用異步函數(shù)調(diào)用方式,加兩個參數(shù)。
下面的問題是,如果是method,如果客戶端要call服務(wù)端的method,那么服務(wù)端就得在call之前運行起來并且在bus上發(fā)布自己,但是如果情況是,你兩個進程之間要相互call,比如A有method ?a, B有method b,A在某些情況下要call b, B在某些情況下要call a,那么就很苦惱了,當然你可以把bus.get 寫在啟動以后具體需要調(diào)用的函數(shù)里。這樣不會一啟動就依賴另一個服務(wù)。或者還有個辦法,就是使用信號signal。其實使用signal的主要場景是事件觸發(fā)。比如A對象會在運行過程中產(chǎn)生一個事件a,他并不想關(guān)心其他進程怎么去處理這個事件,所以只需要發(fā)出一個signal即可。其他所有對這個事件感興趣的進程只需要訂閱這個signal,綁定回調(diào)函數(shù),那么當A發(fā)出這個signal時,其他所有訂閱了這個signal的進程的回調(diào)函數(shù)都會被自動調(diào)用。
signal的定義很簡單,發(fā)送方在docstring里和method的聲明方式一樣,然后
from pydbus.generic import signal
signalname = signal()
這里要注意的是在docstring里聲明的時候,signal的參數(shù)的direction都是out,
然后在需要訂閱signal的進程里,
def hello_signal_handler(hello_string):
? ? print("Received signal and it says: " +hello_string)
bus = SystemBus()
mainloop = GLib.MainLoop()
obj = bus.get('com.pi.mic')
obj.HelloSignal.connect(hello_signal_handler)
mainloop.run()
解釋一下,就是先連上bus,然后get回發(fā)出signal的proxy對象,然后聲明當收到相應(yīng)的signal的時候要調(diào)用哪個回調(diào)函數(shù)。由于要接受信號并回調(diào),所以和publish bus一樣,需要用loop。
這樣信號發(fā)送和接收就都寫完了。
還剩下一個問題,怎么發(fā)出signal呢,只需要在發(fā)送進程里按普通調(diào)用函數(shù)方式,比如在 A 對象里的某個函數(shù)里,要發(fā)送signal,只需要self.HelloSignal('world'),就行了。
但是這個調(diào)用并不能在別的進程里通過proxy對象發(fā),比如我不能在C進程里,obj = bus.get('com.pi.mic'), 然后obj.HelloSignal('world'),這是不行的。
signal還有一個好處就是發(fā)送方不需要等待,發(fā)送完直接返回,至于訂閱的人是怎么執(zhí)行,以及執(zhí)行多久,就不是發(fā)送方要考慮的事情了。
有時候我們可能想監(jiān)聽更加自由的signal,比如不管是哪個obj的同一個名字的信號,可以用bus的subscribe 方法。這個方法有7個參數(shù)。都是可選的。
bus.subscribe(sender=None,iface=None,signal=None,object=None,arg0=None,flags=0,signal_fired=signal_fired)
先定義一個signal_fired 函數(shù),你非要叫callback也行。參數(shù)包括 sender, object, iface, signal, params
def signal_fired(sender, object, iface, signal, params):
? ? ? ? print("receive signal of emit event {0}".format("".join(params)))
這里params就是從發(fā)送信號的對象那發(fā)送的時候給發(fā)送函數(shù)送的參數(shù),是個元組,如果信號沒參數(shù)那就是空元組。
然后訂閱事件:
這里如果你是要監(jiān)聽任何信號,那就全都不傳就行。。。哦,你要想調(diào)用回調(diào)函數(shù),總得傳signal_fired吧。
如果你要監(jiān)聽通過interface為com.pi.event.emit發(fā)的信號名字為event_emit_signal的信號,就傳這兩個,加上signal_fired
bus.subscribe(signal="event_emit_signal", iface='com.pi.event.emit', signal_fired=signal_fired)
這樣不管哪個對象發(fā)出的信號,只要是interface和signal的名字都對的上就能收到,能調(diào)用回調(diào)函數(shù)。
如果你只想關(guān)心信號名字:
bus.subscribe(signal="event_emit_signal", signal_fired=signal_fired)
寫這篇的時候的pydbus版本https://github.com/LEW21/pydbus 還不支持異步函數(shù)調(diào)用。
如果需要c語言版本的dbus server的例子,可以參考 https://github.com/fbuihuu/samples-dbus