Tweak margins
[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.togglebutton = QPushButton("Start")
185                 self.togglebutton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
186                 self.connect(self.togglebutton, SIGNAL('clicked()'), self.toggle_thread)
187
188                 self.statuswidget = QLabel()
189                 self.statuswidget.setWordWrap(True)
190
191                 self.monitorwidget = QLabel()
192
193                 header = QHBoxLayout()
194                 header.addWidget(self.togglebutton)
195                 header.addWidget(self.statuswidget)
196
197                 self.layout = QVBoxLayout()
198                 self.layout.setContentsMargins(0, 0, 0, 0)
199                 self.layout.setSpacing(5)
200                 self.layout.addLayout(header)
201                 self.layout.addWidget(self.monitorwidget)
202
203                 self.setLayout(self.layout)
204
205                 # done
206                 self.update_status()
207
208         def __del__(self):
209                 self.thread = None
210
211         def toggle_thread(self):
212                 if self.thread is None:
213                         self.togglebutton.setText("Stop")
214
215                         self.thread = GPSThread(self.addr, self.name, self)
216                         self.connect(self.thread, SIGNAL("status_updated(QString)"), self.update_status)
217                         self.connect(self.thread, SIGNAL("data_updated(QString)"), self.update_monitor)
218                         self.connect(self.thread, SIGNAL("finished()"), self.gc_thread)
219                         self.thread.start()
220                 else:
221                         self.togglebutton.setEnabled(False)
222                         self.thread.stop()
223
224         def gc_thread(self):
225                 self.thread = None # join
226
227                 self.togglebutton.setText("Start")
228                 self.togglebutton.setEnabled(True)
229                 self.update_status()
230
231         def update_status(self, status = None):
232                 if status is not None:
233                         self.status = status
234                 self.statuswidget.setText("%s\n%s" % (self.name, self.status))
235
236         def update_monitor(self, data = None):
237                 self.monitorwidget.setText(data)
238
239 class InputsList(QWidget):
240         def __init__(self, parent=None):
241                 QWidget.__init__(self, parent)
242
243                 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
244
245                 layout = QVBoxLayout()
246                 layout.setContentsMargins(0, 0, 0, 0)
247                 layout.setSpacing(0)
248
249                 global devices
250                 for addr, name in devices:
251                         layout.addWidget(ContainerWidget(addr, name))
252
253                 self.setLayout(layout)
254
255 def main():
256         app = QApplication(sys.argv)
257
258         scrollarea = QScrollArea()
259         scrollarea.setWindowTitle("UberLogger")
260         scrollarea.setWidgetResizable(True)
261         scrollarea.setWidget(InputsList())
262         scrollarea.show()
263
264         sys.exit(app.exec_())
265
266 if __name__ == "__main__":
267         main()