「石垣一夜城で青空デバッグ、的な」で色々気になったので。
結構 GPS のデータを取れてない場所があるのね。当たり前なんだろうとは思うものの、どの程度当たり前なのかがよくわからないのと、「取れてないぜっ」てのを検知出来ないのかしらねぇ、てのと。それに sqlite3 のエラー時にデータを捨てるのもなんかもったいないかな、とも思い。
ここに行った:
無論これ自体は特に目的ではない。ちなみに夏でも北風運用である。(34R DEP/ARV、34L ARV、05 DEP。)
仕込んでいったプログラムは前回からの変更点としてはポイントとしては3つ:
- 地理院タイルに変えた。これは今日のためではなくて。
- データ記録時点での sqlite3.OperationalError 時はデータは捨てずに pending として取って置いて、次回の機会に突っ込む。
- gps からの on_status イベントを kivy ログに吐き出しつつ、ついでに sqlite3 にも記録してみる
1 # -*- coding: utf-8 -*-
2 import sys
3 # please edit next line for your environment.
4 sys.path.append("/sdcard/kivy-my-site-packages")
5
6 import json
7 import math
8 from kivy.app import App
9 from kivy.lang import Builder
10 from kivy.uix.boxlayout import BoxLayout
11 from kivy.lib import osc
12 from kivy.clock import Clock
13 import android
14 from kivy.garden.mapview import MapView, MapMarkerPopup, MapSource
15
16 kv = '''
17 #: import MapSource kivy.garden.mapview.MapSource
18 <SensorLoggingServiceLauncher>:
19 orientation: 'vertical'
20 label: label
21 map: map
22
23 BoxLayout:
24 size_hint: (1, None)
25 height: ping_btn.height
26 Button:
27 id: ping_btn
28 text: 'ping'
29 on_press: root.send_ping()
30 size_hint: (1, None)
31 height: self.texture_size[1] * 3
32 Button:
33 text: 'start service'
34 on_press: root.start_service()
35 size_hint: (1, None)
36 height: self.texture_size[1] * 3
37 Button:
38 text: 'stop service'
39 on_press: root.stop_service()
40 size_hint: (1, None)
41 height: self.texture_size[1] * 3
42 RstDocument:
43 id: label
44 size_hint: (1, None)
45 height: 350
46 MapView:
47 id: map
48 zoom: 15
49 '''
50 Builder.load_string(kv)
51 _PING_PORT = 3010
52 _RESPONSE_PORT = 3012
53
54
55 class SensorLoggingServiceLauncher(BoxLayout):
56 def __init__(self):
57 super(SensorLoggingServiceLauncher, self).__init__()
58 self.map.map_source = MapSource(
59 url='http://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
60 attribution='http://maps.gsi.go.jp/development/ichiran.html')
61
62 osc.init()
63 oscid = osc.listen(port=_RESPONSE_PORT)
64 osc.bind(
65 oscid,
66 self.on_service_response, '/response')
67 Clock.schedule_interval(
68 lambda *x: osc.readQueue(oscid), 0)
69 self._ping_scev = Clock.schedule_interval(self.send_ping, 1.)
70
71 def start_service(self):
72 android.start_service(
73 title='sensor logging service',
74 description='casual sensor logging service',
75 arg=json.dumps({
76 'ping_port': _PING_PORT,
77 'response_port': _RESPONSE_PORT,
78 'check_interval': 5 * 60, # [secs]
79 'check_distance': 10, # [m]
80 }))
81 if not self._ping_scev:
82 self._ping_scev = Clock.schedule_interval(self.send_ping, 1.)
83
84 def stop_service(self):
85 android.stop_service()
86 self.label.text = ''
87
88 def send_ping(self, *arg, **kwarg):
89 osc.sendMsg('/ping', [], port=_PING_PORT)
90
91 def on_service_response(self, message, *args):
92 Clock.unschedule(self._ping_scev)
93 self._ping_scev = None
94 response = json.loads(message[2])
95 result = response['last']
96
97 self.label.text = "{}\n\n".format(response['message'])
98 last_lon, last_lat = None, None
99 for time, lon, lat, alt, pres, gX, gY, gZ, gps_stype, gps_status in result:
100
101 self.label.text += """
102 * lon={:.5f}, lat={:.5f}, alt={:.2f}, \
103 pressure={:.2f}, g={:.6f}, (gps_stype, gps_status)=({}, {})
104 """.format(lon, lat, alt, pres, math.sqrt(gX**2 + gY**2 + gZ**2), gps_stype, gps_status)
105
106 self.map.add_marker(
107 MapMarkerPopup(lon=lon, lat=lat))
108 last_lon, last_lat = lon, lat
109
110 if last_lon:
111 self.map.center_on(last_lat, last_lon)
112
113
114 class SensorLoggingServiceLauncherApp(App):
115 def build(self):
116 return SensorLoggingServiceLauncher()
117
118
119 if __name__ == '__main__':
120 SensorLoggingServiceLauncherApp().run()
1 # -*- coding: utf-8 -*-
2 import json
3 import sqlite3
4 from kivy.lib import osc
5 from time import sleep
6 from kivy.clock import Clock, mainthread
7 from kivy.logger import Logger
8 from sensor_logging import SensorLogging
9
10 class SensorLoggingService(object):
11 def __init__(self, conf):
12 self._conf = conf
13 Logger.info(str(conf))
14 self.log = SensorLogging(self._conf)
15
16 def run(self):
17 Logger.info("run START")
18 osc.init()
19 self.oscid = osc.listen(
20 ipAddr='0.0.0.0',
21 port=self._conf['ping_port'])
22 osc.bind(
23 self.oscid,
24 self.ping, '/ping')
25
26 t = 0.0
27 delta_t = 0.5
28 while True:
29 osc.readQueue(self.oscid)
30 if self.log.dirty or t >= self._conf['check_interval']:
31 self.log.do_log()
32 t = 0.0
33 sleep(delta_t)
34 t += delta_t
35
36 def ping(self, *args):
37 """just for heart-beat."""
38 Logger.info("received ping")
39 res = []
40 try:
41 res = self.log.query_last(30)
42 except sqlite3.OperationalError as e:
43 Logger.info("query_last failed: {}".format(str(e)))
44 osc.sendMsg(
45 '/response',
46 [json.dumps({
47 'message': 'service is now active',
48 'last': res
49 })],
50 port=self._conf['response_port'])
51
52 if __name__ == '__main__':
53 import os
54 import json
55
56 SensorLoggingService(
57 conf=json.loads(os.getenv('PYTHON_SERVICE_ARGUMENT'))
58 ).run()
1 # -*- coding: utf-8 -*-
2 import sys
3 from datetime import datetime
4 import sqlite3
5 from kivy.clock import Clock, mainthread
6 from kivy.logger import Logger
7 from plyer import gps
8 from gravity_sensor import GravitySensor
9 from environmental_sensors import pressureSensor
10 from sensor_logging_data import LogPhysical
11
12
13 grav_sensor = GravitySensor()
14 # please edit next line for your environment.
15 sys.path.append("/sdcard/kivy-my-site-packages")
16 from geographiclib.geodesic import Geodesic
17 _WGS84 = Geodesic.WGS84
18
19
20 _ERA = datetime(1970, 1, 1)
21
22
23 class SensorLogging(object):
24 _last_lon = None
25 _last_lat = None
26 _last_alt = None
27 _gps_stype = None
28 _gps_status = None
29 dirty = False
30 _pending = []
31
32 def __init__(self, conf):
33 Logger.debug("SensorLogging.__init__")
34 self._conf = conf
35 self._log = LogPhysical()
36
37 pressureSensor.enable()
38 grav_sensor.enable()
39 gps.configure(on_location=self.on_location,
40 on_status=self.on_status)
41 gps.start()
42
43 def on_location(self, **kwargs):
44 Logger.debug("do_location")
45 lon = float(kwargs['lon'])
46 lat = float(kwargs['lat'])
47 alt = float(kwargs['altitude'])
48 last_lon = self._last_lon
49 last_lat = self._last_lat
50 last_alt = self._last_alt
51
52 dis = self._conf['check_distance'] + 1
53 if last_lon is not None:
54 dis = _WGS84.Inverse(last_lat, last_lon, lat, lon)['s12']
55 if dis > self._conf['check_distance']:
56 self.dirty = True
57 self._last_lon = lon
58 self._last_lat = lat
59 self._last_alt = alt
60
61 def on_status(self, stype, status):
62 self._gps_stype = str(stype)
63 self._gps_status = str(status)
64 Logger.info("[{}] on_status: {}, {}".format(
65 datetime.now().strftime("%m-%d %H:%M:%S"),
66 stype, status))
67
68 @property
69 def current(self):
70 return (
71 (datetime.utcnow() - _ERA).total_seconds(),
72 self._last_lon,
73 self._last_lat,
74 self._last_alt,
75 pressureSensor.value,
76 grav_sensor.values,
77 self._gps_stype,
78 self._gps_status)
79
80 def query_last(self, num):
81 return self._log.query_last(num)
82
83 def do_log(self, *args, **kwargs):
84 Logger.debug("do_log")
85 self._pending.append(self.current)
86 try:
87 while self._pending:
88 rec = self._pending[0]
89 self._log.insert_new(*rec)
90 del self._pending[0]
91 self.dirty = False
92 except sqlite3.OperationalError as e:
93 Logger.info("[{}] {}: try next time...".format(
94 datetime.now().strftime("%m-%d %H:%M:%S"),
95 str(e)))
1 import os
2 import sqlite3
3 import sys
4 # please edit next line for your environment.
5 sys.path.append("/sdcard/kivy-my-site-packages")
6 from geographiclib.geodesic import Geodesic
7 _WGS84 = Geodesic.WGS84
8 inverse = _WGS84.Inverse
9
10
11 _DBFILE = "./sensor_log.dat"
12
13 _created = os.path.exists(_DBFILE)
14
15
16 class LogPhysical(object):
17 def __init__(self):
18 self._conn = sqlite3.connect(_DBFILE)
19
20 global _created
21 if not _created:
22 cur = self._conn.cursor()
23 cur.executescript('''\
24 CREATE TABLE sensor_log (
25 time FLOAT NOT NULL,
26 lon FLOAT,
27 lat FLOAT,
28 alt FLOAT,
29 pressure FLOAT,
30 grav_x FLOAT,
31 grav_y FLOAT,
32 grav_z FLOAT,
33 gps_stype TEXT,
34 gps_status TEXT
35 );
36 ''')
37 self._conn.commit()
38 _created = True
39 cur.close()
40
41 def insert_new(
42 self, time, lon, lat, alt, pressure, grav, gps_stype, gps_status):
43
44 grav_x, grav_y, grav_z = grav
45
46 cur = self._conn.cursor()
47 cur.execute(
48 """INSERT INTO sensor_log(
49 time, lon, lat, alt, pressure, grav_x, grav_y, grav_z, gps_stype, gps_status
50 )
51 VALUES (
52 ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
53 )""", (time, lon, lat, alt, pressure, grav_x, grav_y, grav_z, gps_stype, gps_status))
54 self._conn.commit()
55 cur.close()
56
57 def query_last(self, num=None):
58 cur = self._conn.cursor()
59 cur.execute("""SELECT
60 time, lon, lat, alt, pressure, grav_x, grav_y, grav_z, gps_stype, gps_status
61 FROM sensor_log
62 ORDER BY time DESC""")
63 result = []
64 for row in cur:
65 time, lon, lat, alt, pressure, grav_x, grav_y, grav_z, gps_stype, gps_status = row
66 result.append((time, lon, lat, alt, pressure, grav_x, grav_y, grav_z, gps_stype, gps_status))
67 if len(result) >= 3:
68 # reduce noise
69 i = len(result) - 3
70 d01 = inverse(
71 result[i ][2], result[i ][1],
72 result[i + 1][2], result[i + 1][1])['s12']
73 d02 = inverse(
74 result[i ][2], result[i ][1],
75 result[i + 2][2], result[i + 2][1])['s12']
76 d12 = inverse(
77 result[i + 1][2], result[i + 1][1],
78 result[i + 2][2], result[i + 2][1])['s12']
79 if d02 < d01 or d02 < d12:
80 del result[i + 1]
81 if num and len(result) == num:
82 break
83 cur.close()
84 return list(reversed(result))
結論からは、「どうしようもない」かなぁと。これ:
空港付近でデータが大きく欠落してるのがわかる? 伝わりにくいかな。ここは常識的には当然なのね。トンネルの中だから(京急線である)。そうなんだけど、ここでは特に on_status で何か拾えてるわけではない。のに、京急蒲田に着いたか降りたかのあたりで急に on_status が反応して「gps: temporarily-unavailable」とのたまわったあとで、今度はなぜか延々「gps: available」とリポートし続けてる。なんでぢゃ。てわけで全然役に立ちそうにない。
まぁ当然のこと、つまりは「取れないものは取れない」がわかった、てことで今日はよしとしておこうか。
GPS 以外のセンサーで追跡を補えないもんかなぁ、と探ってみてるけど、たぶんむつかしいであろうな。加速度センサーで少し補える可能性はないか、と思うも、そもそも電車とかに乗って「慣性の法則」に従った状態で電車の外からみた加速度を検知出来るとも思えないわけで。これはこれで実験はしてみたいけど、たぶん期待したものにはならないであろうなと思う。→というかそもそも加速度センサーは小さなノイズが常に乗っかってることがやってみてすぐにわかったのね。これはローパスフィルタであるとかでゴミを取り除いて使うのが「普通」なんですと。