/** ****************************************************************************** * @file TelemetryMonitor.java * @author The OpenPilot Team, http://www.openpilot.org Copyright (C) 2012. * @brief High level monitoring of telemetry to handle connection and * disconnection and then signal the rest of the application. * This also makes sure to fetch all objects on initial connection. * @see The GNU Public License (GPL) Version 3 * *****************************************************************************/ /* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.openpilot_nonag.uavtalk; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.Observable; import java.util.Observer; import java.util.Timer; import java.util.TimerTask; import org.openpilot_nonag.androidgcs.telemetry.OPTelemetryService; import android.util.Log; public class TelemetryMonitor extends Observable { private static final String TAG = "TelemetryMonitor"; public static final int LOGLEVEL = 1; public static boolean DEBUG = LOGLEVEL > 2; public static final boolean WARN = LOGLEVEL > 1; public static final boolean ERROR = LOGLEVEL > 0; static final int STATS_UPDATE_PERIOD_MS = 4000; static final int STATS_CONNECT_PERIOD_MS = 1000; static final int CONNECTION_TIMEOUT_MS = 8000; private final boolean HANDSHAKE_IS_CONNECTED = false; private final UAVObjectManager objMngr; private final Telemetry tel; private boolean objectsRegistered; // private UAVObject objPending; private UAVObject gcsStatsObj; private UAVObject flightStatsObj; private final UAVObject firmwareIapObj; private Timer periodicTask; private int currentPeriod; private long lastUpdateTime; private final List<UAVObject> queue; private OPTelemetryService telemService; private boolean connected = false; private boolean objects_updated = false; public boolean getConnected() { return connected; }; public boolean getObjectsUpdated() { return objects_updated; }; public TelemetryMonitor(UAVObjectManager objMngr, Telemetry tel, OPTelemetryService s) { this(objMngr, tel); telemService = s; } public TelemetryMonitor(UAVObjectManager objMngr, Telemetry tel) { this.objMngr = objMngr; this.tel = tel; // this.objPending = null; queue = new ArrayList<UAVObject>(); objectsRegistered = false; // Get stats objects gcsStatsObj = objMngr.getObject("GCSTelemetryStats"); flightStatsObj = objMngr.getObject("FlightTelemetryStats"); firmwareIapObj = objMngr.getObject("FirmwareIAPObj"); // The first update of the firmwareIapObj will trigger registering the objects firmwareIapObj.addUpdatedObserver(firmwareIapUpdated); flightStatsObj.addUpdatedObserver(new Observer() { @Override public void update(Observable observable, Object data) { try { flightStatsUpdated((UAVObject) data); } catch (IOException e) { // The UAVTalk stream was broken, disconnect this signal // TODO: Should this actually be disconnected. Do we create // a new TelemetryMonitor for this // or fix the stream? flightStatsObj.removeUpdatedObserver(this); } } }); // Start update timer setPeriod(STATS_CONNECT_PERIOD_MS); } /** * Initiate object retrieval, initialize queue with objects to be retrieved. * * @throws IOException */ public synchronized void startRetrievingObjects() throws IOException { if (DEBUG) Log.d(TAG, "Start retrieving objects"); // Clear object queue queue.clear(); // Get all objects, add metaobjects, settings and data objects with // OnChange update mode to the queue List<List<UAVObject>> objs = objMngr.getObjects(); ListIterator<List<UAVObject>> objListIterator = objs.listIterator(); while (objListIterator.hasNext()) { List<UAVObject> instList = objListIterator.next(); UAVObject obj = instList.get(0); UAVObject.Metadata mdata = obj.getMetadata(); if (obj.isMetadata()) { queue.add(obj); } else /* Data object */ { UAVDataObject dobj = (UAVDataObject) obj; if (dobj.isSettings()) { queue.add(obj); } else { if (mdata.GetFlightTelemetryUpdateMode() == UAVObject.UpdateMode.UPDATEMODE_ONCHANGE) { queue.add(obj); } } } } // Start retrieving Log.d(TAG, "Starting to retrieve meta and settings objects from the autopilot (" + queue.size() + " objects)"); retrieveNextObject(); } /** * Cancel the object retrieval */ public void stopRetrievingObjects() { Log.d(TAG, "Stop retrieving objects"); queue.clear(); } final Observer transactionObserver = new Observer() { @Override public void update(Observable observable, Object data) { try { UAVObject.TransactionResult result = (UAVObject.TransactionResult) data; transactionCompleted(result.obj, result.success); } catch (IOException e) { // When the telemetry stream is broken disconnect these // updates observable.deleteObserver(this); } } }; /** * Retrieve the next object in the queue * * @throws IOException */ public synchronized void retrieveNextObject() throws IOException { // If queue is empty return if (queue.isEmpty()) { if (telemService != null) telemService.toastMessage("Connected"); if (DEBUG) Log.d(TAG, "All objects retrieved: Connected Successfully"); objects_updated = true; if (!HANDSHAKE_IS_CONNECTED) { setChanged(); notifyObservers(); } return; } // Get next object from the queue UAVObject obj = queue.remove(0); if (obj == null) { throw new Error("Got null object forom transaction queue"); } if (WARN) Log.d(TAG, "Retrieving object: " + obj.getName()); // TODO: Does this need to stay here permanently? This appears to be // used for setup mainly obj.addTransactionCompleted(transactionObserver); // Request update obj.updateRequested(); } /** * Called by the retrieved object when a transaction is completed. * * @throws IOException */ public synchronized void transactionCompleted(UAVObject obj, boolean success) throws IOException { if (WARN) Log.d(TAG, "transactionCompleted. Status: " + success); // Remove the listener for the event that just finished obj.removeTransactionCompleted(transactionObserver); if (!success) { // Right now success = false means received a NAK so don't // re-attempt if (ERROR) Log.e(TAG, "Transaction failed."); } // Process next object if telemetry is still available if (((String) gcsStatsObj.getField("Status").getValue()).compareTo("Connected") == 0) { retrieveNextObject(); } else { stopRetrievingObjects(); } } /** * Called each time the flight stats object is updated by the autopilot * * @throws IOException */ public synchronized void flightStatsUpdated(UAVObject obj) throws IOException { // Force update if not yet connected gcsStatsObj = objMngr.getObject("GCSTelemetryStats"); flightStatsObj = objMngr.getObject("FlightTelemetryStats"); if (DEBUG) Log.d(TAG, "GCS Status: " + gcsStatsObj.getField("Status").getValue()); if (DEBUG) Log.d(TAG, "Flight Status: " + flightStatsObj.getField("Status").getValue()); if (((String) gcsStatsObj.getField("Status").getValue()) .compareTo("Connected") == 0 || ((String) flightStatsObj.getField("Status").getValue()) .compareTo("Connected") == 0) { processStatsUpdates(); } } private long lastStatsTime; /** * Called periodically to update the statistics and connection status. * * @throws IOException */ public synchronized void processStatsUpdates() throws IOException { // Get telemetry stats if (DEBUG) Log.d(TAG, "processStatsUpdates()"); Telemetry.TelemetryStats telStats = tel.getStats(); if (DEBUG) Log.d(TAG, "processStatsUpdates() - stats reset"); // Need to compute time because this update is not regular enough float dT = (System.currentTimeMillis() - lastStatsTime) / 1000.0f; lastStatsTime = System.currentTimeMillis(); // Update stats object gcsStatsObj.getField("RxDataRate").setDouble(telStats.rxBytes / dT); gcsStatsObj.getField("TxDataRate").setDouble(telStats.txBytes / dT); UAVObjectField field; field = gcsStatsObj.getField("TxBytes"); field.setInt(field.getInt() + telStats.txBytes); field = gcsStatsObj.getField("TxFailures"); field.setInt(field.getInt() + telStats.txErrors); field = gcsStatsObj.getField("TxRetries"); field.setInt(field.getInt() + telStats.txRetries); field = gcsStatsObj.getField("RxBytes"); field.setInt(field.getInt() + telStats.rxBytes); field = gcsStatsObj.getField("RxFailures"); field.setInt(field.getInt() + telStats.rxErrors); tel.resetStats(); if (DEBUG) Log.d(TAG, "processStatsUpdates() - stats updated"); // Check for a connection timeout boolean connectionTimeout; if (telStats.rxObjects > 0) { lastUpdateTime = System.currentTimeMillis(); } if ((System.currentTimeMillis() - lastUpdateTime) > CONNECTION_TIMEOUT_MS) { connectionTimeout = true; } else { connectionTimeout = false; } // Update connection state gcsStatsObj = objMngr.getObject("GCSTelemetryStats"); flightStatsObj = objMngr.getObject("FlightTelemetryStats"); if (gcsStatsObj == null) { Log.d(TAG, "No GCS stats yet"); return; } UAVObjectField statusField = gcsStatsObj.getField("Status"); String oldStatus = new String((String) statusField.getValue()); if (DEBUG) Log.d(TAG, "GCS: " + statusField.getValue() + " Flight: " + flightStatsObj.getField("Status").getValue()); if (oldStatus.compareTo("Disconnected") == 0) { // Request connection statusField.setValue("HandshakeReq"); } else if (oldStatus.compareTo("HandshakeReq") == 0) { // Check for connection acknowledge if (((String) flightStatsObj.getField("Status").getValue()) .compareTo("HandshakeAck") == 0) { statusField.setValue("Connected"); if (DEBUG) Log.d(TAG, "Connected" + statusField.toString()); } } else if (oldStatus.compareTo("Connected") == 0) { // Check if the connection is still active and the the autopilot is // still connected if (((String) flightStatsObj.getField("Status").getValue()) .compareTo("Disconnected") == 0 || connectionTimeout) { statusField.setValue("Disconnected"); } } // Force telemetry update if not yet connected boolean gcsStatusChanged = !oldStatus.equals(statusField.getValue()); boolean gcsConnected = statusField.getValue().equals("Connected"); boolean gcsDisconnected = statusField.getValue().equals("Disconnected"); boolean flightConnected = flightStatsObj.getField("Status").equals( "Connected"); if (!gcsConnected || !flightConnected) { if (DEBUG) Log.d(TAG, "Sending gcs status"); gcsStatsObj.updated(); } // Act on new connections or disconnections if (gcsConnected && gcsStatusChanged) { if (DEBUG) Log.d(TAG, "Connection with the autopilot established"); setPeriod(STATS_UPDATE_PERIOD_MS); connected = true; objects_updated = false; if (objectsRegistered) startRetrievingObjects(); else firmwareIapObj.updateRequested(); if (HANDSHAKE_IS_CONNECTED) setChanged(); // Enabling this line makes the opConnected signal // occur whenever we get a handshake } if (gcsDisconnected && gcsStatusChanged) { if (DEBUG) Log.d(TAG, "Trying to connect to the autopilot"); setPeriod(STATS_CONNECT_PERIOD_MS); connected = false; objects_updated = false; setChanged(); } if (DEBUG) Log.d(TAG, "processStatsUpdates() - before notify"); notifyObservers(); if (DEBUG) Log.d(TAG, "processStatsUpdates() - after notify"); } private void setPeriod(int ms) { if (periodicTask == null) periodicTask = new Timer(); periodicTask.cancel(); currentPeriod = ms; periodicTask = new Timer(); periodicTask.scheduleAtFixedRate(new TimerTask() { @Override public void run() { try { processStatsUpdates(); } catch (IOException e) { // Once the stream has died stop trying to process these // updates periodicTask.cancel(); } } }, currentPeriod, currentPeriod); } public void stopMonitor() { periodicTask.cancel(); periodicTask = null; } private final Observer firmwareIapUpdated = new Observer() { @Override public void update(Observable observable, Object data) { if (DEBUG) Log.d(TAG, "Received firmware IAP Updated message"); UAVObjectField description = firmwareIapObj.getField("Description"); if (description == null || description.getNumElements() < 100) { telemService.toastMessage("Failed to determine UAVO set"); } else { final int HASH_SIZE_USED = 8; String jarName = new String(); for (int i = 0; i < HASH_SIZE_USED; i++) { jarName += String.format("%02x", (int) description.getDouble(i + 60)); } jarName += ".jar"; if (DEBUG) Log.d(TAG, "Attempting to load: " + jarName); if (telemService.loadUavobjects(jarName, objMngr)) { telemService.toastMessage("Loaded appropriate UAVO set"); objectsRegistered = true; try { startRetrievingObjects(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else telemService.toastMessage("Failed to load UAVO set: " + jarName); } firmwareIapObj.removeUpdatedObserver(this); } }; }