青空デバッグ2

石垣一夜城で青空デバッグ、的な」で色々気になったので。

結構 GPS のデータを取れてない場所があるのね。当たり前なんだろうとは思うものの、どの程度当たり前なのかがよくわからないのと、「取れてないぜっ」てのを検知出来ないのかしらねぇ、てのと。それに sqlite3 のエラー時にデータを捨てるのもなんかもったいないかな、とも思い。

ここに行った:


無論これ自体は特に目的ではない。ちなみに夏でも北風運用である。(34R DEP/ARV、34L ARV、05 DEP。)

仕込んでいったプログラムは前回からの変更点としてはポイントとしては3つ:

  1. 地理院タイルに変えた。これは今日のためではなくて。
  2. データ記録時点での sqlite3.OperationalError 時はデータは捨てずに pending として取って置いて、次回の機会に突っ込む。
  3. gps からの on_status イベントを kivy ログに吐き出しつつ、ついでに sqlite3 にも記録してみる
main.py
  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()
service/main.py
 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()
service/sensor_logging.py
 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)))
service/sensor_logging_data.py
 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 以外のセンサーで追跡を補えないもんかなぁ、と探ってみてるけど、たぶんむつかしいであろうな。加速度センサーで少し補える可能性はないか、と思うも、そもそも電車とかに乗って「慣性の法則」に従った状態で電車の外からみた加速度を検知出来るとも思えないわけで。これはこれで実験はしてみたいけど、たぶん期待したものにはならないであろうなと思う。→というかそもそも加速度センサーは小さなノイズが常に乗っかってることがやってみてすぐにわかったのね。これはローパスフィルタであるとかでゴミを取り除いて使うのが「普通」なんですと。