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