在做智能家居的产品的时候,设备上跑的是个linux系统,里面有一些C/C++的和Python的程序,互相之间需要来回传一些数据,发一些信号。发现dbus这个东西可以用,Python有pydbus,但是在使用过程中,发现官方的文档确实不是那么详实,所以记录一些经验。
python这边的库本来用的是dbus-python,结果后来发现deprecated了。改成pydbus,发现简洁了很多,不过文档就更简略了。
prerequisite:
非需要一个 python3-gi, 关键是这个包在pypi里没有,没法pip安装,所以很烦人,比如在虚拟环境里,就得折腾。如果是python2,那debian系里的包应该叫python-gi。
还需要dbus,估计还需要dbus-x11
python 这边我也推荐使用pydbus。
要建立一个dbus服务,并提供method可以被其他进程调用,就下面这个例子就行:
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无限循环下去:
loop.run()
bus = SessionBus()
bus.publish("net.lew21.pydbus.ClientServerExample", MyDBUSService())
这两句是声明一个Session Bus的连接,然后把自己定义的这个服务发表出去,名字就是那一串字符。
服务类的定义很简单,method主要靠docstring的注释。里面最主要的无非就是args的类型
arg type='s' name='response' direction='out'
out 是表示这是method的返回值,s就是字符串类型,
arg type='s' name='a' direction='in'
in就是method的调用参数,s是字符串,参数名是a,
def EchoString(self, s):
return s
这个method就实现了这个相应的声明。
这个时候你如果能把这个文件运行起来,那么一个发表在dbus上的服务就开始工作了。
要调用这个dbus服务,可以在另外一个进程里,也连到dbus上,然后通过dbus根据名字获得相应的proxy对象,然后用proxy对象来调用函数。
bus = SessionBus()
obj = bus.get('com.pi.mic')
result = obj.EchoString('Hello')
然而。。这是个sessionbus,就是说这个是每个session里的,也只能跟同一个session里的进程通讯。而我的需求是系统里跑的服务,跟用户登不登录有没有界面没关系。。所以我要用SystemBus。
当然也很简单,就是把上面的SessionBus换成SystemBus就行了。
然而。。当你运行的时候,就会发现报权限错误,无法拥有那个bus名字。。
这是因为策略的问题,所以我们需要一个策略文件
https://github.com/LEW21/pydbus/blob/master/examples/polkit/dbus.conf
allow own="net.lew21.pydbus.PolkitExample"
这一句就是允许拥有这个bus名字。但是例子里这个配置是写在user="root" 里的,所以只有root能运行。如果其他用户运行,还是会出现这个错误: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"
就是允许收发消息了。
这个文件要放到/etc/dbus-1/system.d/ 目录下面。
现在,手动执行那个python脚本是可以启动服务了,但是要想把它变成开机自动启动的,还需要加systemd的配置。
https://stackoverflow.com/questions/31702465/how-to-define-a-d-bus-activated-systemd-service
按这篇答案里的方法把两个配置文件写好,应该就可以开机启动了。
有个问题要注意,调用的函数如果执行时间长,调用者会block在那等返回。如果不想这样,可以用异步函数调用方式,加两个参数。
下面的问题是,如果是method,如果客户端要call服务端的method,那么服务端就得在call之前运行起来并且在bus上发布自己,但是如果情况是,你两个进程之间要相互call,比如A有method a, B有method b,A在某些情况下要call b, B在某些情况下要call a,那么就很苦恼了,当然你可以把bus.get 写在启动以后具体需要调用的函数里。这样不会一启动就依赖另一个服务。或者还有个办法,就是使用信号signal。其实使用signal的主要场景是事件触发。比如A对象会在运行过程中产生一个事件a,他并不想关心其他进程怎么去处理这个事件,所以只需要发出一个signal即可。其他所有对这个事件感兴趣的进程只需要订阅这个signal,绑定回调函数,那么当A发出这个signal时,其他所有订阅了这个signal的进程的回调函数都会被自动调用。
signal的定义很简单,发送方在docstring里和method的声明方式一样,然后
from pydbus.generic import signal
signalname = signal()
这里要注意的是在docstring里声明的时候,signal的参数的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回发出signal的proxy对象,然后声明当收到相应的signal的时候要调用哪个回调函数。由于要接受信号并回调,所以和publish bus一样,需要用loop。
这样信号发送和接收就都写完了。
还剩下一个问题,怎么发出signal呢,只需要在发送进程里按普通调用函数方式,比如在 A 对象里的某个函数里,要发送signal,只需要self.HelloSignal('world'),就行了。
但是这个调用并不能在别的进程里通过proxy对象发,比如我不能在C进程里,obj = bus.get('com.pi.mic'), 然后obj.HelloSignal('world'),这是不行的。
signal还有一个好处就是发送方不需要等待,发送完直接返回,至于订阅的人是怎么执行,以及执行多久,就不是发送方要考虑的事情了。
有时候我们可能想监听更加自由的signal,比如不管是哪个obj的同一个名字的信号,可以用bus的subscribe 方法。这个方法有7个参数。都是可选的。
bus.subscribe(sender=None,iface=None,signal=None,object=None,arg0=None,flags=0,signal_fired=signal_fired)
先定义一个signal_fired 函数,你非要叫callback也行。参数包括 sender, object, iface, signal, params
def signal_fired(sender, object, iface, signal, params):
print("receive signal of emit event {0}".format("".join(params)))
这里params就是从发送信号的对象那发送的时候给发送函数送的参数,是个元组,如果信号没参数那就是空元组。
然后订阅事件:
这里如果你是要监听任何信号,那就全都不传就行。。。哦,你要想调用回调函数,总得传signal_fired吧。
如果你要监听通过interface为com.pi.event.emit发的信号名字为event_emit_signal的信号,就传这两个,加上signal_fired
bus.subscribe(signal="event_emit_signal", iface='com.pi.event.emit', signal_fired=signal_fired)
这样不管哪个对象发出的信号,只要是interface和signal的名字都对的上就能收到,能调用回调函数。
如果你只想关心信号名字:
bus.subscribe(signal="event_emit_signal", signal_fired=signal_fired)
写这篇的时候的pydbus版本https://github.com/LEW21/pydbus 还不支持异步函数调用。
如果需要c语言版本的dbus server的例子,可以参考 https://github.com/fbuihuu/samples-dbus