altosdroid: Add telemetry rate support
[fw/altos] / altosdroid / src / org / altusmetrum / AltosDroid / TelemetryService.java
1 /*
2  * Copyright © 2012 Mike Beattie <mike@ethernal.org>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16  */
17
18 package org.altusmetrum.AltosDroid;
19
20 import java.lang.ref.WeakReference;
21 import java.util.ArrayList;
22 import java.util.concurrent.TimeoutException;
23 import java.util.Timer;
24 import java.util.TimerTask;
25
26 import android.app.Notification;
27 //import android.app.NotificationManager;
28 import android.app.PendingIntent;
29 import android.app.Service;
30 import android.bluetooth.BluetoothDevice;
31 import android.content.Intent;
32 import android.content.Context;
33 import android.os.Bundle;
34 import android.os.IBinder;
35 import android.os.Handler;
36 import android.os.Message;
37 import android.os.Messenger;
38 import android.os.RemoteException;
39 import android.os.Looper;
40 import android.util.Log;
41 import android.widget.Toast;
42 import android.location.Location;
43 import android.location.LocationManager;
44 import android.location.LocationListener;
45 import android.location.Criteria;
46
47 import org.altusmetrum.altoslib_5.*;
48
49
50 public class TelemetryService extends Service implements LocationListener {
51
52         private static final String TAG = "TelemetryService";
53         private static final boolean D = true;
54
55         static final int MSG_REGISTER_CLIENT   = 1;
56         static final int MSG_UNREGISTER_CLIENT = 2;
57         static final int MSG_CONNECT           = 3;
58         static final int MSG_CONNECTED         = 4;
59         static final int MSG_CONNECT_FAILED    = 5;
60         static final int MSG_DISCONNECTED      = 6;
61         static final int MSG_TELEMETRY         = 7;
62         static final int MSG_SETFREQUENCY      = 8;
63         static final int MSG_CRC_ERROR         = 9;
64         static final int MSG_SETBAUD           = 10;
65
66         public static final int STATE_NONE       = 0;
67         public static final int STATE_READY      = 1;
68         public static final int STATE_CONNECTING = 2;
69         public static final int STATE_CONNECTED  = 3;
70
71         // Unique Identification Number for the Notification.
72         // We use it on Notification start, and to cancel it.
73         private int NOTIFICATION = R.string.telemetry_service_label;
74         //private NotificationManager mNM;
75
76         // Timer - we wake up every now and then to decide if the service should stop
77         private Timer timer = new Timer();
78
79         ArrayList<Messenger> mClients = new ArrayList<Messenger>(); // Keeps track of all current registered clients.
80         final Handler   mHandler   = new IncomingHandler(this);
81         final Messenger mMessenger = new Messenger(mHandler); // Target we publish for clients to send messages to IncomingHandler.
82
83         // Name of the connected device
84         private BluetoothDevice device           = null;
85         private AltosBluetooth  mAltosBluetooth  = null;
86         private AltosConfigData mConfigData      = null;
87         private TelemetryReader mTelemetryReader = null;
88         private TelemetryLogger mTelemetryLogger = null;
89
90         // internally track state of bluetooth connection
91         private int state = STATE_NONE;
92
93         // Last data seen; send to UI when it starts
94
95         private AltosState last_state;
96         private Location last_location;
97         private int last_crc_errors;
98
99         // Handler of incoming messages from clients.
100         static class IncomingHandler extends Handler {
101                 private final WeakReference<TelemetryService> service;
102                 IncomingHandler(TelemetryService s) { service = new WeakReference<TelemetryService>(s); }
103
104                 @Override
105                 public void handleMessage(Message msg) {
106                         TelemetryService s = service.get();
107                         switch (msg.what) {
108                         case MSG_REGISTER_CLIENT:
109                                 s.mClients.add(msg.replyTo);
110                                 try {
111                                         // Now we try to send the freshly connected UI any relavant information about what
112                                         // we're talking to - Basically state and Config Data.
113                                         msg.replyTo.send(Message.obtain(null, AltosDroid.MSG_STATE_CHANGE, s.state, -1, s.mConfigData));
114                                         // We also send any recent telemetry or location data that's cached
115                                         if (s.last_state      != null) msg.replyTo.send(Message.obtain(null, AltosDroid.MSG_TELEMETRY, s.last_state     ));
116                                         if (s.last_location   != null) msg.replyTo.send(Message.obtain(null, AltosDroid.MSG_LOCATION , s.last_location  ));
117                                         if (s.last_crc_errors != 0   ) msg.replyTo.send(Message.obtain(null, AltosDroid.MSG_CRC_ERROR, s.last_crc_errors));
118                                 } catch (RemoteException e) {
119                                         s.mClients.remove(msg.replyTo);
120                                 }
121                                 if (D) Log.d(TAG, "Client bound to service");
122                                 break;
123                         case MSG_UNREGISTER_CLIENT:
124                                 s.mClients.remove(msg.replyTo);
125                                 if (D) Log.d(TAG, "Client unbound from service");
126                                 break;
127                         case MSG_CONNECT:
128                                 if (D) Log.d(TAG, "Connect command received");
129                                 s.device = (BluetoothDevice) msg.obj;
130                                 s.startAltosBluetooth();
131                                 break;
132                         case MSG_CONNECTED:
133                                 if (D) Log.d(TAG, "Connected to device");
134                                 s.connected();
135                                 break;
136                         case MSG_CONNECT_FAILED:
137                                 if (D) Log.d(TAG, "Connection failed... retrying");
138                                 s.startAltosBluetooth();
139                                 break;
140                         case MSG_DISCONNECTED:
141                                 // Only do the following if we haven't been shutdown elsewhere..
142                                 if (s.device != null) {
143                                         if (D) Log.d(TAG, "Disconnected from " + s.device.getName());
144                                         s.stopAltosBluetooth();
145                                 }
146                                 break;
147                         case MSG_TELEMETRY:
148                                 // forward telemetry messages
149                                 s.last_state = (AltosState) msg.obj;
150                                 s.sendMessageToClients(Message.obtain(null, AltosDroid.MSG_TELEMETRY, msg.obj));
151                                 break;
152                         case MSG_CRC_ERROR:
153                                 // forward crc error messages
154                                 s.last_crc_errors = (Integer) msg.obj;
155                                 s.sendMessageToClients(Message.obtain(null, AltosDroid.MSG_CRC_ERROR, msg.obj));
156                                 break;
157                         case MSG_SETFREQUENCY:
158                                 if (s.state == STATE_CONNECTED) {
159                                         try {
160                                                 s.mAltosBluetooth.set_radio_frequency((Double) msg.obj);
161                                         } catch (InterruptedException e) {
162                                         } catch (TimeoutException e) {
163                                         }
164                                 }
165                                 break;
166                         case MSG_SETBAUD:
167                                 if (s.state == STATE_CONNECTED) {
168                                         s.mAltosBluetooth.set_telemetry_rate((Integer) msg.obj);
169                                 }
170                                 break;
171                         default:
172                                 super.handleMessage(msg);
173                         }
174                 }
175         }
176
177         private void sendMessageToClients(Message m) {
178                 for (int i=mClients.size()-1; i>=0; i--) {
179                         try {
180                                 mClients.get(i).send(m);
181                         } catch (RemoteException e) {
182                                 mClients.remove(i);
183                         }
184                 }
185         }
186
187         private void stopAltosBluetooth() {
188                 if (D) Log.d(TAG, "stopAltosBluetooth(): begin");
189                 setState(STATE_READY);
190                 if (mTelemetryReader != null) {
191                         if (D) Log.d(TAG, "stopAltosBluetooth(): stopping TelemetryReader");
192                         mTelemetryReader.interrupt();
193                         try {
194                                 mTelemetryReader.join();
195                         } catch (InterruptedException e) {
196                         }
197                         mTelemetryReader = null;
198                 }
199                 if (mTelemetryLogger != null) {
200                         if (D) Log.d(TAG, "stopAltosBluetooth(): stopping TelemetryLogger");
201                         mTelemetryLogger.stop();
202                         mTelemetryLogger = null;
203                 }
204                 if (mAltosBluetooth != null) {
205                         if (D) Log.d(TAG, "stopAltosBluetooth(): stopping AltosBluetooth");
206                         mAltosBluetooth.close();
207                         mAltosBluetooth = null;
208                 }
209                 device = null;
210                 mConfigData = null;
211         }
212
213         private void startAltosBluetooth() {
214                 if (device == null) {
215                         return;
216                 }
217                 if (mAltosBluetooth == null) {
218                         if (D) Log.d(TAG, String.format("startAltosBluetooth(): Connecting to %s (%s)", device.getName(), device.getAddress()));
219                         mAltosBluetooth = new AltosBluetooth(device, mHandler);
220                         setState(STATE_CONNECTING);
221                 } else {
222                         // This is a bit of a hack - if it appears we're still connected, we treat this as a restart.
223                         // So, to give a suitable delay to teardown/bringup, we just schedule a resend of a message
224                         // to ourselves in a few seconds time that will ultimately call this method again.
225                         // ... then we tear down the existing connection.
226                         // We do it this way around so that we don't lose a reference to the device when this method
227                         // is called on reception of MSG_CONNECT_FAILED in the handler above.
228                         mHandler.sendMessageDelayed(Message.obtain(null, MSG_CONNECT, device), 3000);
229                         stopAltosBluetooth();
230                 }
231         }
232
233         private synchronized void setState(int s) {
234                 if (D) Log.d(TAG, "setState(): " + state + " -> " + s);
235                 state = s;
236
237                 // This shouldn't be required - mConfigData should be null for any non-connected
238                 // state, but to be safe and to reduce message size
239                 AltosConfigData acd = (state == STATE_CONNECTED) ? mConfigData : null;
240
241                 sendMessageToClients(Message.obtain(null, AltosDroid.MSG_STATE_CHANGE, state, -1, acd));
242         }
243
244         private void connected() {
245                 try {
246                         if (mAltosBluetooth == null)
247                                 throw new InterruptedException("no bluetooth");
248                         mConfigData = mAltosBluetooth.config_data();
249                 } catch (InterruptedException e) {
250                 } catch (TimeoutException e) {
251                         // If this timed out, then we really want to retry it, but
252                         // probably safer to just retry the connection from scratch.
253                         mHandler.obtainMessage(MSG_CONNECT_FAILED).sendToTarget();
254                         return;
255                 }
256
257                 setState(STATE_CONNECTED);
258
259                 mTelemetryReader = new TelemetryReader(mAltosBluetooth, mHandler);
260                 mTelemetryReader.start();
261                 
262                 mTelemetryLogger = new TelemetryLogger(this, mAltosBluetooth);
263         }
264
265
266         private void onTimerTick() {
267                 if (D) Log.d(TAG, "Timer wakeup");
268                 try {
269                         if (mClients.size() <= 0 && state != STATE_CONNECTED) {
270                                 stopSelf();
271                         }
272                 } catch (Throwable t) {
273                         Log.e(TAG, "Timer failed: ", t);
274                 }
275         }
276
277
278         @Override
279         public void onCreate() {
280                 // Create a reference to the NotificationManager so that we can update our notifcation text later
281                 //mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
282
283                 setState(STATE_READY);
284
285                 // Start our timer - first event in 10 seconds, then every 10 seconds after that.
286                 timer.scheduleAtFixedRate(new TimerTask(){ public void run() {onTimerTick();}}, 10000L, 10000L);
287
288                 // Listen for GPS and Network position updates
289                 LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
290                 
291                 locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, this);
292 //              locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this);
293         }
294
295         @Override
296         public int onStartCommand(Intent intent, int flags, int startId) {
297                 Log.i("TelemetryService", "Received start id " + startId + ": " + intent);
298
299                 CharSequence text = getText(R.string.telemetry_service_started);
300
301                 // Create notification to be displayed while the service runs
302                 Notification notification = new Notification(R.drawable.am_status_c, text, 0);
303
304                 // The PendingIntent to launch our activity if the user selects this notification
305                 PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
306                                 new Intent(this, AltosDroid.class), 0);
307
308                 // Set the info for the views that show in the notification panel.
309                 notification.setLatestEventInfo(this, getText(R.string.telemetry_service_label), text, contentIntent);
310
311                 // Set the notification to be in the "Ongoing" section.
312                 notification.flags |= Notification.FLAG_ONGOING_EVENT;
313
314                 // Move us into the foreground.
315                 startForeground(NOTIFICATION, notification);
316
317                 // We want this service to continue running until it is explicitly
318                 // stopped, so return sticky.
319                 return START_STICKY;
320         }
321
322         @Override
323         public void onDestroy() {
324
325                 // Stop listening for location updates
326                 ((LocationManager) getSystemService(Context.LOCATION_SERVICE)).removeUpdates(this);
327
328                 // Stop the bluetooth Comms threads
329                 stopAltosBluetooth();
330
331                 // Demote us from the foreground, and cancel the persistent notification.
332                 stopForeground(true);
333
334                 // Stop our timer
335                 if (timer != null) {timer.cancel();}
336
337                 // Tell the user we stopped.
338                 Toast.makeText(this, R.string.telemetry_service_stopped, Toast.LENGTH_SHORT).show();
339         }
340
341         @Override
342         public IBinder onBind(Intent intent) {
343                 return mMessenger.getBinder();
344         }
345
346
347         public void onLocationChanged(Location location) {
348                 last_location = location;
349                 sendMessageToClients(Message.obtain(null, AltosDroid.MSG_LOCATION, location));
350         }
351
352         public void onStatusChanged(String provider, int status, Bundle extras) {
353         }
354
355         public void onProviderEnabled(String provider) {
356         }
357
358         public void onProviderDisabled(String provider) {
359         }
360
361 }