Switch from GPLv2 to GPLv2+
[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; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
17  */
18
19 package org.altusmetrum.AltosDroid;
20
21 import java.lang.ref.WeakReference;
22 import java.util.concurrent.TimeoutException;
23 import java.util.*;
24
25 import android.app.Notification;
26 //import android.app.NotificationManager;
27 import android.app.PendingIntent;
28 import android.app.Service;
29 import android.bluetooth.BluetoothDevice;
30 import android.bluetooth.BluetoothAdapter;
31 import android.hardware.usb.*;
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.widget.Toast;
42 import android.location.Criteria;
43
44 import org.altusmetrum.altoslib_11.*;
45
46 public class TelemetryService extends Service implements AltosIdleMonitorListener {
47
48         static final int MSG_REGISTER_CLIENT   = 1;
49         static final int MSG_UNREGISTER_CLIENT = 2;
50         static final int MSG_CONNECT           = 3;
51         static final int MSG_OPEN_USB          = 4;
52         static final int MSG_CONNECTED         = 5;
53         static final int MSG_CONNECT_FAILED    = 6;
54         static final int MSG_DISCONNECTED      = 7;
55         static final int MSG_TELEMETRY         = 8;
56         static final int MSG_SETFREQUENCY      = 9;
57         static final int MSG_CRC_ERROR         = 10;
58         static final int MSG_SETBAUD           = 11;
59         static final int MSG_DISCONNECT        = 12;
60         static final int MSG_DELETE_SERIAL     = 13;
61         static final int MSG_BLUETOOTH_ENABLED = 14;
62         static final int MSG_MONITOR_IDLE_START= 15;
63         static final int MSG_MONITOR_IDLE_STOP = 16;
64         static final int MSG_REBOOT            = 17;
65         static final int MSG_IGNITER_QUERY     = 18;
66         static final int MSG_IGNITER_FIRE      = 19;
67
68         // Unique Identification Number for the Notification.
69         // We use it on Notification start, and to cancel it.
70         private int NOTIFICATION = R.string.telemetry_service_label;
71         //private NotificationManager mNM;
72
73         ArrayList<Messenger> clients = new ArrayList<Messenger>(); // Keeps track of all current registered clients.
74         final Handler   handler   = new IncomingHandler(this);
75         final Messenger messenger = new Messenger(handler); // Target we publish for clients to send messages to IncomingHandler.
76
77         // Name of the connected device
78         DeviceAddress address;
79         private AltosDroidLink  altos_link  = null;
80         private TelemetryReader telemetry_reader = null;
81         private TelemetryLogger telemetry_logger = null;
82
83         // Local Bluetooth adapter
84         private BluetoothAdapter bluetooth_adapter = null;
85
86         // Last data seen; send to UI when it starts
87         private TelemetryState  telemetry_state;
88
89         // Idle monitor if active
90         AltosIdleMonitor idle_monitor = null;
91
92         // Igniter bits
93         AltosIgnite ignite = null;
94         boolean ignite_running;
95
96         // Handler of incoming messages from clients.
97         static class IncomingHandler extends Handler {
98                 private final WeakReference<TelemetryService> service;
99                 IncomingHandler(TelemetryService s) { service = new WeakReference<TelemetryService>(s); }
100
101                 @Override
102                 public void handleMessage(Message msg) {
103                         DeviceAddress address;
104
105                         TelemetryService s = service.get();
106                         AltosDroidLink bt = null;
107                         if (s == null)
108                                 return;
109
110                         switch (msg.what) {
111
112                                 /* Messages from application */
113                         case MSG_REGISTER_CLIENT:
114                                 s.add_client(msg.replyTo);
115                                 break;
116                         case MSG_UNREGISTER_CLIENT:
117                                 s.remove_client(msg.replyTo);
118                                 break;
119                         case MSG_CONNECT:
120                                 AltosDebug.debug("Connect command received");
121                                 address = (DeviceAddress) msg.obj;
122                                 AltosDroidPreferences.set_active_device(address);
123                                 s.start_altos_bluetooth(address, false);
124                                 break;
125                         case MSG_OPEN_USB:
126                                 AltosDebug.debug("Open USB command received");
127                                 UsbDevice device = (UsbDevice) msg.obj;
128                                 s.start_usb(device);
129                                 break;
130                         case MSG_DISCONNECT:
131                                 AltosDebug.debug("Disconnect command received");
132                                 s.address = null;
133                                 if (!(Boolean) msg.obj)
134                                         AltosDroidPreferences.set_active_device(null);
135                                 s.disconnect(true);
136                                 break;
137                         case MSG_DELETE_SERIAL:
138                                 AltosDebug.debug("Delete Serial command received");
139                                 s.delete_serial((Integer) msg.obj);
140                                 break;
141                         case MSG_SETFREQUENCY:
142                                 AltosDebug.debug("MSG_SETFREQUENCY");
143                                 s.telemetry_state.frequency = (Double) msg.obj;
144                                 if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) {
145                                         try {
146                                                 s.altos_link.set_radio_frequency(s.telemetry_state.frequency);
147                                                 s.altos_link.save_frequency();
148                                         } catch (InterruptedException e) {
149                                         } catch (TimeoutException e) {
150                                         }
151                                 }
152                                 s.send_to_clients();
153                                 break;
154                         case MSG_SETBAUD:
155                                 AltosDebug.debug("MSG_SETBAUD");
156                                 s.telemetry_state.telemetry_rate = (Integer) msg.obj;
157                                 if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) {
158                                         s.altos_link.set_telemetry_rate(s.telemetry_state.telemetry_rate);
159                                         s.altos_link.save_telemetry_rate();
160                                 }
161                                 s.send_to_clients();
162                                 break;
163
164                                 /*
165                                  *Messages from AltosBluetooth
166                                  */
167                         case MSG_CONNECTED:
168                                 AltosDebug.debug("MSG_CONNECTED");
169                                 bt = (AltosDroidLink) msg.obj;
170
171                                 if (bt != s.altos_link) {
172                                         AltosDebug.debug("Stale message");
173                                         break;
174                                 }
175                                 AltosDebug.debug("Connected to device");
176                                 try {
177                                         s.connected();
178                                 } catch (InterruptedException ie) {
179                                 }
180                                 break;
181                         case MSG_CONNECT_FAILED:
182                                 AltosDebug.debug("MSG_CONNECT_FAILED");
183                                 bt = (AltosDroidLink) msg.obj;
184
185                                 if (bt != s.altos_link) {
186                                         AltosDebug.debug("Stale message");
187                                         break;
188                                 }
189                                 if (s.address != null) {
190                                         AltosDebug.debug("Connection failed... retrying");
191                                         s.start_altos_bluetooth(s.address, true);
192                                 } else {
193                                         s.disconnect(true);
194                                 }
195                                 break;
196                         case MSG_DISCONNECTED:
197
198                                 /* This can be sent by either AltosDroidLink or TelemetryReader */
199                                 AltosDebug.debug("MSG_DISCONNECTED");
200                                 bt = (AltosDroidLink) msg.obj;
201
202                                 if (bt != s.altos_link) {
203                                         AltosDebug.debug("Stale message");
204                                         break;
205                                 }
206                                 if (s.address != null) {
207                                         AltosDebug.debug("Connection lost... retrying");
208                                         s.start_altos_bluetooth(s.address, true);
209                                 } else {
210                                         s.disconnect(true);
211                                 }
212                                 break;
213
214                                 /*
215                                  * Messages from TelemetryReader
216                                  */
217                         case MSG_TELEMETRY:
218                                 s.telemetry((AltosTelemetry) msg.obj);
219                                 break;
220                         case MSG_CRC_ERROR:
221                                 // forward crc error messages
222                                 s.telemetry_state.crc_errors = (Integer) msg.obj;
223                                 s.send_to_clients();
224                                 break;
225                         case MSG_BLUETOOTH_ENABLED:
226                                 AltosDebug.debug("TelemetryService notes that BT is now enabled");
227                                 address = AltosDroidPreferences.active_device();
228                                 if (address != null && !address.address.startsWith("USB"))
229                                         s.start_altos_bluetooth(address, false);
230                                 break;
231                         case MSG_MONITOR_IDLE_START:
232                                 AltosDebug.debug("start monitor idle");
233                                 s.start_idle_monitor();
234                                 break;
235                         case MSG_MONITOR_IDLE_STOP:
236                                 AltosDebug.debug("stop monitor idle");
237                                 s.stop_idle_monitor();
238                                 break;
239                         case MSG_REBOOT:
240                                 AltosDebug.debug("reboot");
241                                 s.reboot_remote();
242                                 break;
243                         case MSG_IGNITER_QUERY:
244                                 AltosDebug.debug("igniter query");
245                                 s.igniter_query(msg.replyTo);
246                                 break;
247                         case MSG_IGNITER_FIRE:
248                                 AltosDebug.debug("igniter fire");
249                                 s.igniter_fire((String) msg.obj);
250                                 break;
251                         default:
252                                 super.handleMessage(msg);
253                         }
254                 }
255         }
256
257         /* Handle telemetry packet
258          */
259         private void telemetry(AltosTelemetry telem) {
260                 AltosState      state;
261
262                 if (telemetry_state.states.containsKey(telem.serial))
263                         state = telemetry_state.states.get(telem.serial).clone();
264                 else
265                         state = new AltosState();
266                 telem.update_state(state);
267                 telemetry_state.states.put(telem.serial, state);
268                 if (state != null) {
269                         AltosPreferences.set_state(state);
270                 }
271                 send_to_clients();
272         }
273
274         /* Construct the message to deliver to clients
275          */
276         private Message message() {
277                 if (telemetry_state == null)
278                         AltosDebug.debug("telemetry_state null!");
279                 if (telemetry_state.states == null)
280                         AltosDebug.debug("telemetry_state.states null!");
281                 return Message.obtain(null, AltosDroid.MSG_STATE, telemetry_state);
282         }
283
284         /* A new friend has connected
285          */
286         private void add_client(Messenger client) {
287
288                 clients.add(client);
289                 AltosDebug.debug("Client bound to service");
290
291                 /* On connect, send the current state to the new client
292                  */
293                 send_to_client(client);
294                 send_idle_mode_to_client(client);
295
296                 /* If we've got an address from a previous session, then
297                  * go ahead and try to reconnect to the device
298                  */
299                 if (address != null && telemetry_state.connect == TelemetryState.CONNECT_DISCONNECTED) {
300                         AltosDebug.debug("Reconnecting now...");
301                         start_altos_bluetooth(address, false);
302                 }
303         }
304
305         /* A client has disconnected, clean up
306          */
307         private void remove_client(Messenger client) {
308                 clients.remove(client);
309                 AltosDebug.debug("Client unbound from service");
310
311                 /* When the list of clients is empty, stop the service if
312                  * we have no current telemetry source
313                  */
314
315                  if (clients.isEmpty() && telemetry_state.connect == TelemetryState.CONNECT_DISCONNECTED) {
316                          AltosDebug.debug("No clients, no connection. Stopping\n");
317                          stopSelf();
318                  }
319         }
320
321         private void send_to_client(Messenger client) {
322                 Message m = message();
323                 try {
324                         client.send(m);
325                 } catch (RemoteException e) {
326                         AltosDebug.error("Client %s disappeared", client.toString());
327                         remove_client(client);
328                 }
329         }
330
331         private void send_to_clients() {
332                 for (Messenger client : clients)
333                         send_to_client(client);
334         }
335
336         private void send_idle_mode_to_client(Messenger client) {
337                 Message m = Message.obtain(null, AltosDroid.MSG_IDLE_MODE, idle_monitor != null);
338                 try {
339                         client.send(m);
340                 } catch (RemoteException e) {
341                         AltosDebug.error("Client %s disappeared", client.toString());
342                         remove_client(client);
343                 }
344         }
345
346         private void send_idle_mode_to_clients() {
347                 for (Messenger client : clients)
348                         send_idle_mode_to_client(client);
349         }
350
351         private void telemetry_start() {
352                 if (telemetry_reader == null && idle_monitor == null && !ignite_running) {
353                         telemetry_reader = new TelemetryReader(altos_link, handler);
354                         telemetry_reader.start();
355                 }
356         }
357
358         private void telemetry_stop() {
359                 if (telemetry_reader != null) {
360                         AltosDebug.debug("disconnect(): stopping TelemetryReader");
361                         telemetry_reader.interrupt();
362                         try {
363                                 telemetry_reader.join();
364                         } catch (InterruptedException e) {
365                         }
366                         telemetry_reader = null;
367                 }
368         }
369
370         private void disconnect(boolean notify) {
371                 AltosDebug.debug("disconnect(): begin");
372
373                 telemetry_state.connect = TelemetryState.CONNECT_DISCONNECTED;
374                 telemetry_state.address = null;
375
376                 if (idle_monitor != null)
377                         stop_idle_monitor();
378
379                 if (altos_link != null)
380                         altos_link.closing();
381
382                 stop_receiver_voltage_timer();
383
384                 telemetry_stop();
385                 if (telemetry_logger != null) {
386                         AltosDebug.debug("disconnect(): stopping TelemetryLogger");
387                         telemetry_logger.stop();
388                         telemetry_logger = null;
389                 }
390                 if (altos_link != null) {
391                         AltosDebug.debug("disconnect(): stopping AltosDroidLink");
392                         altos_link.close();
393                         altos_link = null;
394                         ignite = null;
395                 }
396                 telemetry_state.config = null;
397                 if (notify) {
398                         AltosDebug.debug("disconnect(): send message to clients");
399                         send_to_clients();
400                         if (clients.isEmpty()) {
401                                 AltosDebug.debug("disconnect(): no clients, terminating");
402                                 stopSelf();
403                         }
404                 }
405         }
406
407         private void start_usb(UsbDevice device) {
408                 AltosUsb        d = new AltosUsb(this, device, handler);
409
410                 if (d != null) {
411                         disconnect(false);
412                         altos_link = d;
413                         try {
414                                 connected();
415                         } catch (InterruptedException ie) {
416                         }
417                 }
418         }
419
420         private void delete_serial(int serial) {
421                 telemetry_state.states.remove((Integer) serial);
422                 AltosPreferences.remove_state(serial);
423                 send_to_clients();
424         }
425
426         private void start_altos_bluetooth(DeviceAddress address, boolean pause) {
427                 if (bluetooth_adapter == null || !bluetooth_adapter.isEnabled())
428                         return;
429
430                 disconnect(false);
431
432                 // Get the BluetoothDevice object
433                 BluetoothDevice device = bluetooth_adapter.getRemoteDevice(address.address);
434
435                 this.address = address;
436                 AltosDebug.debug("start_altos_bluetooth(): Connecting to %s (%s)", device.getName(), device.getAddress());
437                 altos_link = new AltosBluetooth(device, handler, pause);
438                 telemetry_state.connect = TelemetryState.CONNECT_CONNECTING;
439                 telemetry_state.address = address;
440                 send_to_clients();
441         }
442
443         private void start_idle_monitor() {
444                 if (altos_link != null && idle_monitor == null) {
445                         telemetry_stop();
446                         idle_monitor = new AltosIdleMonitor(this, altos_link, true, false);
447                         idle_monitor.set_callsign(AltosPreferences.callsign());
448                         idle_monitor.start();
449                         send_idle_mode_to_clients();
450                 }
451         }
452
453         private void stop_idle_monitor() {
454                 if (idle_monitor != null) {
455                         try {
456                                 idle_monitor.abort();
457                         } catch (InterruptedException ie) {
458                         }
459                         idle_monitor = null;
460                         telemetry_start();
461                         send_idle_mode_to_clients();
462                 }
463         }
464
465         private void reboot_remote() {
466                 if (altos_link != null) {
467                         stop_idle_monitor();
468                         try {
469                                 altos_link.start_remote();
470                                 altos_link.printf("r eboot\n");
471                                 altos_link.flush_output();
472                         } catch (TimeoutException te) {
473                         } catch (InterruptedException ie) {
474                         } finally {
475                                 try {
476                                         altos_link.stop_remote();
477                                 } catch (InterruptedException ie) {
478                                 }
479                         }
480                 }
481         }
482
483         private void ensure_ignite() {
484                 if (ignite == null)
485                         ignite = new AltosIgnite(altos_link, true, false);
486         }
487
488         private synchronized void igniter_query(Messenger client) {
489                 ensure_ignite();
490                 HashMap<String,Integer> status_map = null;
491                 ignite_running = true;
492                 try {
493                         stop_idle_monitor();
494                         try {
495                                 status_map = ignite.status();
496                         } catch (InterruptedException ie) {
497                                 AltosDebug.debug("ignite.status interrupted");
498                         } catch (TimeoutException te) {
499                                 AltosDebug.debug("ignite.status timeout");
500                         }
501                 } finally {
502                         ignite_running = false;
503                 }
504                 Message m = Message.obtain(null, AltosDroid.MSG_IGNITER_STATUS, status_map);
505                 try {
506                         client.send(m);
507                 } catch (RemoteException e) {
508                 }
509         }
510
511         private synchronized void igniter_fire(String igniter) {
512                 ensure_ignite();
513                 ignite_running = true;
514                 stop_idle_monitor();
515                 try {
516                         ignite.fire(igniter);
517                 } catch (InterruptedException ie) {
518                 } finally {
519                         ignite_running = false;
520                 }
521         }
522
523         // Timer for receiver battery voltage monitoring
524         Timer receiver_voltage_timer;
525
526         private void update_receiver_voltage() {
527                 if (altos_link != null && idle_monitor == null && !ignite_running) {
528                         try {
529                                 double  voltage = altos_link.monitor_battery();
530                                 telemetry_state.receiver_battery = voltage;
531                                 send_to_clients();
532                         } catch (InterruptedException ie) {
533                         }
534                 }
535         }
536
537         private void stop_receiver_voltage_timer() {
538                 if (receiver_voltage_timer != null) {
539                         receiver_voltage_timer.cancel();
540                         receiver_voltage_timer.purge();
541                         receiver_voltage_timer = null;
542                 }
543         }
544
545         private void start_receiver_voltage_timer() {
546                 if (receiver_voltage_timer == null && altos_link.has_monitor_battery()) {
547                         receiver_voltage_timer = new Timer();
548                         receiver_voltage_timer.scheduleAtFixedRate(new TimerTask() { public void run() {update_receiver_voltage();}}, 1000L, 10000L);
549                 }
550         }
551
552         private void connected() throws InterruptedException {
553                 AltosDebug.debug("connected top");
554                 AltosDebug.check_ui("connected\n");
555                 try {
556                         if (altos_link == null)
557                                 throw new InterruptedException("no bluetooth");
558                         telemetry_state.config = altos_link.config_data();
559                         altos_link.set_radio_frequency(telemetry_state.frequency);
560                         altos_link.set_telemetry_rate(telemetry_state.telemetry_rate);
561                 } catch (TimeoutException e) {
562                         // If this timed out, then we really want to retry it, but
563                         // probably safer to just retry the connection from scratch.
564                         AltosDebug.debug("connected timeout");
565                         if (address != null) {
566                                 AltosDebug.debug("connected timeout, retrying");
567                                 start_altos_bluetooth(address, true);
568                         } else {
569                                 handler.obtainMessage(MSG_CONNECT_FAILED).sendToTarget();
570                                 disconnect(true);
571                         }
572                         return;
573                 }
574
575                 AltosDebug.debug("connected bluetooth configured");
576                 telemetry_state.connect = TelemetryState.CONNECT_CONNECTED;
577                 telemetry_state.address = address;
578
579                 telemetry_start();
580
581                 AltosDebug.debug("connected TelemetryReader started");
582
583                 telemetry_logger = new TelemetryLogger(this, altos_link);
584
585                 start_receiver_voltage_timer();
586
587                 AltosDebug.debug("Notify UI of connection");
588
589                 send_to_clients();
590         }
591
592
593         @Override
594         public void onCreate() {
595
596                 AltosDebug.init(this);
597
598                 // Initialise preferences
599                 AltosDroidPreferences.init(this);
600
601                 // Get local Bluetooth adapter
602                 bluetooth_adapter = BluetoothAdapter.getDefaultAdapter();
603
604                 telemetry_state = new TelemetryState();
605
606                 // Create a reference to the NotificationManager so that we can update our notifcation text later
607                 //mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
608
609                 telemetry_state.connect = TelemetryState.CONNECT_DISCONNECTED;
610                 telemetry_state.address = null;
611
612                 /* Pull the saved state information out of the preferences database
613                  */
614                 ArrayList<Integer> serials = AltosPreferences.list_states();
615
616                 telemetry_state.latest_serial = AltosPreferences.latest_state();
617
618                 AltosDebug.debug("latest serial %d\n", telemetry_state.latest_serial);
619
620                 for (int serial : serials) {
621                         AltosState saved_state = AltosPreferences.state(serial);
622                         if (saved_state != null) {
623                                 if (telemetry_state.latest_serial == 0)
624                                         telemetry_state.latest_serial = serial;
625
626                                 AltosDebug.debug("recovered old state serial %d flight %d",
627                                                  serial,
628                                                  saved_state.flight);
629                                 if (saved_state.gps != null)
630                                         AltosDebug.debug("\tposition %f,%f",
631                                                          saved_state.gps.lat,
632                                                          saved_state.gps.lon);
633                                 telemetry_state.states.put(serial, saved_state);
634                         } else {
635                                 AltosDebug.debug("Failed to recover state for %d", serial);
636                                 AltosPreferences.remove_state(serial);
637                         }
638                 }
639         }
640
641         @Override
642         public int onStartCommand(Intent intent, int flags, int startId) {
643                 AltosDebug.debug("Received start id %d: %s", startId, intent);
644
645                 CharSequence text = getText(R.string.telemetry_service_started);
646
647                 // Create notification to be displayed while the service runs
648                 Notification notification = new Notification(R.drawable.am_status_c, text, 0);
649
650                 // The PendingIntent to launch our activity if the user selects this notification
651                 PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
652                                 new Intent(this, AltosDroid.class), 0);
653
654                 // Set the info for the views that show in the notification panel.
655                 notification.setLatestEventInfo(this, getText(R.string.telemetry_service_label), text, contentIntent);
656
657                 // Set the notification to be in the "Ongoing" section.
658                 notification.flags |= Notification.FLAG_ONGOING_EVENT;
659
660                 // Move us into the foreground.
661                 startForeground(NOTIFICATION, notification);
662
663                 /* Start bluetooth if we don't have a connection already */
664                 if (intent != null &&
665                     (telemetry_state.connect == TelemetryState.CONNECT_NONE ||
666                      telemetry_state.connect == TelemetryState.CONNECT_DISCONNECTED))
667                 {
668                         String  action = intent.getAction();
669
670                         if (action.equals(AltosDroid.ACTION_BLUETOOTH)) {
671                                 DeviceAddress address = AltosDroidPreferences.active_device();
672                                 if (address != null && !address.address.startsWith("USB"))
673                                         start_altos_bluetooth(address, false);
674                         }
675                 }
676
677                 // We want this service to continue running until it is explicitly
678                 // stopped, so return sticky.
679                 return START_STICKY;
680         }
681
682         @Override
683         public void onDestroy() {
684
685                 // Stop the bluetooth Comms threads
686                 disconnect(true);
687
688                 // Demote us from the foreground, and cancel the persistent notification.
689                 stopForeground(true);
690
691                 // Tell the user we stopped.
692                 Toast.makeText(this, R.string.telemetry_service_stopped, Toast.LENGTH_SHORT).show();
693         }
694
695         @Override
696         public IBinder onBind(Intent intent) {
697                 return messenger.getBinder();
698         }
699
700         /* AltosIdleMonitorListener */
701         public void update(AltosState state, AltosListenerState listener_state) {
702                 telemetry_state.states.put(state.serial, state);
703                 telemetry_state.receiver_battery = listener_state.battery;
704                 send_to_clients();
705         }
706
707         public void failed() {
708         }
709
710         public void error(String reason) {
711                 stop_idle_monitor();
712         }
713 }