altosdroid: add new TelemetryState.java
[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.bluetooth.BluetoothAdapter;
32 import android.content.Intent;
33 import android.content.Context;
34 import android.os.Bundle;
35 import android.os.IBinder;
36 import android.os.Handler;
37 import android.os.Message;
38 import android.os.Messenger;
39 import android.os.RemoteException;
40 import android.os.Looper;
41 import android.util.Log;
42 import android.widget.Toast;
43 import android.location.Location;
44 import android.location.LocationManager;
45 import android.location.LocationListener;
46 import android.location.Criteria;
47
48 import org.altusmetrum.altoslib_5.*;
49
50
51 public class TelemetryService extends Service implements LocationListener {
52
53         private static final String TAG = "TelemetryService";
54         private static final boolean D = true;
55
56         static final int MSG_REGISTER_CLIENT   = 1;
57         static final int MSG_UNREGISTER_CLIENT = 2;
58         static final int MSG_CONNECT           = 3;
59         static final int MSG_CONNECTED         = 4;
60         static final int MSG_CONNECT_FAILED    = 5;
61         static final int MSG_DISCONNECTED      = 6;
62         static final int MSG_TELEMETRY         = 7;
63         static final int MSG_SETFREQUENCY      = 8;
64         static final int MSG_CRC_ERROR         = 9;
65         static final int MSG_SETBAUD           = 10;
66
67         // Unique Identification Number for the Notification.
68         // We use it on Notification start, and to cancel it.
69         private int NOTIFICATION = R.string.telemetry_service_label;
70         //private NotificationManager mNM;
71
72         // Timer - we wake up every now and then to decide if the service should stop
73         private Timer timer = new Timer();
74
75         ArrayList<Messenger> mClients = new ArrayList<Messenger>(); // Keeps track of all current registered clients.
76         final Handler   mHandler   = new IncomingHandler(this);
77         final Messenger mMessenger = new Messenger(mHandler); // Target we publish for clients to send messages to IncomingHandler.
78
79         // Name of the connected device
80         String address;
81         private AltosBluetooth  mAltosBluetooth  = null;
82         private TelemetryReader mTelemetryReader = null;
83         private TelemetryLogger mTelemetryLogger = null;
84         // Local Bluetooth adapter
85         private BluetoothAdapter mBluetoothAdapter = null;
86
87         private TelemetryState  telemetry_state;
88
89         // Last data seen; send to UI when it starts
90
91         // Handler of incoming messages from clients.
92         static class IncomingHandler extends Handler {
93                 private final WeakReference<TelemetryService> service;
94                 IncomingHandler(TelemetryService s) { service = new WeakReference<TelemetryService>(s); }
95
96                 @Override
97                 public void handleMessage(Message msg) {
98                         TelemetryService s = service.get();
99                         switch (msg.what) {
100                         case MSG_REGISTER_CLIENT:
101                                 s.mClients.add(msg.replyTo);
102                                 try {
103                                         // Now we try to send the freshly connected UI any relavant information about what
104                                         // we're talking to
105                                         msg.replyTo.send(s.message());
106                                 } catch (RemoteException e) {
107                                         s.mClients.remove(msg.replyTo);
108                                 }
109                                 if (D) Log.d(TAG, "Client bound to service");
110                                 break;
111                         case MSG_UNREGISTER_CLIENT:
112                                 s.mClients.remove(msg.replyTo);
113                                 if (D) Log.d(TAG, "Client unbound from service");
114                                 break;
115                         case MSG_CONNECT:
116                                 if (D) Log.d(TAG, "Connect command received");
117                                 String address = (String) msg.obj;
118                                 AltosDroidPreferences.set_active_device(address);
119                                 s.startAltosBluetooth(address);
120                                 break;
121                         case MSG_CONNECTED:
122                                 if (D) Log.d(TAG, "Connected to device");
123                                 try {
124                                         s.connected();
125                                 } catch (InterruptedException ie) {
126                                 }
127                                 break;
128                         case MSG_CONNECT_FAILED:
129                                 if (D) Log.d(TAG, "Connection failed... retrying");
130                                 if (s.address != null)
131                                         s.startAltosBluetooth(s.address);
132                                 break;
133                         case MSG_DISCONNECTED:
134                                 Log.d(TAG, "MSG_DISCONNECTED");
135                                 s.stopAltosBluetooth();
136                                 break;
137                         case MSG_TELEMETRY:
138                                 // forward telemetry messages
139                                 s.telemetry_state.state = (AltosState) msg.obj;
140                                 if (D) Log.d(TAG, "MSG_TELEMETRY");
141                                 s.sendMessageToClients();
142                                 break;
143                         case MSG_CRC_ERROR:
144                                 // forward crc error messages
145                                 s.telemetry_state.crc_errors = (Integer) msg.obj;
146                                 if (D) Log.d(TAG, "MSG_CRC_ERROR");
147                                 s.sendMessageToClients();
148                                 break;
149                         case MSG_SETFREQUENCY:
150                                 if (D) Log.d(TAG, "MSG_SETFREQUENCY");
151                                 s.telemetry_state.frequency = (Double) msg.obj;
152                                 if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) {
153                                         try {
154                                                 s.mAltosBluetooth.set_radio_frequency(s.telemetry_state.frequency);
155                                                 s.mAltosBluetooth.save_frequency();
156                                         } catch (InterruptedException e) {
157                                         } catch (TimeoutException e) {
158                                         }
159                                 }
160                                 s.sendMessageToClients();
161                                 break;
162                         case MSG_SETBAUD:
163                                 if (D) Log.d(TAG, "MSG_SETBAUD");
164                                 s.telemetry_state.telemetry_rate = (Integer) msg.obj;
165                                 if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) {
166                                         s.mAltosBluetooth.set_telemetry_rate(s.telemetry_state.telemetry_rate);
167                                         s.mAltosBluetooth.save_telemetry_rate();
168                                 }
169                                 s.sendMessageToClients();
170                                 break;
171                         default:
172                                 super.handleMessage(msg);
173                         }
174                 }
175         }
176
177         private Message message() {
178                 if (telemetry_state == null)
179                         Log.d(TAG, "telemetry_state null!");
180                 return Message.obtain(null, AltosDroid.MSG_STATE, telemetry_state);
181         }
182
183         private void sendMessageToClients() {
184                 Message m = message();
185                 if (D) Log.d(TAG, String.format("Send message to %d clients", mClients.size()));
186                 for (int i=mClients.size()-1; i>=0; i--) {
187                         try {
188                                 if (D) Log.d(TAG, String.format("Send message to client %d", i));
189                                 mClients.get(i).send(m);
190                         } catch (RemoteException e) {
191                                 mClients.remove(i);
192                         }
193                 }
194         }
195
196         private void stopAltosBluetooth() {
197                 if (D) Log.d(TAG, "stopAltosBluetooth(): begin");
198                 telemetry_state.connect = TelemetryState.CONNECT_READY;
199                 if (mTelemetryReader != null) {
200                         if (D) Log.d(TAG, "stopAltosBluetooth(): stopping TelemetryReader");
201                         mTelemetryReader.interrupt();
202                         try {
203                                 mTelemetryReader.join();
204                         } catch (InterruptedException e) {
205                         }
206                         mTelemetryReader = null;
207                 }
208                 if (mTelemetryLogger != null) {
209                         if (D) Log.d(TAG, "stopAltosBluetooth(): stopping TelemetryLogger");
210                         mTelemetryLogger.stop();
211                         mTelemetryLogger = null;
212                 }
213                 if (mAltosBluetooth != null) {
214                         if (D) Log.d(TAG, "stopAltosBluetooth(): stopping AltosBluetooth");
215                         mAltosBluetooth.close();
216                         mAltosBluetooth = null;
217                 }
218                 telemetry_state.config = null;
219                 if (D) Log.d(TAG, "stopAltosBluetooth(): send message to clients");
220                 sendMessageToClients();
221         }
222
223         private void startAltosBluetooth(String address) {
224                 // Get the BLuetoothDevice object
225                 BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
226
227                 this.address = address;
228                 if (mAltosBluetooth == null) {
229                         if (D) Log.d(TAG, String.format("startAltosBluetooth(): Connecting to %s (%s)", device.getName(), device.getAddress()));
230                         mAltosBluetooth = new AltosBluetooth(device, mHandler);
231                         telemetry_state.connect = TelemetryState.CONNECT_CONNECTING;
232                         sendMessageToClients();
233                 } else {
234                         // This is a bit of a hack - if it appears we're still connected, we treat this as a restart.
235                         // So, to give a suitable delay to teardown/bringup, we just schedule a resend of a message
236                         // to ourselves in a few seconds time that will ultimately call this method again.
237                         // ... then we tear down the existing connection.
238                         // We do it this way around so that we don't lose a reference to the device when this method
239                         // is called on reception of MSG_CONNECT_FAILED in the handler above.
240                         mHandler.sendMessageDelayed(Message.obtain(null, MSG_CONNECT, address), 3000);
241                         stopAltosBluetooth();
242                 }
243         }
244
245         private void connected() throws InterruptedException {
246                 if (D) Log.d(TAG, "connected top");
247                 try {
248                         if (mAltosBluetooth == null)
249                                 throw new InterruptedException("no bluetooth");
250                         telemetry_state.config = mAltosBluetooth.config_data();
251                         mAltosBluetooth.set_radio_frequency(telemetry_state.frequency);
252                         mAltosBluetooth.set_telemetry_rate(telemetry_state.telemetry_rate);
253                 } catch (TimeoutException e) {
254                         // If this timed out, then we really want to retry it, but
255                         // probably safer to just retry the connection from scratch.
256                         mHandler.obtainMessage(MSG_CONNECT_FAILED).sendToTarget();
257                         return;
258                 }
259
260                 if (D) Log.d(TAG, "connected bluetooth configured");
261                 telemetry_state.connect = TelemetryState.CONNECT_CONNECTED;
262
263                 mTelemetryReader = new TelemetryReader(mAltosBluetooth, mHandler);
264                 mTelemetryReader.start();
265
266                 if (D) Log.d(TAG, "connected TelemetryReader started");
267
268                 mTelemetryLogger = new TelemetryLogger(this, mAltosBluetooth);
269
270                 if (D) Log.d(TAG, "Notify UI of connection");
271
272                 sendMessageToClients();
273         }
274
275         private void onTimerTick() {
276                 if (D) Log.d(TAG, "Timer wakeup");
277                 try {
278                         if (mClients.size() <= 0 && telemetry_state.connect != TelemetryState.CONNECT_CONNECTED) {
279                                 stopSelf();
280                         }
281                 } catch (Throwable t) {
282                         Log.e(TAG, "Timer failed: ", t);
283                 }
284         }
285
286
287         @Override
288         public void onCreate() {
289                 // Get local Bluetooth adapter
290                 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
291
292                 // If the adapter is null, then Bluetooth is not supported
293                 if (mBluetoothAdapter == null) {
294                         Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
295                 }
296
297                 // Initialise preferences
298                 AltosDroidPreferences.init(this);
299
300                 telemetry_state = new TelemetryState();
301
302                 // Create a reference to the NotificationManager so that we can update our notifcation text later
303                 //mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
304
305                 telemetry_state.connect = TelemetryState.CONNECT_READY;
306
307                 // Start our timer - first event in 10 seconds, then every 10 seconds after that.
308                 timer.scheduleAtFixedRate(new TimerTask(){ public void run() {onTimerTick();}}, 10000L, 10000L);
309
310                 // Listen for GPS and Network position updates
311                 LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
312
313                 locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, this);
314
315                 String address = AltosDroidPreferences.active_device();
316                 if (address != null)
317                         startAltosBluetooth(address);
318         }
319
320         @Override
321         public int onStartCommand(Intent intent, int flags, int startId) {
322                 Log.i("TelemetryService", "Received start id " + startId + ": " + intent);
323
324                 CharSequence text = getText(R.string.telemetry_service_started);
325
326                 // Create notification to be displayed while the service runs
327                 Notification notification = new Notification(R.drawable.am_status_c, text, 0);
328
329                 // The PendingIntent to launch our activity if the user selects this notification
330                 PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
331                                 new Intent(this, AltosDroid.class), 0);
332
333                 // Set the info for the views that show in the notification panel.
334                 notification.setLatestEventInfo(this, getText(R.string.telemetry_service_label), text, contentIntent);
335
336                 // Set the notification to be in the "Ongoing" section.
337                 notification.flags |= Notification.FLAG_ONGOING_EVENT;
338
339                 // Move us into the foreground.
340                 startForeground(NOTIFICATION, notification);
341
342                 // We want this service to continue running until it is explicitly
343                 // stopped, so return sticky.
344                 return START_STICKY;
345         }
346
347         @Override
348         public void onDestroy() {
349
350                 // Stop listening for location updates
351                 ((LocationManager) getSystemService(Context.LOCATION_SERVICE)).removeUpdates(this);
352
353                 // Stop the bluetooth Comms threads
354                 stopAltosBluetooth();
355
356                 // Demote us from the foreground, and cancel the persistent notification.
357                 stopForeground(true);
358
359                 // Stop our timer
360                 if (timer != null) {timer.cancel();}
361
362                 // Tell the user we stopped.
363                 Toast.makeText(this, R.string.telemetry_service_stopped, Toast.LENGTH_SHORT).show();
364         }
365
366         @Override
367         public IBinder onBind(Intent intent) {
368                 return mMessenger.getBinder();
369         }
370
371
372         public void onLocationChanged(Location location) {
373                 telemetry_state.location = location;
374                 if (D) Log.d(TAG, "location changed");
375                 sendMessageToClients();
376         }
377
378         public void onStatusChanged(String provider, int status, Bundle extras) {
379         }
380
381         public void onProviderEnabled(String provider) {
382         }
383
384         public void onProviderDisabled(String provider) {
385         }
386
387 }