kivyLauncher でバックグランドアプリケーションが作れれば世界が広がるよな、と。
実は一週間以上前からちょろっと探っていたんだけれど、今ひとつ説明がわかりにくい。一応 Kivy Planet の記事 の説明が最もまとまった説明なんだけれど、実際読んでてちょっとモヤモヤする。まず、期待通りのものなんだろうか、というのと、kivyLauncher でも通用するのか、という点。
さらっと読む限りポイントは滅茶苦茶少なくて、
- サービスの「実装」はアプリケーションのフォルダの
service/main.py
に書く。 - そのサービスを android のサービスに仕立て上げるのに、
android.AndroidService
を使う。- サービス側が「ワタシはサービスです」と主張するのではなくて、起動側が「お前はサービスだぞ」というノリ。なのはまぁそうだとは思うが。
- 気持ち悪い気もするが「サービスの実体はこれ」と明示するのではなくて、暗黙で
service/main.py
になる。 - つまり(アタシの理解では)クライアントとサービスの関係が一対一ってことである。これがキツい制約なのかどうかはやりたいこと・作り方によるだろう。
- クライアントとサービスとのコミュニケーションは
kivy.lib.osc
で。(絶対ってわけではなくて、twisted でもいい、って書かれてたりする。)
そうなのだな、と読めばだいたいわかるのだが、裏方に隠れていることが多過ぎて、「ほんとにこんなんでサービスとして振舞うわけ?」と思っちゃう。要するにサービスのインスタンスがほんとうに「ちゃんとバックグラウンドで動き続けてくれるのか」と。kivyLauncher アプリのメインの GUI のアクティビティとともに生き死にしちゃうんでは意味がない。
Kivy Planet の記事の末尾に 「A slightly more complex demo based on this can be found here.」として紹介されてた here が検証に使う元ネタとして使いやすそうだったので、これを書き換えて検証してみる。
書き換える、と言っても、要は「クライアントが死んでも生き残ってくれちゃうのか?」を検証するに足るものであればいいので、まずはサービス側をこのようにする:
1 from kivy.lib import osc
2 from time import localtime, asctime, sleep
3 import uuid
4 import base64
5
6 _INSTANCE_ID = base64.urlsafe_b64encode(
7 uuid.uuid4().bytes).rstrip('=')
8 oscid = None
9
10
11 def ping(*args):
12 # when client call '/ping', then we response as /message
13 osc.sendMsg(
14 '/message',
15 ["{}, {}".format(_INSTANCE_ID, oscid), ],
16 port=3002)
17
18
19 def send_date():
20 osc.sendMsg('/date', ["?? " + asctime(localtime()), ], port=3002)
21
22
23 if __name__ == '__main__':
24 osc.init()
25 global oscid
26 oscid = osc.listen(ipAddr='0.0.0.0', port=3000)
27 osc.bind(oscid, ping, '/ping')
28 while True:
29 osc.readQueue(oscid)
30 send_date()
31 sleep(.1)
「ワタシはサービスです」と主張する何かはなく、osc でのコミュニケーションを取っているだけ。(というか osc で、ということがサービスの証なのかもしれんけど。) アタシが書き換えたのはサービスの応答がこの main.py のインスタンスを特定するものを返すようにしただけ。
で、クライアント:
1 __version__ = '0.1'
2
3 from kivy.app import App
4 from kivy.lang import Builder
5 from kivy.lib import osc
6 from kivy.clock import Clock
7
8 kv = '''
9 BoxLayout:
10 orientation: 'vertical'
11 BoxLayout:
12 size_hint_y: None
13 height: '30sp'
14 Button:
15 text: 'start service'
16 on_press: app.start_service()
17 Button:
18 text: 'stop service'
19 on_press: app.stop_service()
20
21 ScrollView:
22 Label:
23 id: label
24 size_hint_y: None
25 height: self.texture_size[1]
26 text_size: self.size[0], None
27
28 BoxLayout:
29 size_hint_y: None
30 height: '30sp'
31 Button:
32 text: 'ping'
33 on_press: app.send()
34 Button:
35 text: 'clear'
36 on_press: label.text = ''
37 Label:
38 id: date
39 size_hint_y: None
40 height: '30sp'
41 '''
42
43
44 class ClientServerApp(App):
45 def build(self):
46 self.service = None
47
48 osc.init()
49 oscid = osc.listen(port=3002)
50
51 # when we /ping to service, then our service will
52 # response as /message
53 osc.bind(oscid, self.display_message, '/message')
54
55 # our service will send to /date unilaterally
56 osc.bind(oscid, self.date, '/date')
57
58 self.root = Builder.load_string(kv)
59
60 #
61 Clock.schedule_interval(
62 lambda *x: osc.readQueue(oscid), 0)
63
64 return self.root
65
66 def start_service(self):
67 from android import AndroidService
68 service = AndroidService('my pong service', 'running')
69 service.start('service started')
70 self.service = service
71
72 def stop_service(self):
73 if self.service:
74 self.service.stop()
75 self.service = None
76
77 def send(self, *args):
78 osc.sendMsg('/ping', [], port=3000)
79
80 def display_message(self, message, *args):
81 self.root.ids.label.text += '%s\n' % message[2]
82
83 def date(self, message, *args):
84 self.root.ids.date.text = message[2]
85
86
87 if __name__ == '__main__':
88 ClientServerApp().run()
android.txt はいつも通り kivyLauncher で動かすには必要だが、service/ フォルダ側にはいらない。一応:
1 title=Kivy Service Demo
2 author=original author is tshirtman
3 orientation=portrait
オリジナルの「here」は検証には迷惑な構造になっていて、起動するや start_service()
しておったが、無論そりゃぁおかしい。
「やや複雑」と言っている通りで少々は入り組んでるがこういうこと:
- サービスは一方的に /date に投げ付け続けている
- クライアントが /ping するとサービスが /message で応答
これだけ。
てわけで動かしてみた:
うんうん、期待通りね。
ところで、「サービスさんよ、お主、生きておるのか?」の問い合わせって出来ないんかなぁ、と思ったんだけど、これは ping 的なハートビートを必ず実装せい、てことなのかいね??
さーて、世界が広がったぞ。重力とか気圧とか、あるいはそもそも GPS の情報も、「記録し続けてみたい」かったりするのですね。例えば散歩の記録みたいなことは簡単に出来るよね。
と、ここまで書いてみてから AndroidService が「Old p4a toolchain doc」というカテゴリに属していることに気付く。うーん? 使っていいの?
代わりのもの、はServicesか。service/main.py
なノリは同じだね。なんだこれ、start_service
はあるのに stop_service
について書かれてない。ないの? なかったら困るさね…。あ、あった。あらぁ? でもこれ、AndroidService と同じ場所じゃないのかね?
仕方ない、ひとまず試すか…:
1 __version__ = '0.1'
2
3 from kivy.app import App
4 from kivy.lang import Builder
5 from kivy.lib import osc
6 from kivy.clock import Clock
7 import android
8
9 kv = '''
10 BoxLayout:
11 orientation: 'vertical'
12 BoxLayout:
13 size_hint_y: None
14 height: '30sp'
15 Button:
16 text: 'start service'
17 on_press: app.start_service()
18 Button:
19 text: 'stop service'
20 on_press: app.stop_service()
21
22 ScrollView:
23 Label:
24 id: label
25 size_hint_y: None
26 height: self.texture_size[1]
27 text_size: self.size[0], None
28
29 BoxLayout:
30 size_hint_y: None
31 height: '30sp'
32 Button:
33 text: 'ping'
34 on_press: app.send()
35 Button:
36 text: 'clear'
37 on_press: label.text = ''
38 Label:
39 id: date
40 size_hint_y: None
41 height: '30sp'
42 '''
43
44
45 class ClientServerApp(App):
46 def build(self):
47 self.service = None
48
49 osc.init()
50 oscid = osc.listen(port=3002)
51
52 # when we /ping to service, then our service will
53 # response as /message
54 osc.bind(oscid, self.display_message, '/message')
55
56 # our service will send to /date unilaterally
57 osc.bind(oscid, self.date, '/date')
58
59 self.root = Builder.load_string(kv)
60
61 #
62 Clock.schedule_interval(
63 lambda *x: osc.readQueue(oscid), 0)
64
65 return self.root
66
67 def start_service(self):
68 android.start_service(
69 title='my pong service',
70 description='service description...',
71 arg='running')
72
73 def stop_service(self):
74 android.stop_service()
75
76 def send(self, *args):
77 osc.sendMsg('/ping', [], port=3000)
78
79 def display_message(self, message, *args):
80 self.root.ids.label.text += '%s\n' % message[2]
81
82 def date(self, message, *args):
83 self.root.ids.date.text = message[2]
84
85
86 if __name__ == '__main__':
87 ClientServerApp().run()
どう考えても同じコードになってるような気がする。うーん、わがらん。ともあれこちらのコードでも同じ結果になった。そりゃぁそうだろうなぁという気もするし…。
ちなみに「kivyLauncher にこだわらないなら」、service/main.py 以外のスクリプトを使うことも出来る、けど今は kivyLauncher でお気楽に、がマイブームなのでアタシはまだやらない。
おっと…あぶない、あぶない。これは書いとかないと確実に迷子になる。なんかドキュメントが整理されてなくてやんなっちゃうんだけど、ともあれ偶然これを見つけてた。それ自体は「どーして動かないんですかぁ?」の内容が「そりゃそーだ」なトホホな質問なんだけど、注目すべきはそれではなくて
arg = os.getenv('PYTHON_SERVICE_ARGUMENT')
。これが start_service
に渡す arg
を取ってるらしい。
ので上の例だと例えば「サービスとクライアント両方でポート番号管理すんのやだ!」てことで:
1 from kivy.lib import osc
2 from time import localtime, asctime, sleep
3 import uuid
4 import base64
5
6 _INSTANCE_ID = base64.urlsafe_b64encode(
7 uuid.uuid4().bytes).rstrip('=')
8 oscid = None
9 ports = None
10
11 def ping(*args):
12 # when client call '/ping', then we response as /message
13 osc.sendMsg(
14 '/message',
15 ["{}, {}".format(_INSTANCE_ID, oscid), ],
16 port=ports[1])
17
18
19 def send_date():
20 osc.sendMsg('/date', ["?? " + asctime(localtime()), ],
21 port=ports[1])
22
23
24 if __name__ == '__main__':
25 import os
26 global ports
27 ports = map(
28 int, os.getenv('PYTHON_SERVICE_ARGUMENT').split(","))
29
30 osc.init()
31 global oscid
32 oscid = osc.listen(ipAddr='0.0.0.0', port=ports[0])
33 osc.bind(oscid, ping, '/ping')
34 while True:
35 osc.readQueue(oscid)
36 send_date()
37 sleep(.1)
1 def start_service(self):
2 android.start_service(
3 title='my pong service',
4 description='service description...',
5 arg='3000,3002')
なんてことも出来る。
ていうかもちっとドキュメントちゃんとして…。