Add license
[uberlogger] / uberlogger.py
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2010 Dmitry Marakasov
4 #
5 # This file is part of UberLogger.
6 #
7 # UberLogger is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # UberLogger is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with UberLogger.  If not, see <http://www.gnu.org/licenses/>.
19 #
20
21 from PyQt4.QtGui import *
22 from PyQt4.QtCore import SIGNAL, SLOT, Qt, QTimer, QThread
23
24 from time import sleep
25 from threading import Thread
26 from datetime import datetime
27
28 import bluetooth
29 import os
30 import socket
31 import sys
32 import dbus
33
34 from nmea import GPSData
35
36 devices = [
37         [ "00:0D:B5:38:9E:16", "BT-335" ],
38         [ "00:0D:B5:38:AF:C7", "BT-821" ],
39 ]
40
41 reconnect_delay = 10
42 logprefix = "/home/user/gps/"
43
44 # lets you get/set powered state of your bluetooth adapter
45 # code from http://tomch.com/wp/?p=132
46 def enable_bluetooth(enabled = None):
47         bus = dbus.SystemBus();
48         root = bus.get_object('org.bluez', '/')
49         manager = dbus.Interface(root, 'org.bluez.Manager')
50         defaultAdapter = manager.DefaultAdapter()
51         obj = bus.get_object('org.bluez', defaultAdapter)
52         adapter = dbus.Interface(obj, 'org.bluez.Adapter')
53         props = adapter.GetProperties()
54
55         if enabled is None:
56                 return adapter.GetProperties()['Powered']
57         elif enabled:
58                 adapter.SetProperty('Powered', True)
59         else:
60                 adapter.SetProperty('Powered', False)
61
62
63 class GPSThread(QThread):
64         def __init__(self, addr, name, parent = None):
65                 QThread.__init__(self, parent)
66
67                 self.addr = addr
68                 self.name = name
69                 self.exiting = False
70                 self.logfile = None
71
72                 self.total_length = 0
73                 self.total_connects = 0
74                 self.total_lines = 0
75
76         def __del__(self):
77                 self.exiting = True
78                 self.wait()
79
80         def stop(self):
81                 self.exiting = True
82
83         def init(self):
84                 logname = os.path.join(logprefix, "%s.%s.nmea" % (datetime.today().strftime("%Y.%m.%d"), self.name))
85
86                 enable_bluetooth(True)
87                 self.logfile = open(logname, 'a')
88
89         def cleanup(self):
90                 if self.logfile is not None:
91                         self.logfile.close()
92                         self.logfile = None
93
94         def main_loop(self):
95                 error = None
96                 buffer = ""
97                 last_length = 0
98                 socket = None
99
100                 gpsdata = GPSData()
101
102                 try:
103                         # connect
104                         while not self.exiting and socket is None:
105                                 self.emit(SIGNAL("status_updated(QString)"), "Connecting...")
106                                 socket = bluetooth.BluetoothSocket()
107                                 socket.connect((self.addr, 1))
108                                 socket.settimeout(10)
109                                 self.total_connects += 1
110
111                                 self.emit(SIGNAL("status_updated(QString)"), "Connected")
112
113                         # read
114                         while not self.exiting and socket is not None:
115                                 chunk = socket.recv(1024)
116
117                                 if len(chunk) == 0:
118                                         raise Exception("Zero read")
119
120                                 buffer += chunk
121                                 self.total_length += len(chunk)
122                                 self.logfile.write(chunk)
123
124                                 # parse lines
125                                 lines = buffer.split('\n')
126                                 buffer = lines.pop()
127
128                                 for line in lines:
129                                         gpsdata.parse_nmea_string(line)
130
131                                 self.total_lines += len(lines)
132
133                                 # update display info every 10k
134                                 if self.total_length - last_length > 512:
135                                         self.emit(SIGNAL("status_updated(QString)"), "Logged %d lines, %d bytes, %d connects" % (self.total_lines, self.total_length, self.total_connects))
136                                         last_length = self.total_length
137
138                                 self.emit(SIGNAL("data_updated(QString)"), gpsdata.dump())
139
140                 except IOError, e:
141                         error = "%s: %s" % ("Cannot connect" if socket is None else "Read error", str(e))
142                 except:
143                         error = "%s: %s" % ("Cannot connect" if socket is None else "Read error", sys.exc_info())
144
145                 if self.exiting or error is None: return
146
147                 # process error: wait some time and retry
148                 global reconnect_delay
149                 count = reconnect_delay
150                 while not self.exiting and count > 0:
151                         self.emit(SIGNAL("status_updated(QString)"), "%s, retry in %d" % (error, count))
152                         sleep(1)
153                         count -= 1
154
155                 socket = None
156
157         def run(self):
158                 try:
159                         self.init()
160
161                         while not self.exiting:
162                                 self.main_loop()
163                 except Exception, e:
164                         self.emit(SIGNAL("status_updated(QString)"), "FATAL: %s" % str(e))
165
166                 try:
167                         self.cleanup()
168                 except:
169                         self.emit(SIGNAL("status_updated(QString)"), "FATAL: cleanup failed")
170
171                 self.emit(SIGNAL("status_updated(QString)"), "stopped")
172
173 class ContainerWidget(QWidget):
174         def __init__(self, addr, name, parent=None):
175                 QWidget.__init__(self, parent)
176
177                 # data
178                 self.addr = addr
179                 self.name = name
180                 self.thread = None
181                 self.status = "stopped"
182
183                 # UI: header
184                 self.startbutton = QPushButton("Start")
185                 self.startbutton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
186                 self.connect(self.startbutton, SIGNAL('clicked()'), self.start_thread)
187
188                 self.stopbutton = QPushButton("Stop")
189                 self.stopbutton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
190                 self.stopbutton.setEnabled(False)
191                 self.connect(self.stopbutton, SIGNAL('clicked()'), self.stop_thread)
192
193                 self.statuswidget = QLabel()
194                 self.statuswidget.setWordWrap(True)
195
196                 self.monitorwidget = QLabel()
197
198                 header = QHBoxLayout()
199                 header.addWidget(self.startbutton)
200                 header.addWidget(self.stopbutton)
201                 header.addWidget(self.statuswidget)
202
203                 self.layout = QVBoxLayout()
204                 self.layout.addLayout(header)
205                 self.layout.addWidget(self.monitorwidget)
206
207                 self.setLayout(self.layout)
208
209                 # done
210                 self.update_status()
211
212         def __del__(self):
213                 self.thread = None
214
215         def start_thread(self):
216                 if self.thread is not None:
217                         return
218
219                 self.startbutton.setEnabled(False)
220                 self.stopbutton.setEnabled(True)
221
222                 self.thread = GPSThread(self.addr, self.name, self)
223                 self.connect(self.thread, SIGNAL("status_updated(QString)"), self.update_status)
224                 self.connect(self.thread, SIGNAL("data_updated(QString)"), self.update_monitor)
225                 self.connect(self.thread, SIGNAL("finished()"), self.gc_thread)
226                 self.thread.start()
227
228         def stop_thread(self):
229                 if self.thread is None:
230                         return
231
232                 self.stopbutton.setEnabled(False)
233                 self.thread.stop()
234
235         def gc_thread(self):
236                 self.thread = None # join
237
238                 self.startbutton.setEnabled(True)
239                 self.stopbutton.setEnabled(False)
240                 self.update_status()
241
242         def update_status(self, status = None):
243                 if status is not None:
244                         self.status = status
245                 self.statuswidget.setText("%s: %s" % (self.name, self.status))
246
247         def update_monitor(self, data = None):
248                 self.monitorwidget.setText(data)
249
250 class MainWindow(QWidget):
251         def __init__(self, parent=None):
252                 QWidget.__init__(self, parent)
253                 self.setWindowTitle("UberLogger")
254
255                 layout = QVBoxLayout()
256
257                 global devices
258                 for addr, name in devices:
259                         layout.addWidget(ContainerWidget(addr, name))
260
261                 self.setLayout(layout)
262
263 def main():
264         app = QApplication(sys.argv)
265
266         window = MainWindow()
267         window.show()
268         sys.exit(app.exec_())
269
270 if __name__ == "__main__":
271         main()