diff --git a/AidlLib/build.gradle b/AidlLib/build.gradle index dc26b2401c..0984907441 100644 --- a/AidlLib/build.gradle +++ b/AidlLib/build.gradle @@ -7,8 +7,8 @@ android { defaultConfig { minSdkVersion 14 targetSdkVersion 21 - versionCode 20046 - versionName '2.0.46' + versionCode 20052 + versionName '2.0.52' } defaultPublishConfig "release" @@ -37,6 +37,7 @@ android { } dependencies { + provided 'com.google.android.gms:play-services-base:6.5.87' compile project(':dependencyLibs:Mavlink') } diff --git a/AidlLib/src/com/o3dr/services/android/lib/data/ServiceDataContract.java b/AidlLib/src/com/o3dr/services/android/lib/data/ServiceDataContract.java index 13678b74ef..e68f937aaa 100644 --- a/AidlLib/src/com/o3dr/services/android/lib/data/ServiceDataContract.java +++ b/AidlLib/src/com/o3dr/services/android/lib/data/ServiceDataContract.java @@ -30,6 +30,11 @@ public class ServiceDataContract { */ public static final String EXTRA_REQUEST_TLOG_APP_ID = PACKAGE_NAME + ".extra.REQUEST_TLOG_APP_ID"; + /** + * Key used to access the file path for the request tlog data. + */ + public static final String EXTRA_TLOG_ABSOLUTE_PATH = "extra_tlog_absolute_path"; + /** * Action used to notify of the availability of a tlog file. */ diff --git a/AidlLib/src/com/o3dr/services/android/lib/drone/action/ExperimentalActions.java b/AidlLib/src/com/o3dr/services/android/lib/drone/action/ExperimentalActions.java index c3599607e6..35338b2236 100644 --- a/AidlLib/src/com/o3dr/services/android/lib/drone/action/ExperimentalActions.java +++ b/AidlLib/src/com/o3dr/services/android/lib/drone/action/ExperimentalActions.java @@ -14,4 +14,8 @@ public class ExperimentalActions { public static final String ACTION_SEND_MAVLINK_MESSAGE = Utils.PACKAGE_NAME + ".action.SEND_MAVLINK_MESSAGE"; public static final String EXTRA_MAVLINK_MESSAGE = "extra_mavlink_message"; + + public static final String ACTION_SET_RELAY = Utils.PACKAGE_NAME + ".action.SET_RELAY"; + public static final String EXTRA_RELAY_NUMBER = "extra_relay_number"; + public static final String EXTRA_IS_RELAY_ON = "extra_is_relay_on"; } diff --git a/AidlLib/src/com/o3dr/services/android/lib/drone/mission/MissionItemType.java b/AidlLib/src/com/o3dr/services/android/lib/drone/mission/MissionItemType.java index 95c9fa6bfa..050871d409 100644 --- a/AidlLib/src/com/o3dr/services/android/lib/drone/mission/MissionItemType.java +++ b/AidlLib/src/com/o3dr/services/android/lib/drone/mission/MissionItemType.java @@ -13,12 +13,12 @@ import com.o3dr.services.android.lib.drone.mission.item.command.SetServo; import com.o3dr.services.android.lib.drone.mission.item.command.Takeoff; import com.o3dr.services.android.lib.drone.mission.item.command.YawCondition; +import com.o3dr.services.android.lib.drone.mission.item.complex.StructureScanner; import com.o3dr.services.android.lib.drone.mission.item.complex.Survey; import com.o3dr.services.android.lib.drone.mission.item.spatial.Circle; import com.o3dr.services.android.lib.drone.mission.item.spatial.Land; import com.o3dr.services.android.lib.drone.mission.item.spatial.RegionOfInterest; import com.o3dr.services.android.lib.drone.mission.item.spatial.SplineWaypoint; -import com.o3dr.services.android.lib.drone.mission.item.complex.StructureScanner; import com.o3dr.services.android.lib.drone.mission.item.spatial.Waypoint; import com.o3dr.services.android.lib.util.ParcelableUtils; @@ -52,27 +52,15 @@ protected Parcelable.Creator getMissionItemCreator() { } }, - CIRCLE("Circle") { - @Override - public MissionItem getNewItem() { - return new Circle(); - } - - @Override - protected Creator getMissionItemCreator() { - return Circle.CREATOR; - } - }, - - REGION_OF_INTEREST("Region of Interest") { + TAKEOFF("Takeoff") { @Override public MissionItem getNewItem() { - return new RegionOfInterest(); + return new Takeoff(); } @Override - protected Creator getMissionItemCreator() { - return RegionOfInterest.CREATOR; + protected Parcelable.Creator getMissionItemCreator() { + return Takeoff.CREATOR; } }, @@ -83,33 +71,34 @@ public MissionItem getNewItem() { } @Override - protected Parcelable.Creator getMissionItemCreator(){ + protected Parcelable.Creator getMissionItemCreator() { return ChangeSpeed.CREATOR; } }, - TAKEOFF("Takeoff") { + CAMERA_TRIGGER("Camera Trigger") { @Override public MissionItem getNewItem() { - return new Takeoff(); + return new CameraTrigger(); } @Override - protected Parcelable.Creator getMissionItemCreator() { - return Takeoff.CREATOR; + protected Creator getMissionItemCreator() { + return CameraTrigger.CREATOR; } }, - LAND("Land") { + EPM_GRIPPER("EPM Gripper") { @Override public MissionItem getNewItem() { - return new Land(); + return new EpmGripper(); } @Override - protected Creator getMissionItemCreator() { - return Land.CREATOR; + protected Creator getMissionItemCreator() { + return EpmGripper.CREATOR; } + }, RETURN_TO_LAUNCH("Return to Launch") { @@ -124,64 +113,63 @@ protected Creator getMissionItemCreator() { } }, - SURVEY("Survey") { + LAND("Land") { @Override public MissionItem getNewItem() { - return new Survey(); + return new Land(); } @Override - protected Creator getMissionItemCreator() { - return Survey.CREATOR; + protected Creator getMissionItemCreator() { + return Land.CREATOR; } }, - STRUCTURE_SCANNER("Structure Scanner") { + CIRCLE("Circle") { @Override public MissionItem getNewItem() { - return new StructureScanner(); + return new Circle(); } @Override - protected Creator getMissionItemCreator() { - return StructureScanner.CREATOR; + protected Creator getMissionItemCreator() { + return Circle.CREATOR; } }, - CAMERA_TRIGGER("Camera Trigger") { + REGION_OF_INTEREST("Region of Interest") { @Override public MissionItem getNewItem() { - return new CameraTrigger(); + return new RegionOfInterest(); } @Override - protected Creator getMissionItemCreator() { - return CameraTrigger.CREATOR; + protected Creator getMissionItemCreator() { + return RegionOfInterest.CREATOR; } }, - EPM_GRIPPER("EPM Gripper") { + SURVEY("Survey") { @Override public MissionItem getNewItem() { - return new EpmGripper(); + return new Survey(); } @Override - protected Creator getMissionItemCreator() { - return EpmGripper.CREATOR; + protected Creator getMissionItemCreator() { + return Survey.CREATOR; } - }, - YAW_CONDITION("Set Yaw"){ + STRUCTURE_SCANNER("Structure Scanner") { @Override - public MissionItem getNewItem(){ - return new YawCondition(); + public MissionItem getNewItem() { + return new StructureScanner(); } @Override - protected Creator getMissionItemCreator() { - return YawCondition.CREATOR; + protected Creator getMissionItemCreator() { + return StructureScanner.CREATOR; } }, @@ -197,6 +185,18 @@ protected Creator getMissionItemCreator() { } }, + YAW_CONDITION("Set Yaw") { + @Override + public MissionItem getNewItem() { + return new YawCondition(); + } + + @Override + protected Creator getMissionItemCreator() { + return YawCondition.CREATOR; + } + }, + SET_RELAY("Set Relay") { @Override public MissionItem getNewItem() { @@ -214,22 +214,22 @@ protected Creator getMissionItemCreator() { private final String label; - private MissionItemType(String label){ + private MissionItemType(String label) { this.label = label; } - public String getLabel(){ + public String getLabel() { return this.label; } @Override - public String toString(){ + public String toString() { return getLabel(); } public abstract MissionItem getNewItem(); - public final Bundle storeMissionItem(MissionItem item){ + public final Bundle storeMissionItem(MissionItem item) { Bundle bundle = new Bundle(2); storeMissionItem(item, bundle); return bundle; @@ -242,17 +242,17 @@ public void storeMissionItem(MissionItem missionItem, Bundle bundle) { protected abstract Creator getMissionItemCreator(); - public static T restoreMissionItemFromBundle(Bundle bundle){ - if(bundle == null) + public static T restoreMissionItemFromBundle(Bundle bundle) { + if (bundle == null) return null; String typeName = bundle.getString(EXTRA_MISSION_ITEM_TYPE); byte[] marshalledItem = bundle.getByteArray(EXTRA_MISSION_ITEM); - if(typeName == null || marshalledItem == null) + if (typeName == null || marshalledItem == null) return null; MissionItemType type = MissionItemType.valueOf(typeName); - if(type == null) + if (type == null) return null; T missionItem = ParcelableUtils.unmarshall(marshalledItem, type.getMissionItemCreator()); diff --git a/AidlLib/src/com/o3dr/services/android/lib/util/googleApi/GoogleApiClientManager.java b/AidlLib/src/com/o3dr/services/android/lib/util/googleApi/GoogleApiClientManager.java new file mode 100644 index 0000000000..5a56c206a8 --- /dev/null +++ b/AidlLib/src/com/o3dr/services/android/lib/util/googleApi/GoogleApiClientManager.java @@ -0,0 +1,328 @@ +package com.o3dr.services.android.lib.util.googleApi; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Handles the lifecycle for the google api client. Also takes care of running submitted tasks + * when the google api client is connected. + */ +public class GoogleApiClientManager implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { + + private final static String TAG = GoogleApiClientManager.class.getSimpleName(); + + public interface ManagerListener { + void onGoogleApiConnectionError(ConnectionResult result); + + void onUnavailableGooglePlayServices(int status); + + void onManagerStarted(); + + void onManagerStopped(); + } + + /** + * Manager background thread used to run the submitted google api client tasks. + */ + private final Runnable mDriverRunnable = new Runnable() { + @Override + public void run() { + try { + while (isStarted.get()) { + if (!mGoogleApiClient.isConnected()) { + stop(); + continue; + } + + final GoogleApiClientTask task = mTaskQueue.take(); + if (task == null) + continue; + + if (task.mRunOnBackgroundThread) { + mBgHandler.post(task); + } else { + mMainHandler.post(task); + } + } + } catch (InterruptedException e) { + Log.v(TAG, e.getMessage(), e); + } + } + }; + + private final GoogleApiClientTask stopTask = new GoogleApiClientTask() { + @Override + protected void doRun() { + stop(); + } + }; + + private final AtomicBoolean isStarted = new AtomicBoolean(false); + + private Thread mDriverThread; + + /** + * This handler is in charge of running google api client tasks on the calling thread. + */ + private final Handler mMainHandler; + + /** + * This handler is in charge of running google api client tasks on the background thread. + */ + private Handler mBgHandler; + private HandlerThread mBgHandlerThread; + + /** + * Application context. + */ + private final Context mContext; + + /** + * Handle to the google api client. + */ + private final GoogleApiClient mGoogleApiClient; + + private ManagerListener listener; + + /** + * Holds tasks that needs to be run using the google api client. + * A background thread will be blocking on this queue until new tasks are inserted. In which + * case, it will retrieve the new task, and process it. + */ + private final LinkedBlockingQueue mTaskQueue = new LinkedBlockingQueue<>(); + + public GoogleApiClientManager(Context context, Handler handler, + Api[] apis) { + mContext = context; + mMainHandler = handler; + + final GoogleApiClient.Builder apiBuilder = new GoogleApiClient.Builder(context); + for (Api api : apis) { + apiBuilder.addApi(api); + } + + mGoogleApiClient = apiBuilder + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + public void setManagerListener(ManagerListener listener) { + this.listener = listener; + } + + private void destroyBgHandler() { + if (mBgHandlerThread != null && mBgHandlerThread.isAlive()) { + mBgHandlerThread.quit(); + mBgHandlerThread.interrupt(); + mBgHandlerThread = null; + } + + mBgHandler = null; + } + + private void destroyDriverThread() { + if (mDriverThread != null && mDriverThread.isAlive()) { + mDriverThread.interrupt(); + mDriverThread = null; + } + } + + private void initializeBgHandler() { + if (mBgHandlerThread == null || mBgHandlerThread.isInterrupted()) { + mBgHandlerThread = new HandlerThread("GAC Manager Background Thread"); + mBgHandlerThread.start(); + mBgHandler = null; + } + + if (mBgHandler == null) { + mBgHandler = new Handler(mBgHandlerThread.getLooper()); + } + } + + private void initializeDriverThread() { + if (mDriverThread == null || mDriverThread.isInterrupted()) { + mDriverThread = new Thread(mDriverRunnable, "GAC Manager Driver Thread"); + mDriverThread.start(); + } + } + + /** + * Adds a task to the google api client manager tasks queue. This task will be scheduled to + * run on the calling thread. + * + * @param task task making use of the google api client. + * @return true if the task was successfully added to the queue. + * @throws IllegalStateException is the start() method was not called. + */ + public boolean addTask(GoogleApiClientTask task) { + if (!isStarted()) { + Log.d(TAG, "GoogleApiClientManager is not started."); + return false; + } + + task.gApiClient = mGoogleApiClient; + task.taskQueue = mTaskQueue; + task.mRunOnBackgroundThread = false; + return mTaskQueue.offer(task); + } + + /** + * Adds a task to the google api client manager tasks queue. This task will be scheduled to + * run on a background thread. + * + * @param task task making use of the google api client. + * @return true if the task was successfully added to the queue. + * @throws IllegalStateException is the start() method was not called. + */ + public boolean addTaskToBackground(GoogleApiClientTask task) { + if (!isStarted()) { + Log.d(TAG, "GoogleApiClientManager is not started."); + return false; + } + + task.gApiClient = mGoogleApiClient; + task.taskQueue = mTaskQueue; + task.mRunOnBackgroundThread = true; + return mTaskQueue.offer(task); + } + + /** + * @return true the google api client manager was started. + */ + private boolean isStarted() { + return isStarted.get(); + } + + /** + * Activates the google api client manager. + */ + public void start() { + //Check if google play services is available. + final int playStatus = GooglePlayServicesUtil.isGooglePlayServicesAvailable(mContext); + final boolean isValid = playStatus == ConnectionResult.SUCCESS; + + if (isValid) { + //Clear the queue + mTaskQueue.clear(); + + //Toggle the started flag + isStarted.set(true); + if (mGoogleApiClient.isConnected()) { + onConnected(null); + } else if (!mGoogleApiClient.isConnecting()) { + //Connect to the google api. + mGoogleApiClient.connect(); + } + } else { + Log.e(TAG, "Google Play Services is unavailable."); + if (listener != null) + listener.onUnavailableGooglePlayServices(playStatus); + } + } + +// private boolean isGooglePlayServicesValid(){ +// // Check for the google play services is available +// +// if(!isValid){ +// PendingIntent errorPI = GooglePlayServicesUtil.getErrorPendingIntent(playStatus, mContext, 0); +// if(errorPI != null){ +// try { +// errorPI.send(); +// } catch (PendingIntent.CanceledException e) { +// Log.e(TAG, "Seems the pending intent was cancelled.", e); +// } +// } +// } +// +// return isValid; +// } + + /** + * Release the resources used by this manager. + * After calling this method, start() needs to be called again to use that manager again. + */ + private void stop() { + isStarted.set(false); + destroyDriverThread(); + destroyBgHandler(); + + mTaskQueue.clear(); + if (mGoogleApiClient.isConnected() || mGoogleApiClient.isConnecting()) { + mGoogleApiClient.disconnect(); + } + + if(listener != null) + listener.onManagerStopped(); + } + + public void stopSafely(){ + addTask(stopTask); + } + + @Override + public void onConnected(Bundle bundle) { + initializeBgHandler(); + initializeDriverThread(); + + if (listener != null) + listener.onManagerStarted(); + } + + @Override + public void onConnectionSuspended(int i) { + + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + if(listener != null) + listener.onGoogleApiConnectionError(connectionResult); + + stop(); + } + + /** + * Type for the google api client tasks. + */ + public static abstract class GoogleApiClientTask implements Runnable { + + /** + * If true, this task will be scheduled to run on a background thread. + * Otherwise, it will run on the calling thread. + */ + private boolean mRunOnBackgroundThread = false; + private GoogleApiClient gApiClient; + private LinkedBlockingQueue taskQueue; + + protected GoogleApiClient getGoogleApiClient() { + return gApiClient; + } + + @Override + public void run() { + if (!gApiClient.isConnected()) { + //Add the task back to the queue. + taskQueue.offer(this); + return; + } + + //Run the task + doRun(); + } + + protected abstract void doRun(); + + } +} diff --git a/ClientLib/mobile/build.gradle b/ClientLib/mobile/build.gradle index 21d13f04e1..5138903a31 100644 --- a/ClientLib/mobile/build.gradle +++ b/ClientLib/mobile/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' ext { PUBLISH_ARTIFACT_ID = '3dr-services-lib' - PUBLISH_VERSION = '2.2.10' + PUBLISH_VERSION = '2.2.16' PROJECT_DESCRIPTION = "3DR Services Client Library" PROJECT_LABELS = ['3DR', '3DR Services', 'DroneAPI', 'Android'] PROJECT_LICENSES = ['Apache-2.0'] @@ -15,7 +15,7 @@ android { defaultConfig { minSdkVersion 14 targetSdkVersion 21 - versionCode 20210 + versionCode 20216 versionName PUBLISH_VERSION } diff --git a/ClientLib/mobile/libs/AidlLib.jar b/ClientLib/mobile/libs/AidlLib.jar index a48ef164f4..b17a554d2a 100644 Binary files a/ClientLib/mobile/libs/AidlLib.jar and b/ClientLib/mobile/libs/AidlLib.jar differ diff --git a/ClientLib/mobile/src/main/java/com/o3dr/android/client/apis/drone/ExperimentalApi.java b/ClientLib/mobile/src/main/java/com/o3dr/android/client/apis/drone/ExperimentalApi.java index fbbdad40c7..da639d3af8 100644 --- a/ClientLib/mobile/src/main/java/com/o3dr/android/client/apis/drone/ExperimentalApi.java +++ b/ClientLib/mobile/src/main/java/com/o3dr/android/client/apis/drone/ExperimentalApi.java @@ -8,12 +8,15 @@ import static com.o3dr.services.android.lib.drone.action.ExperimentalActions.ACTION_EPM_COMMAND; import static com.o3dr.services.android.lib.drone.action.ExperimentalActions.ACTION_SEND_MAVLINK_MESSAGE; +import static com.o3dr.services.android.lib.drone.action.ExperimentalActions.ACTION_SET_RELAY; import static com.o3dr.services.android.lib.drone.action.ExperimentalActions.ACTION_TRIGGER_CAMERA; import static com.o3dr.services.android.lib.drone.action.ExperimentalActions.EXTRA_EPM_RELEASE; +import static com.o3dr.services.android.lib.drone.action.ExperimentalActions.EXTRA_IS_RELAY_ON; import static com.o3dr.services.android.lib.drone.action.ExperimentalActions.EXTRA_MAVLINK_MESSAGE; +import static com.o3dr.services.android.lib.drone.action.ExperimentalActions.EXTRA_RELAY_NUMBER; /** - * Created by Fredia Huya-Kouadio on 1/19/15. + * Contains drone commands with no defined interaction model yet. */ public class ExperimentalApi { @@ -48,4 +51,18 @@ public static void sendMavlinkMessage(Drone drone, MavlinkMessageWrapper message params.putParcelable(EXTRA_MAVLINK_MESSAGE, messageWrapper); drone.performAsyncAction(new Action(ACTION_SEND_MAVLINK_MESSAGE, params)); } + + /** + * Set a Relay pin’s voltage high or low + * + * @param drone target vehicle + * @param relayNumber + * @param enabled true for relay to be on, false for relay to be off. + */ + public static void setRelay(Drone drone, int relayNumber, boolean enabled) { + Bundle params = new Bundle(2); + params.putInt(EXTRA_RELAY_NUMBER, relayNumber); + params.putBoolean(EXTRA_IS_RELAY_ON, enabled); + drone.performAsyncAction(new Action(ACTION_SET_RELAY, params)); + } } diff --git a/ServiceApp/build.gradle b/ServiceApp/build.gradle index 8dac9dd5f9..b75650a5ed 100644 --- a/ServiceApp/build.gradle +++ b/ServiceApp/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.application' dependencies { - compile 'com.google.android.gms:play-services-base:6.5.87' + compile 'com.google.android.gms:play-services-location:6.5.87' compile 'com.android.support:support-v4:21.0.3' @@ -27,7 +27,7 @@ android { applicationId 'org.droidplanner.services.android' minSdkVersion 14 targetSdkVersion 21 - versionCode 10205 + versionCode 10207 versionName getGitVersion() } diff --git a/ServiceApp/src/org/droidplanner/services/android/api/DPServices.java b/ServiceApp/src/org/droidplanner/services/android/api/DPServices.java index 73a1016839..faf56ac1d2 100644 --- a/ServiceApp/src/org/droidplanner/services/android/api/DPServices.java +++ b/ServiceApp/src/org/droidplanner/services/android/api/DPServices.java @@ -11,6 +11,7 @@ import com.o3dr.services.android.lib.model.IDroneApi; import org.droidplanner.services.android.BuildConfig; +import org.droidplanner.services.android.drone.DroneManager; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -23,18 +24,14 @@ final class DPServices extends IDroidPlannerServices.Stub { private final static String TAG = DPServices.class.getSimpleName(); - private final WeakReference serviceRef; + private DroidPlannerService serviceRef; DPServices(DroidPlannerService service) { - serviceRef = new WeakReference(service); + serviceRef = service; } - private DroidPlannerService getService() { - final DroidPlannerService service = serviceRef.get(); - if (service == null) - throw new IllegalStateException("Lost reference to parent service."); - - return service; + void destroy(){ + serviceRef = null; } @Override @@ -49,7 +46,7 @@ public int getApiVersionCode() throws RemoteException { @Override public IDroneApi registerDroneApi(IApiListener listener, String appId) throws RemoteException { - return getService().registerDroneApi(listener, appId); + return serviceRef.registerDroneApi(listener, appId); } @Override @@ -57,17 +54,20 @@ public Bundle[] getConnectedApps(String requesterId) throws RemoteException { Log.d(TAG, "List of connected apps request from " + requesterId); List appsInfo = new ArrayList<>(); - for(DroneApi droneApi : getService().droneApiStore){ + for(DroneApi droneApi : serviceRef.droneApiStore.values()){ if(droneApi.isConnected()){ - final ConnectionParameter droneParams = droneApi.getDroneManager().getConnectionParameter(); - final ConnectionParameter sanitizedParams = new ConnectionParameter(droneParams.getConnectionType(), - droneParams.getParamsBundle(), null); - - Bundle info = new Bundle(); - info.putString(GCSEvent.EXTRA_APP_ID, droneApi.getOwnerId()); - info.putParcelable(GCSEvent.EXTRA_VEHICLE_CONNECTION_PARAMETER, sanitizedParams); - - appsInfo.add(info); + DroneManager droneManager = droneApi.getDroneManager(); + if(droneManager != null) { + final ConnectionParameter droneParams = droneApi.getDroneManager().getConnectionParameter(); + final ConnectionParameter sanitizedParams = new ConnectionParameter(droneParams.getConnectionType(), + droneParams.getParamsBundle(), null); + + Bundle info = new Bundle(); + info.putString(GCSEvent.EXTRA_APP_ID, droneApi.getOwnerId()); + info.putParcelable(GCSEvent.EXTRA_VEHICLE_CONNECTION_PARAMETER, sanitizedParams); + + appsInfo.add(info); + } } } @@ -78,7 +78,7 @@ public Bundle[] getConnectedApps(String requesterId) throws RemoteException { public void releaseDroneApi(IDroneApi dpApi) throws RemoteException { Log.d(TAG, "Releasing acquired drone api handle."); if(dpApi instanceof DroneApi) { - getService().releaseDroneApi((DroneApi) dpApi); + serviceRef.releaseDroneApi(((DroneApi) dpApi).getOwnerId()); } } } diff --git a/ServiceApp/src/org/droidplanner/services/android/api/DroidPlannerService.java b/ServiceApp/src/org/droidplanner/services/android/api/DroidPlannerService.java index 013558da4a..254799d8c7 100644 --- a/ServiceApp/src/org/droidplanner/services/android/api/DroidPlannerService.java +++ b/ServiceApp/src/org/droidplanner/services/android/api/DroidPlannerService.java @@ -9,9 +9,10 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; -import android.os.Looper; import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; import android.util.Log; import com.google.android.gms.analytics.HitBuilders; @@ -28,15 +29,17 @@ import org.droidplanner.services.android.communication.connection.AndroidUdpConnection; import org.droidplanner.services.android.communication.connection.BluetoothConnection; import org.droidplanner.services.android.communication.connection.usb.UsbConnection; +import org.droidplanner.services.android.drone.DroneManager; +import org.droidplanner.services.android.exception.ConnectionException; +import org.droidplanner.services.android.interfaces.DroneEventsListener; import org.droidplanner.services.android.ui.activity.MainActivity; import org.droidplanner.services.android.utils.Utils; import org.droidplanner.services.android.utils.analytics.GAUtils; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; /** - * Created by fhuya on 10/30/14. + * 3DR Services background service implementation. */ public class DroidPlannerService extends Service { @@ -47,16 +50,25 @@ public class DroidPlannerService extends Service { public static final String ACTION_DRONE_CREATED = Utils.PACKAGE_NAME + ".ACTION_DRONE_CREATED"; public static final String ACTION_DRONE_DESTROYED = Utils.PACKAGE_NAME + ".ACTION_DRONE_DESTROYED"; public static final String ACTION_KICK_START_DRONESHARE_UPLOADS = Utils.PACKAGE_NAME + ".ACTION_KICK_START_DRONESHARE_UPLOADS"; + public static final String ACTION_RELEASE_API_INSTANCE = Utils.PACKAGE_NAME + ".action.RELEASE_API_INSTANCE"; + public static final String EXTRA_API_INSTANCE_APP_ID = "extra_api_instance_app_id"; private LocalBroadcastManager lbm; - final ConcurrentLinkedQueue droneApiStore = new ConcurrentLinkedQueue(); + final ConcurrentHashMap droneApiStore = new ConcurrentHashMap<>(); /** * Caches mavlink connections per connection type. */ final ConcurrentHashMap mavConnections = new ConcurrentHashMap<>(); + /** + * Caches drone managers per connection type. + */ + final ConcurrentHashMap droneManagers = new ConcurrentHashMap<>(); + + private HandlerThread handlerThread; + private DPServices dpServices; private DroneAccess droneAccess; private MavLinkServiceApi mavlinkApi; @@ -65,19 +77,55 @@ DroneApi registerDroneApi(IApiListener listener, String appId) { if (listener == null) return null; - DroneApi droneApi = new DroneApi(this, new Handler(Looper.getMainLooper()), mavlinkApi, listener, appId); - droneApiStore.add(droneApi); + DroneApi droneApi = new DroneApi(this, handlerThread.getLooper(), mavlinkApi, listener, appId); + droneApiStore.put(appId, droneApi); lbm.sendBroadcast(new Intent(ACTION_DRONE_CREATED)); + updateForegroundNotification(); return droneApi; } - void releaseDroneApi(DroneApi droneApi) { - if (droneApi == null) + void releaseDroneApi(String appId) { + if (appId == null) return; - droneApiStore.remove(droneApi); - droneApi.destroy(); - lbm.sendBroadcast(new Intent(ACTION_DRONE_DESTROYED)); + DroneApi droneApi = droneApiStore.remove(appId); + if (droneApi != null) { + Log.d(TAG, "Releasing drone api instance for " + appId); + droneApi.destroy(); + lbm.sendBroadcast(new Intent(ACTION_DRONE_DESTROYED)); + updateForegroundNotification(); + } + } + + DroneManager connectDroneManager(ConnectionParameter connParams, String appId, + DroneEventsListener listener) throws ConnectionException { + if (connParams == null || TextUtils.isEmpty(appId) || listener == null) + return null; + + DroneManager droneMgr = droneManagers.get(connParams); + if (droneMgr == null) { + Log.d(TAG, "Generating new drone manager."); + droneMgr = new DroneManager(getApplicationContext(), connParams, new Handler(handlerThread.getLooper()), + mavlinkApi); + droneManagers.put(connParams, droneMgr); + } + + Log.d(TAG, "Drone manager connection for " + appId); + droneMgr.connect(appId, listener); + return droneMgr; + } + + void disconnectDroneManager(DroneManager droneMgr, String appId) throws ConnectionException { + if (droneMgr == null || TextUtils.isEmpty(appId)) + return; + + Log.d(TAG, "Drone manager disconnection for " + appId); + droneMgr.disconnect(appId); + if (droneMgr.getConnectedAppsCount() == 0) { + Log.d(TAG, "Destroying drone manager."); + droneMgr.destroy(); + droneManagers.remove(droneMgr.getConnectionParameter()); + } } void connectMAVConnection(ConnectionParameter connParams, String listenerTag, @@ -140,6 +188,22 @@ void connectMAVConnection(ConnectionParameter connParams, String listenerTag, } } + void addLoggingFile(ConnectionParameter connParams, String tag, String loggingFilePath) { + AndroidMavLinkConnection conn = mavConnections.get(connParams); + if (conn == null) + return; + + conn.addLoggingPath(tag, loggingFilePath); + } + + void removeLoggingFile(ConnectionParameter connParams, String tag) { + AndroidMavLinkConnection conn = mavConnections.get(connParams); + if (conn == null) + return; + + conn.removeLoggingPath(tag); + } + void disconnectMAVConnection(ConnectionParameter connParams, String listenerTag) { final AndroidMavLinkConnection conn = mavConnections.get(connParams); if (conn == null) @@ -180,11 +244,20 @@ public void onCreate() { final Context context = getApplicationContext(); + handlerThread = new HandlerThread("Connected apps looper"); + handlerThread.start(); + mavlinkApi = new MavLinkServiceApi(this); droneAccess = new DroneAccess(this); dpServices = new DPServices(this); lbm = LocalBroadcastManager.getInstance(context); + updateForegroundNotification(); + } + + private void updateForegroundNotification() { + final Context context = getApplicationContext(); + //Put the service in the foreground final Notification.Builder notifBuilder = new Notification.Builder(context) .setContentTitle("3DR Services") @@ -192,6 +265,16 @@ public void onCreate() { .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0)); + final int connectedCount = droneApiStore.size(); + if(connectedCount > 0){ + if(connectedCount == 1){ + notifBuilder.setContentText("1 connected app"); + } + else{ + notifBuilder.setContentText(connectedCount + " connected apps"); + } + } + final Notification notification = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? notifBuilder.build() : notifBuilder.getNotification(); @@ -203,7 +286,7 @@ public void onDestroy() { super.onDestroy(); Log.d(TAG, "Destroying 3DR Services."); - for (DroneApi droneApi : droneApiStore) { + for (DroneApi droneApi : droneApiStore.values()) { droneApi.destroy(); } droneApiStore.clear(); @@ -214,6 +297,8 @@ public void onDestroy() { } mavConnections.clear(); + dpServices.destroy(); + handlerThread.quit(); stopForeground(true); } @@ -222,10 +307,17 @@ public void onDestroy() { public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { final String action = intent.getAction(); - if (ACTION_KICK_START_DRONESHARE_UPLOADS.equals(action)) { - for (DroneApi droneApi : droneApiStore) { - droneApi.getDroneManager().kickStartDroneShareUpload(); - } + switch (action) { + case ACTION_KICK_START_DRONESHARE_UPLOADS: + for (DroneManager droneMgr : droneManagers.values()) { + droneMgr.kickStartDroneShareUpload(); + } + break; + + case ACTION_RELEASE_API_INSTANCE: + final String appId = intent.getStringExtra(EXTRA_API_INSTANCE_APP_ID); + releaseDroneApi(appId); + break; } } diff --git a/ServiceApp/src/org/droidplanner/services/android/api/DroneAccess.java b/ServiceApp/src/org/droidplanner/services/android/api/DroneAccess.java index 2375fcaaf8..9fe1a30178 100644 --- a/ServiceApp/src/org/droidplanner/services/android/api/DroneAccess.java +++ b/ServiceApp/src/org/droidplanner/services/android/api/DroneAccess.java @@ -21,6 +21,6 @@ public final class DroneAccess extends Binder { } public List getDroneApiList() { - return new ArrayList<>(serviceRef.droneApiStore); + return new ArrayList<>(serviceRef.droneApiStore.values()); } } diff --git a/ServiceApp/src/org/droidplanner/services/android/api/DroneApi.java b/ServiceApp/src/org/droidplanner/services/android/api/DroneApi.java index 8964578e25..4eb7a66beb 100644 --- a/ServiceApp/src/org/droidplanner/services/android/api/DroneApi.java +++ b/ServiceApp/src/org/droidplanner/services/android/api/DroneApi.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; @@ -24,6 +25,7 @@ import com.o3dr.services.android.lib.drone.attribute.AttributeType; import com.o3dr.services.android.lib.drone.connection.ConnectionParameter; import com.o3dr.services.android.lib.drone.connection.ConnectionResult; +import com.o3dr.services.android.lib.drone.connection.DroneSharePrefs; import com.o3dr.services.android.lib.drone.mission.Mission; import com.o3dr.services.android.lib.drone.mission.MissionItemType; import com.o3dr.services.android.lib.drone.mission.action.MissionActions; @@ -58,7 +60,7 @@ import com.o3dr.services.android.lib.model.action.Action; import org.droidplanner.core.MAVLink.MavLinkArm; -import org.droidplanner.core.MAVLink.MavLinkROI; +import org.droidplanner.core.MAVLink.command.doCmd.MavLinkDoCmds; import org.droidplanner.core.drone.DroneInterfaces; import org.droidplanner.core.drone.profiles.VehicleProfile; import org.droidplanner.core.drone.variables.Calibration; @@ -75,6 +77,7 @@ import org.droidplanner.core.parameters.Parameter; import org.droidplanner.core.survey.CameraInfo; import org.droidplanner.core.survey.Footprint; +import org.droidplanner.core.util.Pair; import org.droidplanner.services.android.R; import org.droidplanner.services.android.drone.DroneManager; import org.droidplanner.services.android.exception.ConnectionException; @@ -86,8 +89,8 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; -import java.lang.ref.SoftReference; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -98,29 +101,34 @@ import ellipsoidFit.ThreeSpacePoint; /** - * Created by fhuya on 10/30/14. + * Implementation for the IDroneApi interface. */ public final class DroneApi extends IDroneApi.Stub implements DroneEventsListener, IBinder.DeathRecipient { private final static String TAG = DroneApi.class.getSimpleName(); - private final SoftReference serviceRef; private final Context context; private final DroneInterfaces.Handler droneHandler; private final ConcurrentLinkedQueue observersList; private final ConcurrentLinkedQueue mavlinkObserversList; - private final DroneManager droneMgr; + private DroneManager droneMgr; private final IApiListener apiListener; private final String ownerId; + private final DroidPlannerService service; + private ConnectionParameter connectionParams; private List cachedCameraDetails; - DroneApi(DroidPlannerService dpService, final Handler handler, MavLinkServiceApi mavlinkApi, IApiListener listener, + DroneApi(DroidPlannerService dpService, Looper looper, MavLinkServiceApi mavlinkApi, IApiListener listener, String ownerId) { + this.service = dpService; this.context = dpService.getApplicationContext(); - this.droneHandler = new DroneInterfaces.Handler() { + + final Handler handler = new Handler(looper); + + this.droneHandler = new DroneInterfaces.Handler() { @Override public void removeCallbacks(Runnable thread) { handler.removeCallbacks(thread); @@ -139,12 +147,8 @@ public void postDelayed(Runnable thread, long timeout) { this.ownerId = ownerId; - serviceRef = new SoftReference(dpService); - observersList = new ConcurrentLinkedQueue(); - mavlinkObserversList = new ConcurrentLinkedQueue(); - - this.droneMgr = new DroneManager(context, this.ownerId, handler, mavlinkApi); - this.droneMgr.setDroneEventsListener(this); + observersList = new ConcurrentLinkedQueue<>(); + mavlinkObserversList = new ConcurrentLinkedQueue<>(); this.apiListener = listener; try { @@ -152,18 +156,22 @@ public void postDelayed(Runnable thread, long timeout) { checkForSelfRelease(); } catch (RemoteException e) { Log.e(TAG, e.getMessage(), e); - dpService.releaseDroneApi(this); + dpService.releaseDroneApi(this.ownerId); } } void destroy() { - this.serviceRef.clear(); + Log.d(TAG, "Destroying drone api instance for " + this.ownerId); this.observersList.clear(); this.mavlinkObserversList.clear(); this.apiListener.asBinder().unlinkToDeath(this, 0); - this.droneMgr.setDroneEventsListener(null); - this.droneMgr.destroy(); + + try { + this.service.disconnectDroneManager(this.droneMgr, this.ownerId); + } catch (ConnectionException e) { + Log.e(TAG, e.getMessage(), e); + } } public String getOwnerId() { @@ -174,14 +182,6 @@ public DroneManager getDroneManager() { return this.droneMgr; } - private DroidPlannerService getService() { - final DroidPlannerService service = serviceRef.get(); - if (service == null) - throw new IllegalStateException("Lost reference to parent service."); - - return service; - } - @Override public Bundle getAttribute(String type) throws RemoteException { Bundle carrier = new Bundle(); @@ -234,6 +234,9 @@ public Bundle getAttribute(String type) throws RemoteException { } private CameraProxy getCameraProxy() { + if (droneMgr == null) + return null; + Drone drone = droneMgr.getDrone(); Camera droneCamera = drone.getCamera(); @@ -255,6 +258,9 @@ private CameraProxy getCameraProxy() { } private Gps getGps() { + if (droneMgr == null) + return new Gps(); + final GPS droneGps = droneMgr.getDrone().getGps(); LatLong dronePosition = droneGps.isPositionValid() ? new LatLong(droneGps.getPosition().getLat(), droneGps.getPosition().getLng()) @@ -265,6 +271,9 @@ private Gps getGps() { } private State getState() { + if (droneMgr == null) + return new State(); + final Drone drone = this.droneMgr.getDrone(); org.droidplanner.core.drone.variables.State droneState = drone.getState(); ApmModes droneMode = droneState.getMode(); @@ -402,13 +411,15 @@ private static int getDroneProxyType(int originalType) { } private Parameters getParameters() { + if (droneMgr == null) + return new Parameters(); + final Drone drone = this.droneMgr.getDrone(); - final Map proxyParams = - new HashMap(); + final Map proxyParams = new HashMap<>(); - List droneParameters = drone.getParameters().getParametersList(); + Map droneParameters = drone.getParameters().getParameters(); if (!droneParameters.isEmpty()) { - for (Parameter param : droneParameters) { + for (Parameter param : droneParameters.values()) { if (param.name != null) { proxyParams.put(param.name, new com.o3dr.services.android.lib.drone.property .Parameter(param.name, param.value, param.type)); @@ -420,22 +431,21 @@ private Parameters getParameters() { if (profile != null) { String metadataType = profile.getParameterMetadataType(); if (metadataType != null) { - ParameterMetadataLoader.load(getService().getApplicationContext(), - metadataType, proxyParams); + ParameterMetadataLoader.load(this.context, metadataType, proxyParams); } } - } catch (IOException e) { - Log.e(TAG, e.getMessage(), e); - } catch (XmlPullParserException e) { + } catch (IOException | XmlPullParserException e) { Log.e(TAG, e.getMessage(), e); } } - return new Parameters(new ArrayList(proxyParams.values())); + return new Parameters(new ArrayList<>(proxyParams.values())); } private Speed getSpeed() { + if (droneMgr == null) + return new Speed(); + org.droidplanner.core.drone.variables.Speed droneSpeed = this.droneMgr.getDrone().getSpeed(); return new Speed(droneSpeed.getVerticalSpeed().valueInMetersPerSecond(), droneSpeed.getGroundSpeed().valueInMetersPerSecond(), @@ -443,12 +453,18 @@ private Speed getSpeed() { } private Attitude getAttitude() { + if (droneMgr == null) + return new Attitude(); + Orientation droneOrientation = this.droneMgr.getDrone().getOrientation(); return new Attitude(droneOrientation.getRoll(), droneOrientation.getPitch(), droneOrientation.getYaw()); } private Home getHome() { + if (droneMgr == null) + return new Home(); + org.droidplanner.core.drone.variables.Home droneHome = this.droneMgr.getDrone().getHome(); LatLongAlt homePosition = droneHome.isValid() ? new LatLongAlt(droneHome.getCoord().getLat(), droneHome.getCoord().getLng(), @@ -459,22 +475,32 @@ private Home getHome() { } private Battery getBattery() { + if (droneMgr == null) + return new Battery(); + org.droidplanner.core.drone.variables.Battery droneBattery = this.droneMgr.getDrone().getBattery(); return new Battery(droneBattery.getBattVolt(), droneBattery.getBattRemain(), droneBattery.getBattCurrent(), droneBattery.getBattDischarge()); } private Altitude getAltitude() { + if (droneMgr == null) + return new Altitude(); + org.droidplanner.core.drone.variables.Altitude droneAltitude = this.droneMgr.getDrone().getAltitude(); return new Altitude(droneAltitude.getAltitude(), droneAltitude.getTargetAltitude()); } private Mission getMission() { + Mission proxyMission = new Mission(); + if (droneMgr == null) + return proxyMission; + final Drone drone = this.droneMgr.getDrone(); org.droidplanner.core.mission.Mission droneMission = drone.getMission(); List droneMissionItems = droneMission.getItems(); - Mission proxyMission = new Mission(); + proxyMission.setCurrentMissionItem((short) drone.getMissionStats().getCurrentWP()); if (!droneMissionItems.isEmpty()) { for (org.droidplanner.core.mission.MissionItem item : droneMissionItems) { @@ -486,6 +512,9 @@ private Mission getMission() { } private Signal getSignal() { + if (droneMgr == null) + return new Signal(); + Radio droneRadio = this.droneMgr.getDrone().getRadio(); return new Signal(droneRadio.isValid(), droneRadio.getRxErrors(), droneRadio.getFixed(), droneRadio.getTxBuf(), droneRadio.getRssi(), droneRadio.getRemRssi(), @@ -493,15 +522,21 @@ private Signal getSignal() { } private Type getType() { + if (droneMgr == null) + return new Type(); + final Drone drone = this.droneMgr.getDrone(); return new Type(getDroneProxyType(drone.getType()), drone.getFirmwareVersion()); } - boolean isConnected() { - return droneMgr.isConnected(); + public boolean isConnected() { + return droneMgr != null && droneMgr.isConnected(); } private GuidedState getGuidedState() { + if (droneMgr == null) + return new GuidedState(); + final GuidedPoint guidedPoint = this.droneMgr.getDrone().getGuidedPoint(); int guidedState; switch (guidedPoint.getState()) { @@ -530,6 +565,9 @@ private GuidedState getGuidedState() { } public void changeVehicleMode(VehicleMode newMode) { + if (droneMgr == null) + return; + int mavType; switch (newMode.getDroneType()) { default: @@ -550,15 +588,9 @@ public void changeVehicleMode(VehicleMode newMode) { } public void connect(ConnectionParameter connParams) { - if (connParams == null || !connParams.equals(droneMgr.getConnectionParameter())) - droneMgr.setConnectionParameter(connParams); - try { - // Do a quick scan to see if we need any droneshare uploads - if (connParams != null) { - this.droneMgr.kickStartDroneShareUpload(); - this.droneMgr.connect(); - } + this.connectionParams = connParams; + this.droneMgr = service.connectDroneManager(connParams, ownerId, this); } catch (ConnectionException e) { notifyConnectionFailed(new ConnectionResult(0, e.getMessage())); disconnect(); @@ -567,33 +599,37 @@ public void connect(ConnectionParameter connParams) { public void disconnect() { try { - droneMgr.disconnect(); + service.disconnectDroneManager(this.droneMgr, this.ownerId); + this.droneMgr = null; } catch (ConnectionException e) { notifyConnectionFailed(new ConnectionResult(0, e.getMessage())); } } public void refreshParameters() { + if (droneMgr == null) + return; this.droneMgr.getDrone().getParameters().refreshParameters(); } public void writeParameters(Parameters parameters) { - if (parameters == null) return; + if (droneMgr == null || parameters == null) return; - List parametersList = parameters - .getParameters(); + List parametersList = parameters.getParameters(); if (parametersList.isEmpty()) return; final Drone drone = this.droneMgr.getDrone(); org.droidplanner.core.drone.profiles.Parameters droneParams = drone.getParameters(); for (com.o3dr.services.android.lib.drone.property.Parameter proxyParam : parametersList) { - droneParams.sendParameter(new Parameter(proxyParam.getName(), proxyParam.getValue(), - proxyParam.getType())); + droneParams.sendParameter(new Parameter(proxyParam.getName(), proxyParam.getValue(), proxyParam.getType())); } } public void setMission(Mission mission, boolean pushToDrone) { + if (droneMgr == null) + return; + org.droidplanner.core.mission.Mission droneMission = this.droneMgr.getDrone().getMission(); droneMission.clearMissionItems(); @@ -607,6 +643,9 @@ public void setMission(Mission mission, boolean pushToDrone) { } public void generateDronie() { + if (droneMgr == null) + return; + float bearing = (float) this.droneMgr.getDrone().getMission().makeAndUploadDronie(); Bundle bundle = new Bundle(1); bundle.putFloat(AttributeEventExtra.EXTRA_MISSION_DRONIE_BEARING, bearing); @@ -614,19 +653,29 @@ public void generateDronie() { } public void arm(boolean arm) { + if (droneMgr == null) + return; MavLinkArm.sendArmMessage(this.droneMgr.getDrone(), arm); } public void startMagnetometerCalibration(double[] startPointsX, double[] startPointsY, double[] startPointsZ) { + if (droneMgr == null) + return; + this.droneMgr.startMagnetometerCalibration(MathUtils.pointsArrayToThreeSpacePoint(new double[][]{startPointsX, startPointsY, startPointsZ})); } public void stopMagnetometerCalibration() { + if (droneMgr == null) + return; this.droneMgr.stopMagnetometerCalibration(); } public void startIMUCalibration() { + if (droneMgr == null) + return; + if (!this.droneMgr.getDrone().getCalibrationSetup().startCalibration()) { Bundle extrasBundle = new Bundle(1); extrasBundle.putString(AttributeEventExtra.EXTRA_CALIBRATION_IMU_MESSAGE, @@ -636,23 +685,29 @@ public void startIMUCalibration() { } public void sendIMUCalibrationAck(int step) { + if (droneMgr == null) + return; + this.droneMgr.getDrone().getCalibrationSetup().sendAckk(step); } public void doGuidedTakeoff(double altitude) { + if (droneMgr == null) + return; + this.droneMgr.getDrone().getGuidedPoint().doGuidedTakeoff(new org.droidplanner.core .helpers.units.Altitude(altitude)); } public void sendMavlinkMessage(MavlinkMessageWrapper messageWrapper) { - if (messageWrapper == null) + if (droneMgr == null || messageWrapper == null) return; MAVLinkMessage message = messageWrapper.getMavLinkMessage(); if (message == null) return; - Drone drone = getDroneManager().getDrone(); + Drone drone = droneMgr.getDrone(); if (drone == null) return; @@ -662,6 +717,9 @@ public void sendMavlinkMessage(MavlinkMessageWrapper messageWrapper) { } public void sendGuidedPoint(LatLong point, boolean force) { + if (droneMgr == null) + return; + GuidedPoint guidedPoint = this.droneMgr.getDrone().getGuidedPoint(); if (guidedPoint.isInitialized()) { guidedPoint.newGuidedCoord(MathUtils.latLongToCoord2D(point)); @@ -675,10 +733,16 @@ public void sendGuidedPoint(LatLong point, boolean force) { } public void setGuidedAltitude(double altitude) { + if (droneMgr == null) + return; + this.droneMgr.getDrone().getGuidedPoint().changeGuidedAltitude(altitude); } public void enableFollowMe(FollowType followType) { + if (droneMgr == null) + return; + final FollowAlgorithm.FollowModes selectedMode = followTypeToMode(followType); if (selectedMode != null) { @@ -687,13 +751,16 @@ public void enableFollowMe(FollowType followType) { followMe.toggleFollowMeState(); FollowAlgorithm currentAlg = followMe.getFollowAlgorithm(); - if(currentAlg.getType() != selectedMode) { + if (currentAlg.getType() != selectedMode) { followMe.setAlgorithm(selectedMode.getAlgorithmType(droneMgr.getDrone(), droneHandler)); } } } private FollowState getFollowState() { + if (droneMgr == null) + return new FollowState(); + final Follow followMe = this.droneMgr.getFollowMe(); final int state; @@ -728,11 +795,11 @@ private FollowState getFollowState() { final FollowAlgorithm currentAlg = followMe.getFollowAlgorithm(); Map modeParams = currentAlg.getParams(); Bundle params = new Bundle(); - for(Map.Entry entry : modeParams.entrySet()){ - switch(entry.getKey()){ + for (Map.Entry entry : modeParams.entrySet()) { + switch (entry.getKey()) { case FollowType.EXTRA_FOLLOW_ROI_TARGET: Coord3D target = (Coord3D) entry.getValue(); - if(target != null){ + if (target != null) { params.putParcelable(entry.getKey(), new LatLongAlt(target.getLat(), target.getLng(), target.getAltitude().valueInMeters())); } @@ -740,7 +807,7 @@ private FollowState getFollowState() { case FollowType.EXTRA_FOLLOW_RADIUS: Double radius = (Double) entry.getValue(); - if(radius != null) + if (radius != null) params.putDouble(entry.getKey(), radius); break; } @@ -749,6 +816,9 @@ private FollowState getFollowState() { } private List getCameraDetails() { + if (droneMgr == null) + return Collections.emptyList(); + if (cachedCameraDetails == null) { final CameraInfoLoader camInfoLoader = this.droneMgr.getCameraInfoLoader(); List cameraInfoNames = camInfoLoader.getCameraInfoList(); @@ -775,11 +845,6 @@ private List getCameraDetails() { return cachedCameraDetails; } - private FootPrint getLastCameraFootPrint() { - Footprint lastFootprint = this.droneMgr.getDrone().getCamera().getLastFootprint(); - return getProxyCameraFootPrint(lastFootprint); - } - private static FootPrint getProxyCameraFootPrint(Footprint footprint) { if (footprint == null) return null; @@ -814,6 +879,9 @@ public void buildComplexMissionItem(Bundle itemBundle) { } private Survey buildSurvey(Survey survey) { + if (droneMgr == null) + return survey; + org.droidplanner.core.mission.Mission droneMission = this.droneMgr.getDrone().getMission(); org.droidplanner.core.mission.survey.Survey updatedSurvey = (org.droidplanner.core.mission.survey.Survey) ProxyUtils.getMissionItemImpl (droneMission, survey); @@ -822,6 +890,9 @@ private Survey buildSurvey(Survey survey) { } private StructureScanner buildStructureScanner(StructureScanner item) { + if (droneMgr == null) + return item; + org.droidplanner.core.mission.Mission droneMission = this.droneMgr.getDrone().getMission(); org.droidplanner.core.mission.waypoints.StructureScanner updatedScan = (org.droidplanner.core.mission.waypoints.StructureScanner) ProxyUtils .getMissionItemImpl(droneMission, item); @@ -834,7 +905,9 @@ private void checkForSelfRelease() { //Check if the apiListener is still connected instead. if (!apiListener.asBinder().pingBinder()) { Log.w(TAG, "Client is not longer available."); - getService().releaseDroneApi(this); + this.context.startService(new Intent(this.context, DroidPlannerService.class) + .setAction(DroidPlannerService.ACTION_RELEASE_API_INSTANCE) + .putExtra(DroidPlannerService.EXTRA_API_INSTANCE_APP_ID, this.ownerId)); } } @@ -869,15 +942,15 @@ public void removeMavlinkObserver(IMavlinkObserver observer) throws RemoteExcept @Override public void performAction(Action action) throws RemoteException { - if(action == null) + if (action == null) return; final String type = action.getType(); - if(type == null) + if (type == null) return; Bundle data = action.getData(); - switch(type){ + switch (type) { // MISSION ACTIONS case MissionActions.ACTION_GENERATE_DRONIE: generateDronie(); @@ -925,6 +998,14 @@ public void performAction(Action action) throws RemoteException { sendMavlinkMessage(messageWrapper); break; + case ExperimentalActions.ACTION_SET_RELAY: + if (droneMgr != null) { + int relayNumber = data.getInt(ExperimentalActions.EXTRA_RELAY_NUMBER); + boolean isOn = data.getBoolean(ExperimentalActions.EXTRA_IS_RELAY_ON); + MavLinkDoCmds.setRelay(droneMgr.getDrone(), relayNumber, isOn); + } + break; + //GUIDED ACTIONS case GuidedActions.ACTION_DO_GUIDED_TAKEOFF: double takeoffAltitude = data.getDouble(GuidedActions.EXTRA_ALTITUDE); @@ -995,34 +1076,34 @@ public void performAction(Action action) throws RemoteException { break; case FollowMeActions.ACTION_UPDATE_FOLLOW_PARAMS: - data.setClassLoader(LatLong.class.getClassLoader()); - - final FollowAlgorithm followAlgorithm = this.droneMgr.getFollowMe().getFollowAlgorithm(); - if(followAlgorithm != null){ - Map paramsMap = new HashMap<>(); - Set dataKeys = data.keySet(); - - for(String key: dataKeys){ - if(FollowType.EXTRA_FOLLOW_ROI_TARGET.equals(key)){ - LatLong target = data.getParcelable(key); - if(target != null) { - final Coord2D roiTarget; - if(target instanceof LatLongAlt) { - roiTarget = new Coord3D(target.getLatitude(), target.getLongitude(), - new org.droidplanner.core.helpers.units.Altitude(((LatLongAlt) target) - .getAltitude())); - } - else{ - roiTarget = new Coord2D(target.getLatitude(), target.getLongitude()); + if (droneMgr != null) { + data.setClassLoader(LatLong.class.getClassLoader()); + + final FollowAlgorithm followAlgorithm = this.droneMgr.getFollowMe().getFollowAlgorithm(); + if (followAlgorithm != null) { + Map paramsMap = new HashMap<>(); + Set dataKeys = data.keySet(); + + for (String key : dataKeys) { + if (FollowType.EXTRA_FOLLOW_ROI_TARGET.equals(key)) { + LatLong target = data.getParcelable(key); + if (target != null) { + final Coord2D roiTarget; + if (target instanceof LatLongAlt) { + roiTarget = new Coord3D(target.getLatitude(), target.getLongitude(), + new org.droidplanner.core.helpers.units.Altitude(((LatLongAlt) target) + .getAltitude())); + } else { + roiTarget = new Coord2D(target.getLatitude(), target.getLongitude()); + } + paramsMap.put(key, roiTarget); } - paramsMap.put(key, roiTarget); - } + } else + paramsMap.put(key, data.get(key)); } - else - paramsMap.put(key, data.get(key)); - } - followAlgorithm.updateAlgorithmParams(paramsMap); + followAlgorithm.updateAlgorithmParams(paramsMap); + } } break; @@ -1037,6 +1118,15 @@ public void performAsyncAction(Action action) throws RemoteException { performAction(action); } + private void notifyAttributeUpdate(List> attributesInfo) { + if (observersList.isEmpty() || attributesInfo == null || attributesInfo.isEmpty()) + return; + + for (Pair info : attributesInfo) { + notifyAttributeUpdate(info.first, info.second); + } + } + private void notifyAttributeUpdate(String attributeEvent, Bundle extrasBundle) { if (observersList.isEmpty()) return; @@ -1181,20 +1271,30 @@ private static FollowType followModeToType(FollowAlgorithm.FollowModes followMod } public void disableFollowMe() { + if (droneMgr == null) + return; + Follow follow = this.droneMgr.getFollowMe(); if (follow.isEnabled()) follow.toggleFollowMeState(); } public void triggerCamera() throws RemoteException { - MavLinkROI.triggerCamera(this.droneMgr.getDrone()); + if (droneMgr == null) + return; + MavLinkDoCmds.triggerCamera(this.droneMgr.getDrone()); } public void epmCommand(boolean release) { - MavLinkROI.empCommand(this.droneMgr.getDrone(), release); + if (droneMgr == null) + return; + + MavLinkDoCmds.empCommand(this.droneMgr.getDrone(), release); } public void loadWaypoints() { + if (droneMgr == null) + return; this.droneMgr.getDrone().getWaypointManager().getWaypoints(); } @@ -1202,6 +1302,7 @@ public void loadWaypoints() { public void onDroneEvent(DroneInterfaces.DroneEventsType event, Drone drone) { Bundle extrasBundle = null; String droneEvent = null; + final List> attributesInfo = new ArrayList<>(); switch (event) { case DISCONNECTED: @@ -1295,8 +1396,7 @@ public void onDroneEvent(DroneInterfaces.DroneEventsType event, Drone drone) { break; case CALIBRATION_IMU: - final String calIMUMessage = this.droneMgr.getDrone().getCalibrationSetup() - .getMessage(); + final String calIMUMessage = drone.getCalibrationSetup().getMessage(); extrasBundle = new Bundle(1); extrasBundle.putString(AttributeEventExtra.EXTRA_CALIBRATION_IMU_MESSAGE, calIMUMessage); droneEvent = AttributeEvent.CALIBRATION_IMU; @@ -1310,7 +1410,7 @@ public void onDroneEvent(DroneInterfaces.DroneEventsType event, Drone drone) { * flag and re-trigger the HEARBEAT_TIMEOUT this however should * not be happening */ - final Calibration calibration = this.droneMgr.getDrone().getCalibrationSetup(); + final Calibration calibration = drone.getCalibrationSetup(); final String message = calibration.getMessage(); if (calibration.isCalibrating() && TextUtils.isEmpty(message)) { calibration.setCalibrating(false); @@ -1320,6 +1420,7 @@ public void onDroneEvent(DroneInterfaces.DroneEventsType event, Drone drone) { extrasBundle.putString(AttributeEventExtra.EXTRA_CALIBRATION_IMU_MESSAGE, message); droneEvent = AttributeEvent.CALIBRATION_IMU_TIMEOUT; } + break; case HEARTBEAT_TIMEOUT: @@ -1347,20 +1448,20 @@ public void onDroneEvent(DroneInterfaces.DroneEventsType event, Drone drone) { break; case HEARTBEAT_FIRST: + final Bundle heartBeatExtras = new Bundle(1); + heartBeatExtras.putInt(AttributeEventExtra.EXTRA_MAVLINK_VERSION, drone.getMavlinkVersion()); + attributesInfo.add(Pair.create(AttributeEvent.HEARTBEAT_FIRST, heartBeatExtras)); + + case CONNECTED: //Broadcast the vehicle connection. - final ConnectionParameter droneParams = droneMgr.getConnectionParameter(); - final ConnectionParameter sanitizedParameter = new ConnectionParameter(droneParams.getConnectionType(), - droneParams.getParamsBundle(), null); + final ConnectionParameter sanitizedParameter = new ConnectionParameter(connectionParams + .getConnectionType(), connectionParams.getParamsBundle(), null); context.sendBroadcast(new Intent(GCSEvent.ACTION_VEHICLE_CONNECTION) .putExtra(GCSEvent.EXTRA_APP_ID, ownerId) .putExtra(GCSEvent.EXTRA_VEHICLE_CONNECTION_PARAMETER, sanitizedParameter)); - notifyAttributeUpdate(AttributeEvent.STATE_CONNECTED, null); - - extrasBundle = new Bundle(1); - extrasBundle.putInt(AttributeEventExtra.EXTRA_MAVLINK_VERSION, drone.getMavlinkVersion()); - droneEvent = AttributeEvent.HEARTBEAT_FIRST; + attributesInfo.add(Pair.create(AttributeEvent.STATE_CONNECTED, null)); break; case HEARTBEAT_RESTORED: @@ -1377,11 +1478,10 @@ public void onDroneEvent(DroneInterfaces.DroneEventsType event, Drone drone) { break; case MISSION_WP_UPDATE: - final int currentWaypoint = this.droneMgr.getDrone().getMissionStats() - .getCurrentWP(); - extrasBundle = new Bundle(1); - extrasBundle.putInt(AttributeEventExtra.EXTRA_MISSION_CURRENT_WAYPOINT, currentWaypoint); - droneEvent = AttributeEvent.MISSION_ITEM_UPDATED; + final int currentWaypoint = drone.getMissionStats().getCurrentWP(); + extrasBundle = new Bundle(1); + extrasBundle.putInt(AttributeEventExtra.EXTRA_MISSION_CURRENT_WAYPOINT, currentWaypoint); + droneEvent = AttributeEvent.MISSION_ITEM_UPDATED; break; case FOLLOW_START: @@ -1417,7 +1517,13 @@ public void onDroneEvent(DroneInterfaces.DroneEventsType event, Drone drone) { break; } - notifyAttributeUpdate(droneEvent, extrasBundle); + if (droneEvent != null) { + notifyAttributeUpdate(droneEvent, extrasBundle); + } + + if (!attributesInfo.isEmpty()) { + notifyAttributeUpdate(attributesInfo); + } } @Override @@ -1434,7 +1540,7 @@ public void onParameterReceived(Parameter parameter, int index, int count) { } @Override - public void onEndReceivingParameters(List parameter) { + public void onEndReceivingParameters() { notifyAttributeUpdate(AttributeEvent.PARAMETERS_REFRESH_ENDED, null); } @@ -1483,6 +1589,14 @@ public void finished(FitPoints fit, double[] offsets) { notifyAttributeUpdate(AttributeEvent.CALIBRATION_MAG_COMPLETED, paramsBundle); } + @Override + public DroneSharePrefs getDroneSharePrefs() { + if (connectionParams == null) + return null; + + return connectionParams.getDroneSharePrefs(); + } + @Override public void onConnectionFailed(String error) { notifyConnectionFailed(new ConnectionResult(0, error)); diff --git a/ServiceApp/src/org/droidplanner/services/android/api/MavLinkServiceApi.java b/ServiceApp/src/org/droidplanner/services/android/api/MavLinkServiceApi.java index 28650c93bb..f2a64e4eb1 100644 --- a/ServiceApp/src/org/droidplanner/services/android/api/MavLinkServiceApi.java +++ b/ServiceApp/src/org/droidplanner/services/android/api/MavLinkServiceApi.java @@ -29,19 +29,20 @@ private DroidPlannerService getService() { return service; } - public void sendData(ConnectionParameter connParams, MAVLinkPacket packet) { - final AndroidMavLinkConnection mavConnection = getService().mavConnections.get - (connParams); - if (mavConnection == null) return; + public boolean sendData(ConnectionParameter connParams, MAVLinkPacket packet) { + final AndroidMavLinkConnection mavConnection = getService().mavConnections.get(connParams); + if (mavConnection == null) return false; if (mavConnection.getConnectionStatus() != MavLinkConnection.MAVLINK_DISCONNECTED) { mavConnection.sendMavPacket(packet); + return true; } + + return false; } public int getConnectionStatus(ConnectionParameter connParams, String tag) { - final AndroidMavLinkConnection mavConnection = getService().mavConnections.get - (connParams); + final AndroidMavLinkConnection mavConnection = getService().mavConnections.get(connParams); if (mavConnection == null || !mavConnection.hasMavLinkConnectionListener(tag)) { return MavLinkConnection.MAVLINK_DISCONNECTED; } @@ -54,6 +55,14 @@ public void connectMavLink(ConnectionParameter connParams, String tag, getService().connectMAVConnection(connParams, tag, listener); } + public void addLoggingFile(ConnectionParameter connParams, String tag, String loggingFilePath){ + getService().addLoggingFile(connParams, tag, loggingFilePath); + } + + public void removeLoggingFile(ConnectionParameter connParams, String tag){ + getService().removeLoggingFile(connParams, tag); + } + public void disconnectMavLink(ConnectionParameter connParams, String tag) { getService().disconnectMAVConnection(connParams, tag); } diff --git a/ServiceApp/src/org/droidplanner/services/android/communication/connection/usb/UsbCDCConnection.java b/ServiceApp/src/org/droidplanner/services/android/communication/connection/usb/UsbCDCConnection.java index 8e01650cdb..c5ac0c4e73 100644 --- a/ServiceApp/src/org/droidplanner/services/android/communication/connection/usb/UsbCDCConnection.java +++ b/ServiceApp/src/org/droidplanner/services/android/communication/connection/usb/UsbCDCConnection.java @@ -17,6 +17,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; class UsbCDCConnection extends UsbConnection.UsbConnectionImpl { private static final String TAG = UsbCDCConnection.class.getSimpleName(); @@ -24,7 +25,7 @@ class UsbCDCConnection extends UsbConnection.UsbConnectionImpl { private static final IntentFilter intentFilter = new IntentFilter(ACTION_USB_PERMISSION); - private static UsbSerialDriver sDriver = null; + private final AtomicReference serialDriverRef = new AtomicReference<>(); private final PendingIntent usbPermissionIntent; @@ -97,7 +98,7 @@ protected void openUsbConnection() throws IOException { //Get the list of available devices List availableDevices = UsbSerialProber.getAvailableSupportedDevices(manager); if (availableDevices.isEmpty()) { - Log.d("USB", "No Devices found"); + Log.d(TAG, "No Devices found"); throw new IOException("No Devices found"); } @@ -120,39 +121,42 @@ private void openUsbDevice(UsbDevice device) throws IOException { UsbManager manager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE); // Find the first available driver. - sDriver = UsbSerialProber.openUsbDevice(manager, device); + final UsbSerialDriver serialDriver = UsbSerialProber.openUsbDevice(manager, device); - if (sDriver == null) { - Log.d("USB", "No Devices found"); + if (serialDriver == null) { + Log.d(TAG, "No Devices found"); throw new IOException("No Devices found"); } else { - Log.d("USB", "Opening using Baud rate " + mBaudRate); + Log.d(TAG, "Opening using Baud rate " + mBaudRate); try { - sDriver.open(); - sDriver.setParameters(mBaudRate, 8, UsbSerialDriver.STOPBITS_1, UsbSerialDriver.PARITY_NONE); + serialDriver.open(); + serialDriver.setParameters(mBaudRate, 8, UsbSerialDriver.STOPBITS_1, UsbSerialDriver.PARITY_NONE); + + serialDriverRef.set(serialDriver); onUsbConnectionOpened(); } catch (IOException e) { - Log.e("USB", "Error setting up device: " + e.getMessage(), e); + Log.e(TAG, "Error setting up device: " + e.getMessage(), e); try { - sDriver.close(); + serialDriver.close(); } catch (IOException e2) { // Ignore. } - sDriver = null; } } } @Override protected int readDataBlock(byte[] readData) throws IOException { - // Read data from driver. This call will return upto readData.length - // bytes. - // If no data is received it will timeout after 200ms (as set by - // parameter 2) + // Read data from driver. This call will return up to readData.length bytes. + // If no data is received it will timeout after 200ms (as set by parameter 2) + final UsbSerialDriver serialDriver = serialDriverRef.get(); + if(serialDriver == null) + throw new IOException("Device is unavailable."); + int iavailable = 0; try { - iavailable = sDriver.read(readData, 200); + iavailable = serialDriver.read(readData, 200); } catch (NullPointerException e) { final String errorMsg = "Error Reading: " + e.getMessage() + "\nAssuming inaccessible USB device. Closing connection."; @@ -170,11 +174,12 @@ protected void sendBuffer(byte[] buffer) { // Write data to driver. This call should write buffer.length bytes // if data cant be sent , then it will timeout in 500ms (as set by // parameter 2) - if (sDriver != null) { + final UsbSerialDriver serialDriver = serialDriverRef.get(); + if (serialDriver != null) { try { - sDriver.write(buffer, 500); + serialDriver.write(buffer, 500); } catch (IOException e) { - Log.e("USB", "Error Sending: " + e.getMessage(), e); + Log.e(TAG, "Error Sending: " + e.getMessage(), e); } } } @@ -183,13 +188,13 @@ protected void sendBuffer(byte[] buffer) { protected void closeUsbConnection() throws IOException { unregisterUsbPermissionBroadcastReceiver(); - if (sDriver != null) { + final UsbSerialDriver serialDriver = serialDriverRef.getAndSet(null); + if (serialDriver != null) { try { - sDriver.close(); + serialDriver.close(); } catch (IOException e) { - // Ignore. + Log.e(TAG, e.getMessage(), e); } - sDriver = null; } } diff --git a/ServiceApp/src/org/droidplanner/services/android/communication/connection/usb/UsbFTDIConnection.java b/ServiceApp/src/org/droidplanner/services/android/communication/connection/usb/UsbFTDIConnection.java index 155c6f8d71..ae628d6e7b 100644 --- a/ServiceApp/src/org/droidplanner/services/android/communication/connection/usb/UsbFTDIConnection.java +++ b/ServiceApp/src/org/droidplanner/services/android/communication/connection/usb/UsbFTDIConnection.java @@ -1,6 +1,7 @@ package org.droidplanner.services.android.communication.connection.usb; import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; import android.content.Context; import android.util.Log; @@ -14,7 +15,7 @@ class UsbFTDIConnection extends UsbConnection.UsbConnectionImpl { private static final byte LATENCY_TIMER = 32; - private FT_Device ftDev; + private final AtomicReference ftDevRef = new AtomicReference<>(); protected UsbFTDIConnection(Context context, UsbConnection parentConn, int baudRate) { super(context, parentConn, baudRate); @@ -39,6 +40,7 @@ protected void openUsbConnection() throws IOException { throw new IOException("No Devices found"); } + FT_Device ftDev = null; try { // FIXME: The NPE is coming from the library. Investigate if it's // possible to fix there. @@ -51,7 +53,7 @@ protected void openUsbConnection() throws IOException { } } - Log.d("USB", "Opening using Baud rate " + mBaudRate); + Log.d(TAG, "Opening using Baud rate " + mBaudRate); ftDev.setBitMode((byte) 0, D2xxManager.FT_BITMODE_RESET); ftDev.setBaudRate(mBaudRate); ftDev.setDataCharacteristics(D2xxManager.FT_DATA_BITS_8, D2xxManager.FT_STOP_BITS_1, @@ -61,16 +63,19 @@ protected void openUsbConnection() throws IOException { ftDev.purge((byte) (D2xxManager.FT_PURGE_TX | D2xxManager.FT_PURGE_RX)); if (!ftDev.isOpen()) { - throw new IOException(); + throw new IOException("Unable to open usb device connection."); } else { - Log.d("USB", "COM open"); + Log.d(TAG, "COM open"); } + ftDevRef.set(ftDev); + onUsbConnectionOpened(); } @Override protected int readDataBlock(byte[] readData) throws IOException { + final FT_Device ftDev = ftDevRef.get(); if (ftDev == null || !ftDev.isOpen()) { throw new IOException("Device is unavailable."); } @@ -98,24 +103,25 @@ protected int readDataBlock(byte[] readData) throws IOException { @Override protected void sendBuffer(byte[] buffer) { - if (ftDev != null) { + final FT_Device ftDev = ftDevRef.get(); + if (ftDev != null && ftDev.isOpen()) { try { ftDev.write(buffer); } catch (Exception e) { - Log.e("USB", "Error Sending: " + e.getMessage(), e); + Log.e(TAG, "Error Sending: " + e.getMessage(), e); } } } @Override protected void closeUsbConnection() throws IOException { + final FT_Device ftDev = ftDevRef.getAndSet(null); if (ftDev != null) { try { ftDev.close(); } catch (Exception e) { - // Ignore. + Log.e(TAG, e.getMessage(), e); } - ftDev = null; } } diff --git a/ServiceApp/src/org/droidplanner/services/android/communication/service/MAVLinkClient.java b/ServiceApp/src/org/droidplanner/services/android/communication/service/MAVLinkClient.java index ac83b12f55..1b7d905c97 100644 --- a/ServiceApp/src/org/droidplanner/services/android/communication/service/MAVLinkClient.java +++ b/ServiceApp/src/org/droidplanner/services/android/communication/service/MAVLinkClient.java @@ -1,7 +1,6 @@ package org.droidplanner.services.android.communication.service; import android.content.Context; -import android.util.Log; import com.MAVLink.MAVLinkPacket; import com.o3dr.services.android.lib.drone.connection.ConnectionParameter; @@ -12,16 +11,11 @@ import org.droidplanner.core.MAVLink.connection.MavLinkConnectionTypes; import org.droidplanner.services.android.api.MavLinkServiceApi; import org.droidplanner.services.android.data.SessionDB; +import org.droidplanner.services.android.utils.file.DirectoryPath; import org.droidplanner.services.android.utils.file.FileUtils; -import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.util.Date; -import java.util.concurrent.LinkedBlockingQueue; /** * Provide a common class for some ease of use functionality @@ -30,6 +24,8 @@ public class MAVLinkClient implements MAVLinkStreams.MAVLinkOutputStream { private final static String TAG = MAVLinkClient.class.getSimpleName(); + private static final String TLOG_PREFIX = "log"; + /** * Maximum possible sequence number for a packet. */ @@ -38,7 +34,7 @@ public class MAVLinkClient implements MAVLinkStreams.MAVLinkOutputStream { private final MavLinkConnectionListener mConnectionListener = new MavLinkConnectionListener() { @Override - public void onStartingConnection(){ + public void onStartingConnection() { listener.notifyStartingConnection(); } @@ -51,7 +47,6 @@ public void onConnect(long connectionTime) { @Override public void onReceivePacket(final MAVLinkPacket packet) { listener.notifyReceivedData(packet); - queueToLog(packet.encodePacket()); } @Override @@ -68,104 +63,37 @@ public void onComError(final String errMsg) { } }; - private static final String TLOG_PREFIX = "log"; - private static final String TEMP_TLOG_EXT = ".tmp"; - - /** - * Queue the set of packets to log. A thread will be blocking on it until - * there's element(s) available for logging. - */ - private final LinkedBlockingQueue mPacketsToLog = new LinkedBlockingQueue<>(); - - /** - * Blocks until there's packets to log, then dispatch them. - */ - private final Runnable mLoggingTask = new Runnable() { - @Override - public void run() { - final File tmpLogFile = getTempTLogFile(connectionTime); - final ByteBuffer logBuffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); - logBuffer.order(ByteOrder.BIG_ENDIAN); - - try { - final BufferedOutputStream logWriter = new BufferedOutputStream(new FileOutputStream(tmpLogFile)); - try { - while (isConnected()) { - final byte[] packetData = mPacketsToLog.take(); - - logBuffer.clear(); - logBuffer.putLong(System.currentTimeMillis() * 1000); - - logWriter.write(logBuffer.array()); - logWriter.write(packetData); - } - } catch (InterruptedException e) { - final String errorMessage = e.getMessage(); - if(errorMessage != null) - Log.v(TAG, errorMessage); - } finally { - logWriter.close(); - - //Rename the file. - tmpLogFile.renameTo(getCompleteTLogFile(tmpLogFile)); - } - } catch (IOException e) { - final String errorMessage = e.getMessage(); - if(errorMessage != null) - Log.e(TAG, errorMessage, e); - } - } - }; - - private final File loggingDir; - - private Thread loggingThread; - private long connectionTime; - private int packetSeqNumber = 0; - - private ConnectionParameter connParams; private final MAVLinkStreams.MavlinkInputStream listener; - private final MavLinkServiceApi mavLinkApi; private final SessionDB sessionDB; + private final Context context; - public MAVLinkClient(Context context, MAVLinkStreams.MavlinkInputStream listener, MavLinkServiceApi serviceApi, - File logDir) { + private int packetSeqNumber = 0; + private final ConnectionParameter connParams; + + public MAVLinkClient(Context context, MAVLinkStreams.MavlinkInputStream listener, + ConnectionParameter connParams, MavLinkServiceApi serviceApi) { + this.context = context; this.listener = listener; this.mavLinkApi = serviceApi; - this.loggingDir = logDir; - this.sessionDB = new SessionDB(context); - } - - public ConnectionParameter getConnectionParameter() { - return connParams; - } - - public void setConnectionParameter(ConnectionParameter connParams) { - boolean isConnected = isConnected(); - if(isConnected) - closeConnection(); - this.connParams = connParams; - - if(isConnected) - openConnection(); + this.sessionDB = new SessionDB(context); } @Override public void openConnection() { - if(this.connParams == null) + if (this.connParams == null) return; final String tag = toString(); - if(mavLinkApi.getConnectionStatus(this.connParams, tag) == MavLinkConnection.MAVLINK_DISCONNECTED) { + if (mavLinkApi.getConnectionStatus(this.connParams, tag) == MavLinkConnection.MAVLINK_DISCONNECTED) { mavLinkApi.connectMavLink(this.connParams, tag, mConnectionListener); } } @Override public void closeConnection() { - if(this.connParams == null) + if (this.connParams == null) return; final String tag = toString(); @@ -178,15 +106,15 @@ public void closeConnection() { @Override public void sendMavPacket(MAVLinkPacket pack) { - if (!isConnected()) { + if (this.connParams == null) { return; } pack.seq = packetSeqNumber; - mavLinkApi.sendData(this.connParams, pack); - queueToLog(pack.encodePacket()); - packetSeqNumber = (packetSeqNumber + 1) % (MAX_PACKET_SEQUENCE + 1); + if(mavLinkApi.sendData(this.connParams, pack)) { + packetSeqNumber = (packetSeqNumber + 1) % (MAX_PACKET_SEQUENCE + 1); + } } @Override @@ -195,6 +123,11 @@ public boolean isConnected() { && mavLinkApi.getConnectionStatus(this.connParams, toString()) == MavLinkConnection.MAVLINK_CONNECTED; } + public boolean isConnecting(){ + return this.connParams != null && mavLinkApi.getConnectionStatus(this.connParams, + toString()) == MavLinkConnection.MAVLINK_CONNECTING; + } + @Override public void toggleConnectionState() { if (isConnected()) { @@ -204,48 +137,41 @@ public void toggleConnectionState() { } } - private File getTempTLogFile(long connectionTimestamp) { - return new File(loggingDir, getTLogFilename(connectionTimestamp)); + private File getTLogDir(String appId) { + return DirectoryPath.getTLogPath(this.context, appId); } - private File getCompleteTLogFile(File tempFile){ - return new File(tempFile.getParentFile(), tempFile.getName().replace(TEMP_TLOG_EXT, "")); + private File getTempTLogFile(String appId, long connectionTimestamp) { + return new File(getTLogDir(appId), getTLogFilename(connectionTimestamp)); } private String getTLogFilename(long connectionTimestamp) { - return TLOG_PREFIX + "_" + MavLinkConnectionTypes.getConnectionTypeLabel(connParams.getConnectionType()) + - "_" + FileUtils.getTimeStamp(connectionTimestamp) + FileUtils.TLOG_FILENAME_EXT + TEMP_TLOG_EXT; + return TLOG_PREFIX + "_" + MavLinkConnectionTypes.getConnectionTypeLabel(this.connParams.getConnectionType()) + + "_" + FileUtils.getTimeStamp(connectionTimestamp) + FileUtils.TLOG_FILENAME_EXT; + } + + public void addLoggingFile(String appId){ + if(isConnecting() || isConnected()) { + final File logFile = getTempTLogFile(appId, System.currentTimeMillis()); + mavLinkApi.addLoggingFile(this.connParams, appId, logFile.getAbsolutePath()); + } } - private void startLoggingThread(long startTime){ - this.connectionTime = startTime; + public void removeLoggingFile(String appId){ + if(isConnecting() || isConnected()){ + mavLinkApi.removeLoggingFile(this.connParams, appId); + } + } + private void startLoggingThread(long startTime) { //log into the database the connection time. final String connectionType = MavLinkConnectionTypes.getConnectionTypeLabel(connParams.getConnectionType()); - this.sessionDB.startSession(new Date(connectionTime), connectionType); - - loggingThread = new Thread(mLoggingTask, "MavLinkClient-Logging Thread"); - loggingThread.start(); + this.sessionDB.startSession(new Date(startTime), connectionType); } - private void stopLoggingThread(long stopTime){ + private void stopLoggingThread(long stopTime) { //log into the database the disconnection time. final String connectionType = MavLinkConnectionTypes.getConnectionTypeLabel(connParams.getConnectionType()); this.sessionDB.endSession(new Date(stopTime), connectionType, new Date()); - - if (loggingThread != null && loggingThread.isAlive()) { - loggingThread.interrupt(); - loggingThread = null; - } - } - - /** - * Queue a mavlink packet for logging. - * - * @param packetData MAVLinkPacket packet data - * @return true if the packet was queued successfully. - */ - private boolean queueToLog(byte[] packetData) { - return mPacketsToLog.offer(packetData); } } diff --git a/ServiceApp/src/org/droidplanner/services/android/drone/DroneManager.java b/ServiceApp/src/org/droidplanner/services/android/drone/DroneManager.java index 75f08cacea..e116cdd410 100644 --- a/ServiceApp/src/org/droidplanner/services/android/drone/DroneManager.java +++ b/ServiceApp/src/org/droidplanner/services/android/drone/DroneManager.java @@ -3,6 +3,7 @@ import android.content.Context; import android.os.Handler; import android.os.SystemClock; +import android.text.TextUtils; import android.util.Log; import com.MAVLink.MAVLinkPacket; @@ -27,18 +28,18 @@ import org.droidplanner.services.android.location.FusedLocation; import org.droidplanner.services.android.utils.AndroidApWarningParser; import org.droidplanner.services.android.utils.analytics.GAUtils; -import org.droidplanner.services.android.utils.file.DirectoryPath; import org.droidplanner.services.android.utils.file.IO.CameraInfoLoader; import org.droidplanner.services.android.utils.prefs.DroidPlannerPrefs; -import java.io.File; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import ellipsoidFit.FitPoints; import ellipsoidFit.ThreeSpacePoint; /** - * Created by fhuya on 11/1/14. + * Bridge between the communication channel, the drone instance(s), and the connected client(s). */ public class DroneManager implements MAVLinkStreams.MavlinkInputStream, MagnetometerCalibration.OnMagCalibrationListener, DroneInterfaces.OnDroneListener, @@ -46,25 +47,23 @@ public class DroneManager implements MAVLinkStreams.MavlinkInputStream, private static final String TAG = DroneManager.class.getSimpleName(); - private DroneEventsListener droneEventsListener; + private final ConcurrentHashMap connectedApps = new ConcurrentHashMap<>(); + private final ConcurrentHashMap tlogUploaders = new ConcurrentHashMap<>(); private final Context context; - private final String appId; private final Drone drone; private final Follow followMe; private final CameraInfoLoader cameraInfoLoader; private final MavLinkMsgHandler mavLinkMsgHandler; private MagnetometerCalibration magCalibration; + private final ConnectionParameter connectionParameter; - private DroneshareClient uploader; - private ConnectionParameter connectionParams; - - public DroneManager(Context context, String appId, final Handler handler, MavLinkServiceApi mavlinkApi) { - this.appId = appId; + public DroneManager(Context context, ConnectionParameter connParams, final Handler handler, MavLinkServiceApi mavlinkApi) { this.context = context; + this.connectionParameter = connParams; this.cameraInfoLoader = new CameraInfoLoader(context); - MAVLinkClient mavClient = new MAVLinkClient(context, this, mavlinkApi, getTLogDir()); + MAVLinkClient mavClient = new MAVLinkClient(context, this, connParams, mavlinkApi); DroneInterfaces.Clock clock = new DroneInterfaces.Clock() { @Override @@ -105,23 +104,16 @@ public void postDelayed(Runnable thread, long timeout) { drone.getParameters().setParameterListener(this); } - private File getTLogDir() { - return DirectoryPath.getTLogPath(this.context, this.appId); - } - public void destroy() { Log.d(TAG, "Destroying drone manager."); drone.removeDroneListener(this); drone.getParameters().setParameterListener(null); - try { - disconnect(); - } catch (ConnectionException e) { - Log.e(TAG, e.getMessage(), e); - } + disconnect(); - droneEventsListener = null; + connectedApps.clear(); + tlogUploaders.clear(); if (magCalibration.isRunning()) magCalibration.stop(); @@ -130,40 +122,57 @@ public void destroy() { followMe.toggleFollowMeState(); } - public void setDroneEventsListener(DroneEventsListener listener) { - if (droneEventsListener != null && listener == null) { - droneEventsListener.onDroneEvent(DroneInterfaces.DroneEventsType.DISCONNECTED, drone); - } + public void connect(String appId, DroneEventsListener listener) throws ConnectionException { + if (listener == null || TextUtils.isEmpty(appId)) + return; - droneEventsListener = listener; + connectedApps.put(appId, listener); - if (listener != null) { - if (isConnected()) { - listener.onDroneEvent(DroneInterfaces.DroneEventsType.CONNECTED, drone); - } else { - listener.onDroneEvent(DroneInterfaces.DroneEventsType.DISCONNECTED, drone); - } - } - } - - public void connect() throws ConnectionException { MAVLinkClient mavClient = (MAVLinkClient) drone.getMavClient(); + if (!mavClient.isConnected()) { mavClient.openConnection(); } else { - onDroneEvent(DroneInterfaces.DroneEventsType.CONNECTED, drone); + listener.onDroneEvent(DroneInterfaces.DroneEventsType.CONNECTED, drone); + notifyConnected(appId, listener); } + + mavClient.addLoggingFile(appId); } - public void disconnect() throws ConnectionException { - if(followMe.isEnabled()) - followMe.toggleFollowMeState(); + private void disconnect() { + if (!connectedApps.isEmpty()) { + for (String appId : connectedApps.keySet()) { + try { + disconnect(appId); + } catch (ConnectionException e) { + Log.e(TAG, e.getMessage(), e); + } + } + } + } - MAVLinkClient mavClient = (MAVLinkClient) drone.getMavClient(); - if (mavClient.isConnected()) - mavClient.closeConnection(); - else - onDroneEvent(DroneInterfaces.DroneEventsType.DISCONNECTED, drone); + public int getConnectedAppsCount(){ + return connectedApps.size(); + } + + public void disconnect(String appId) throws ConnectionException { + if (TextUtils.isEmpty(appId)) + return; + + DroneEventsListener listener = connectedApps.remove(appId); + + if (listener != null) { + MAVLinkClient mavClient = (MAVLinkClient) drone.getMavClient(); + mavClient.removeLoggingFile(appId); + + if (mavClient.isConnected() && connectedApps.isEmpty()) { + mavClient.closeConnection(); + } + + listener.onDroneEvent(DroneInterfaces.DroneEventsType.DISCONNECTED, drone); + notifyDisconnected(appId, listener); + } } @Override @@ -171,47 +180,72 @@ public void notifyStartingConnection() { onDroneEvent(DroneInterfaces.DroneEventsType.CONNECTING, drone); } + private void notifyConnected(String appId, DroneEventsListener listener) { + if (TextUtils.isEmpty(appId) || listener == null) + return; + + final DroneSharePrefs droneSharePrefs = listener.getDroneSharePrefs(); + + //TODO: restore live upload functionality when issue + // 'https://github.com/diydrones/droneapi-java/issues/2' is fixed. + boolean isLiveUploadEnabled = false; //droneSharePrefs.isLiveUploadEnabled(); + if (droneSharePrefs != null && isLiveUploadEnabled && droneSharePrefs.areLoginCredentialsSet()) { + + Log.i(TAG, "Starting live upload for " + appId); + try { + DroneshareClient uploader = tlogUploaders.get(appId); + if (uploader == null) { + uploader = new DroneshareClient(); + tlogUploaders.put(appId, uploader); + } + + uploader.connect(droneSharePrefs.getUsername(), droneSharePrefs.getPassword()); + } catch (Exception e) { + Log.e(TAG, "DroneShare uploader error for " + appId, e); + } + } else { + Log.i(TAG, "Skipping live upload for " + appId); + } + } + @Override public void notifyConnected() { - if (this.connectionParams != null) { - final DroneSharePrefs droneSharePrefs = connectionParams.getDroneSharePrefs(); - - // Start a new ga analytics session. The new session will be tagged - // with the mavlink connection mechanism, as well as whether the user has an active droneshare account. - GAUtils.startNewSession(droneSharePrefs); - - //TODO: restore live upload functionality when issue - // 'https://github.com/diydrones/droneapi-java/issues/2' is fixed. - boolean isLiveUploadEnabled = false; //droneSharePrefs.isLiveUploadEnabled(); - if (droneSharePrefs != null && isLiveUploadEnabled && droneSharePrefs.areLoginCredentialsSet()) { - Log.i(TAG, "Starting live upload"); - try { - if (uploader == null) - uploader = new DroneshareClient(); + // Start a new ga analytics session. The new session will be tagged + // with the mavlink connection mechanism, as well as whether the user has an active droneshare account. + GAUtils.startNewSession(null); - uploader.connect(droneSharePrefs.getUsername(), droneSharePrefs.getPassword()); - } catch (Exception e) { - Log.e(TAG, "DroneShare uploader error.", e); - } - } else { - Log.i(TAG, "Skipping live upload"); + if (!connectedApps.isEmpty()) { + for (Map.Entry entry : connectedApps.entrySet()) { + notifyConnected(entry.getKey(), entry.getValue()); } } this.drone.notifyDroneEvent(DroneInterfaces.DroneEventsType.CHECKING_VEHICLE_LINK); } - public void kickStartDroneShareUpload(){ - if (this.connectionParams != null) { - // See if we can at least do a delayed upload - UploaderService.kickStart(context, this.appId, this.connectionParams.getDroneSharePrefs()); + public void kickStartDroneShareUpload() { + // See if we can at least do a delayed upload + if (!connectedApps.isEmpty()) { + for (Map.Entry entry : connectedApps.entrySet()) { + kickStartDroneShareUpload(entry.getKey(), entry.getValue().getDroneSharePrefs()); + } } } - @Override - public void notifyDisconnected() { - kickStartDroneShareUpload(); + private void kickStartDroneShareUpload(String appId, DroneSharePrefs prefs) { + if (TextUtils.isEmpty(appId) || prefs == null) + return; + + UploaderService.kickStart(context, appId, prefs); + } + + private void notifyDisconnected(String appId, DroneEventsListener listener) { + if (TextUtils.isEmpty(appId) || listener == null) + return; + kickStartDroneShareUpload(appId, listener.getDroneSharePrefs()); + + DroneshareClient uploader = tlogUploaders.remove(appId); if (uploader != null) { try { uploader.close(); @@ -219,6 +253,15 @@ public void notifyDisconnected() { Log.e(TAG, "Error while closing the drone share upload handler.", e); } } + } + + @Override + public void notifyDisconnected() { + if (!connectedApps.isEmpty()) { + for (Map.Entry entry : connectedApps.entrySet()) { + notifyDisconnected(entry.getKey(), entry.getValue()); + } + } this.drone.notifyDroneEvent(DroneInterfaces.DroneEventsType.DISCONNECTED); } @@ -241,35 +284,34 @@ public void notifyReceivedData(MAVLinkPacket packet) { MAVLinkMessage receivedMsg = packet.unpack(); this.mavLinkMsgHandler.receiveData(receivedMsg); - if (droneEventsListener != null) { - droneEventsListener.onReceivedMavLinkMessage(receivedMsg); + if (!connectedApps.isEmpty()) { + for (DroneEventsListener droneEventsListener : connectedApps.values()) { + droneEventsListener.onReceivedMavLinkMessage(receivedMsg); + } } - if (uploader != null) { - try { - uploader.filterMavlink(uploader.interfaceNum, packet.encodePacket()); - } catch (Exception e) { - Log.e(TAG, e.getMessage(), e); + if (!tlogUploaders.isEmpty()) { + final byte[] packetData = packet.encodePacket(); + for (DroneshareClient uploader : tlogUploaders.values()) { + try { + uploader.filterMavlink(uploader.interfaceNum, packetData); + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } } } } @Override public void onStreamError(String errorMsg) { - if (droneEventsListener != null) { + if (connectedApps.isEmpty()) + return; + + for (DroneEventsListener droneEventsListener : connectedApps.values()) { droneEventsListener.onConnectionFailed(errorMsg); } } - public ConnectionParameter getConnectionParameter() { - return this.connectionParams; - } - - public void setConnectionParameter(ConnectionParameter connParams) { - this.connectionParams = connParams; - ((MAVLinkClient) drone.getMavClient()).setConnectionParameter(connParams); - } - public Drone getDrone() { return this.drone; } @@ -288,56 +330,81 @@ public CameraInfoLoader getCameraInfoLoader() { @Override public void onDroneEvent(DroneInterfaces.DroneEventsType event, Drone drone) { - if (droneEventsListener != null) { + if (connectedApps.isEmpty()) + return; + + for (DroneEventsListener droneEventsListener : connectedApps.values()) { droneEventsListener.onDroneEvent(event, drone); } } @Override public void onBeginReceivingParameters() { - if (droneEventsListener != null) { + if (connectedApps.isEmpty()) + return; + + for (DroneEventsListener droneEventsListener : connectedApps.values()) { droneEventsListener.onBeginReceivingParameters(); } } @Override public void onParameterReceived(Parameter parameter, int index, int count) { - if (droneEventsListener != null) { + if (connectedApps.isEmpty()) + return; + + for (DroneEventsListener droneEventsListener : connectedApps.values()) { droneEventsListener.onParameterReceived(parameter, index, count); } } @Override - public void onEndReceivingParameters(List parameter) { - if (droneEventsListener != null) { - droneEventsListener.onEndReceivingParameters(parameter); + public void onEndReceivingParameters() { + if (connectedApps.isEmpty()) + return; + + for (DroneEventsListener droneEventsListener : connectedApps.values()) { + droneEventsListener.onEndReceivingParameters(); } } @Override public void onStarted(List points) { - if (droneEventsListener != null) { + if (connectedApps.isEmpty()) + return; + + for (DroneEventsListener droneEventsListener : connectedApps.values()) { droneEventsListener.onStarted(points); } } @Override public void newEstimation(FitPoints fit, List points) { - if (droneEventsListener != null) { + if (connectedApps.isEmpty()) + return; + + for (DroneEventsListener droneEventsListener : connectedApps.values()) { droneEventsListener.newEstimation(fit, points); } } @Override public void finished(FitPoints fit, double[] offsets) { + if (connectedApps.isEmpty()) + return; + try { offsets = magCalibration.sendOffsets(); } catch (Exception e) { Log.e(TAG, e.getMessage(), e); } - if (droneEventsListener != null) { + for (DroneEventsListener droneEventsListener : connectedApps.values()) { droneEventsListener.finished(fit, offsets); } } + + public ConnectionParameter getConnectionParameter() { + return connectionParameter; + } } diff --git a/ServiceApp/src/org/droidplanner/services/android/interfaces/DroneEventsListener.java b/ServiceApp/src/org/droidplanner/services/android/interfaces/DroneEventsListener.java index 2df8c0fdc9..fc7cc88cf1 100644 --- a/ServiceApp/src/org/droidplanner/services/android/interfaces/DroneEventsListener.java +++ b/ServiceApp/src/org/droidplanner/services/android/interfaces/DroneEventsListener.java @@ -1,6 +1,7 @@ package org.droidplanner.services.android.interfaces; import com.MAVLink.Messages.MAVLinkMessage; +import com.o3dr.services.android.lib.drone.connection.DroneSharePrefs; import org.droidplanner.core.drone.DroneInterfaces; import org.droidplanner.core.drone.variables.helpers.MagnetometerCalibration; @@ -9,8 +10,10 @@ * Created by fhuya on 11/2/14. */ public interface DroneEventsListener extends DroneInterfaces.OnDroneListener, - DroneInterfaces.OnParameterManagerListener, MagnetometerCalibration - .OnMagCalibrationListener { + DroneInterfaces.OnParameterManagerListener, MagnetometerCalibration.OnMagCalibrationListener { + + DroneSharePrefs getDroneSharePrefs(); + void onConnectionFailed(String error); void onReceivedMavLinkMessage(MAVLinkMessage msg); diff --git a/ServiceApp/src/org/droidplanner/services/android/location/FusedLocation.java b/ServiceApp/src/org/droidplanner/services/android/location/FusedLocation.java index c949a38d93..fb57825bc8 100644 --- a/ServiceApp/src/org/droidplanner/services/android/location/FusedLocation.java +++ b/ServiceApp/src/org/droidplanner/services/android/location/FusedLocation.java @@ -4,22 +4,25 @@ import android.location.Location; import android.os.Handler; import android.util.Log; -import android.widget.Toast; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.common.api.Api; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; +import com.o3dr.services.android.lib.util.googleApi.GoogleApiClientManager; +import com.o3dr.services.android.lib.util.googleApi.GoogleApiClientManager.GoogleApiClientTask; import org.droidplanner.core.gcs.location.Location.LocationFinder; import org.droidplanner.core.gcs.location.Location.LocationReceiver; import org.droidplanner.core.helpers.coordinates.Coord3D; import org.droidplanner.core.helpers.units.Altitude; -import org.droidplanner.services.android.utils.GoogleApiClientManager; -import org.droidplanner.services.android.utils.GoogleApiClientManager.GoogleApiClientTask; /** * Feeds Location Data from Android's FusedLocation LocationProvider */ -public class FusedLocation implements LocationFinder, com.google.android.gms.location.LocationListener { +public class FusedLocation implements LocationFinder, com.google.android.gms.location.LocationListener, + GoogleApiClientManager.ManagerListener { private static final String TAG = FusedLocation.class.getSimpleName(); @@ -28,9 +31,29 @@ public class FusedLocation implements LocationFinder, com.google.android.gms.loc private static final float LOCATION_ACCURACY_THRESHOLD = 15.0f; private static final float JUMP_FACTOR = 4.0f; + private final static Api[] apisList = new Api[]{LocationServices.API}; + private final GoogleApiClientManager gApiMgr; - private final GoogleApiClientTask requestLocationUpdate; - private final GoogleApiClientTask removeLocationUpdate; + private final GoogleApiClientTask requestLocationUpdate = new GoogleApiClientTask() { + @Override + protected void doRun() { + final LocationRequest locationRequest = LocationRequest.create(); + locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); + locationRequest.setInterval(MIN_TIME_MS); + locationRequest.setFastestInterval(MIN_TIME_MS); + locationRequest.setSmallestDisplacement(MIN_DISTANCE_M); + LocationServices.FusedLocationApi.requestLocationUpdates(getGoogleApiClient(), + locationRequest, FusedLocation.this); + } + }; + + private final GoogleApiClientTask removeLocationUpdate = new GoogleApiClientTask() { + @Override + protected void doRun() { + LocationServices.FusedLocationApi.removeLocationUpdates(getGoogleApiClient(), + FusedLocation.this); + } + }; private LocationReceiver receiver; @@ -39,51 +62,26 @@ public class FusedLocation implements LocationFinder, com.google.android.gms.loc private float mTotalSpeed; private long mSpeedReadings; - public FusedLocation(Context context, Handler handler) { - gApiMgr = new GoogleApiClientManager(context, handler, LocationServices.API); - - requestLocationUpdate = gApiMgr.new GoogleApiClientTask() { - @Override - protected void doRun() { - final LocationRequest locationRequest = LocationRequest.create(); - locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); - locationRequest.setInterval(MIN_TIME_MS); - locationRequest.setFastestInterval(MIN_TIME_MS); - locationRequest.setSmallestDisplacement(MIN_DISTANCE_M); - LocationServices.FusedLocationApi.requestLocationUpdates(getGoogleApiClient(), - locationRequest, FusedLocation.this); - } - }; + private final Context context; - removeLocationUpdate = gApiMgr.new GoogleApiClientTask() { - @Override - protected void doRun() { - LocationServices.FusedLocationApi.removeLocationUpdates(getGoogleApiClient(), - FusedLocation.this); - } - }; - - gApiMgr.start(); + public FusedLocation(Context context, Handler handler) { + this.context = context; + gApiMgr = new GoogleApiClientManager(context, handler, apisList); + gApiMgr.setManagerListener(this); } @Override public void enableLocationUpdates() { + gApiMgr.start(); mSpeedReadings = 0; mTotalSpeed = 0f; - try { - gApiMgr.addTask(requestLocationUpdate); - } catch (IllegalStateException e) { - Log.e(TAG, "Unable to request location updates."); - } + mLastLocation = null; } @Override public void disableLocationUpdates() { - try { - gApiMgr.addTask(removeLocationUpdate); - } catch (IllegalStateException e) { - Log.e(TAG, "Unable to disable location updates."); - } + gApiMgr.addTask(removeLocationUpdate); + gApiMgr.stopSafely(); } @Override @@ -100,8 +98,7 @@ public void onLocationChanged(Location androidLocation) { final float currentSpeed = distanceToLast > 0f && timeSinceLast > 0 ? (distanceToLast / timeSinceLast) : 0f; - final boolean isLocationAccurate = isLocationAccurate(androidLocation.getAccuracy(), - currentSpeed); + final boolean isLocationAccurate = isLocationAccurate(androidLocation.getAccuracy(), currentSpeed); org.droidplanner.core.gcs.location.Location location = new org.droidplanner.core.gcs.location.Location( new Coord3D(androidLocation.getLatitude(), androidLocation.getLongitude(), @@ -109,7 +106,7 @@ public void onLocationChanged(Location androidLocation) { androidLocation.getSpeed(), isLocationAccurate, androidLocation.getTime()); mLastLocation = androidLocation; - receiver.onLocationChanged(location); + receiver.onLocationUpdate(location); } } @@ -141,4 +138,28 @@ private boolean isLocationAccurate(float accuracy, float currentSpeed) { public void setLocationListener(LocationReceiver receiver) { this.receiver = receiver; } + + @Override + public void onGoogleApiConnectionError(ConnectionResult result) { + if(receiver != null) + receiver.onLocationUnavailable(); + + GooglePlayServicesUtil.showErrorNotification(result.getErrorCode(), this.context); + } + + @Override + public void onUnavailableGooglePlayServices(int status) { + if(receiver != null) + receiver.onLocationUnavailable(); + + GooglePlayServicesUtil.showErrorNotification(status, this.context); + } + + @Override + public void onManagerStarted() { + gApiMgr.addTask(requestLocationUpdate); + } + + @Override + public void onManagerStopped() {} } diff --git a/ServiceApp/src/org/droidplanner/services/android/ui/activity/TLogFileSelector.java b/ServiceApp/src/org/droidplanner/services/android/ui/activity/TLogFileSelector.java index a7f28d9738..039b3f53bc 100644 --- a/ServiceApp/src/org/droidplanner/services/android/ui/activity/TLogFileSelector.java +++ b/ServiceApp/src/org/droidplanner/services/android/ui/activity/TLogFileSelector.java @@ -3,10 +3,8 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -18,10 +16,16 @@ import com.o3dr.services.android.lib.data.ServiceDataContract; import org.droidplanner.services.android.R; -import org.droidplanner.services.android.data.provider.FileProvider; import org.droidplanner.services.android.utils.file.FileUtils; import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; /** * TLog file selector activity. @@ -31,7 +35,7 @@ public class TLogFileSelector extends ActionBarActivity { private static final String TAG = TLogFileSelector.class.getSimpleName(); @Override - public void onCreate(Bundle savedInstanceState){ + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_tlog_file_selector); @@ -47,7 +51,17 @@ public void onCreate(Bundle savedInstanceState){ setResult(Activity.RESULT_CANCELED, null); final File[] tlogFiles = FileUtils.getTLogFileList(context, appId); - final FileAdapter filesAdapter = new FileAdapter(context, tlogFiles); + final TreeMap sortedFiles = new TreeMap<>(); + for(File file : tlogFiles){ + sortedFiles.put(file.getName(), file); + } + + final ArrayList sortedList = new ArrayList<>(sortedFiles.size()); + for(Map.Entry entry : sortedFiles.entrySet()){ + sortedList.add(entry.getValue()); + } + + final FileAdapter filesAdapter = new FileAdapter(context, sortedList); final ListView tlogListView = (ListView) findViewById(R.id.tlog_files_list); tlogListView.setAdapter(filesAdapter); @@ -56,28 +70,31 @@ public void onCreate(Bundle savedInstanceState){ public void onItemClick(AdapterView parent, View view, int position, long id) { //Get a file for the selected file name. File requestFile = tlogFiles[position]; - - //Use the FileProvider to get a content URI - try{ - Uri fileUri = FileProvider.getUriForFile(context, ServiceDataContract.FILE_PROVIDER_AUTHORITY, - requestFile); - - //Grant temporary read permission to the content URI - resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - - //Put the uri and mime type in the result intent - final String mimeType = getContentResolver().getType(fileUri); - resultIntent.setDataAndType(fileUri, mimeType); - - //Set the result - setResult(Activity.RESULT_OK, resultIntent); - } - catch(IllegalArgumentException e){ - Log.e(TAG, "The selected file can't be shared: " + requestFile.getName()); - - resultIntent.setDataAndType(null, ""); - setResult(Activity.RESULT_CANCELED, resultIntent); - } + resultIntent.putExtra(ServiceDataContract.EXTRA_TLOG_ABSOLUTE_PATH, requestFile.getAbsolutePath()); + //Set the result + setResult(Activity.RESULT_OK, resultIntent); + +// //Use the FileProvider to get a content URI +// try{ +// Uri fileUri = FileProvider.getUriForFile(context, ServiceDataContract.FILE_PROVIDER_AUTHORITY, +// requestFile); +// +// //Grant temporary read permission to the content URI +// resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); +// +// //Put the uri and mime type in the result intent +// final String mimeType = getContentResolver().getType(fileUri); +// resultIntent.setDataAndType(fileUri, mimeType); +// +// //Set the result +// setResult(Activity.RESULT_OK, resultIntent); +// } +// catch(IllegalArgumentException e){ +// Log.e(TAG, "The selected file can't be shared: " + requestFile.getName()); +// +// resultIntent.setDataAndType(null, ""); +// setResult(Activity.RESULT_CANCELED, resultIntent); +// } finish(); } @@ -86,18 +103,17 @@ public void onItemClick(AdapterView parent, View view, int position, long id) private static class FileAdapter extends ArrayAdapter { - public FileAdapter(Context context, File[] objects) { + public FileAdapter(Context context, List objects) { super(context, 0, objects); } @Override - public View getView(int position, View convertView, ViewGroup parent){ + public View getView(int position, View convertView, ViewGroup parent) { TextView view; File file = getItem(position); - if(convertView != null){ + if (convertView != null) { view = (TextView) convertView; - } - else{ + } else { view = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.list_item_tlog_info, parent, false); } diff --git a/ServiceApp/src/org/droidplanner/services/android/ui/adapter/AppConnectionAdapter.java b/ServiceApp/src/org/droidplanner/services/android/ui/adapter/AppConnectionAdapter.java index c1ca153dff..06235c6b72 100644 --- a/ServiceApp/src/org/droidplanner/services/android/ui/adapter/AppConnectionAdapter.java +++ b/ServiceApp/src/org/droidplanner/services/android/ui/adapter/AppConnectionAdapter.java @@ -114,7 +114,7 @@ public void onClick(View v) { } } - if (droneApi.getDroneManager().isConnected()) { + if (droneApi.isConnected()) { viewHolder.clientConnectionInfo.setText(SpannableUtils.normal("Status: ", SpannableUtils.color(Color .GREEN, "Connected"))); } else { diff --git a/ServiceApp/src/org/droidplanner/services/android/ui/adapter/RecommendedAppsAdapter.java b/ServiceApp/src/org/droidplanner/services/android/ui/adapter/RecommendedAppsAdapter.java index 5430f67180..118468986c 100644 --- a/ServiceApp/src/org/droidplanner/services/android/ui/adapter/RecommendedAppsAdapter.java +++ b/ServiceApp/src/org/droidplanner/services/android/ui/adapter/RecommendedAppsAdapter.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.net.URL; import java.util.Map; import java.util.WeakHashMap; @@ -174,20 +175,29 @@ public DownloadImageTask(String appId, ImageView bmImage, Map ca } protected Bitmap doInBackground(String... urls) { - String urldisplay = urls[0]; + String urlDisplay = urls[0]; + if(urlDisplay == null) + return null; + Bitmap mIcon11 = null; try { - InputStream in = new java.net.URL(urldisplay).openStream(); + InputStream in = new URL(urlDisplay).openStream(); mIcon11 = BitmapFactory.decodeStream(in); } catch (Exception e) { - Log.e("Error", e.getMessage()); + Log.e(TAG, e.getMessage(), e); } + return mIcon11; } protected void onPostExecute(Bitmap result) { - bmImage.setImageBitmap(result); - cachedMap.put(appId, result); + if(result != null) { + if(bmImage != null) + bmImage.setImageBitmap(result); + + if(cachedMap != null && appId != null) + cachedMap.put(appId, result); + } } } } diff --git a/ServiceApp/src/org/droidplanner/services/android/ui/fragment/AppConnectionsFragment.java b/ServiceApp/src/org/droidplanner/services/android/ui/fragment/AppConnectionsFragment.java index 5d31a62c54..c86bbf7b5a 100644 --- a/ServiceApp/src/org/droidplanner/services/android/ui/fragment/AppConnectionsFragment.java +++ b/ServiceApp/src/org/droidplanner/services/android/ui/fragment/AppConnectionsFragment.java @@ -115,6 +115,9 @@ public void onStop(){ } public void refreshDroneList(){ + if(parent == null) + return; + DroneAccess droneAccess = parent.getDroneAccess(); if(droneAccess != null) { List dronesList = droneAccess.getDroneApiList(); diff --git a/ServiceApp/src/org/droidplanner/services/android/utils/GoogleApiClientManager.java b/ServiceApp/src/org/droidplanner/services/android/utils/GoogleApiClientManager.java deleted file mode 100644 index 1a314268db..0000000000 --- a/ServiceApp/src/org/droidplanner/services/android/utils/GoogleApiClientManager.java +++ /dev/null @@ -1,245 +0,0 @@ -package org.droidplanner.services.android.utils; - -import java.util.concurrent.LinkedBlockingQueue; - -import android.content.Context; -import android.os.Handler; -import android.os.HandlerThread; -import android.util.Log; - -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GooglePlayServicesUtil; -import com.google.android.gms.common.api.Api; -import com.google.android.gms.common.api.GoogleApiClient; - -/** - * Handles the lifecycle for the google api client. Also takes care of running submitted tasks - * when the google api client is connected. - */ -public class GoogleApiClientManager { - - private final static String TAG = GoogleApiClientManager.class.getSimpleName(); - - /** - * Manager background thread used to run the submitted google api client tasks. - */ - private final Runnable mDriverRunnable = new Runnable() { - @Override - public void run() { - try{ - while(true){ - final GoogleApiClientTask task = mTaskQueue.take(); - - if(!mGoogleApiClient.isConnected()){ - final ConnectionResult result = mGoogleApiClient.blockingConnect(); - if(!result.isSuccess()){ - throw new IllegalStateException("Unable to connect to the google api " + - "client: " + result.getErrorCode()); - } - } - - if(task.mRunOnBackgroundThread) { - mBgHandler.post(task); - } - else{ - mMainHandler.post(task); - } - } - } - catch(InterruptedException e){ - Log.v(TAG, e.getMessage(), e); - } - } - }; - - private Thread mDriverThread; - - /** - * This handler is in charge of running google api client tasks on the calling thread. - */ - private final Handler mMainHandler; - - /** - * This handler is in charge of running google api client tasks on the background thread. - */ - private Handler mBgHandler; - private HandlerThread mBgHandlerThread; - - /** - * Application context. - */ - private final Context mContext; - - /** - * Handle to the google api client. - */ - private final GoogleApiClient mGoogleApiClient; - - /** - * Holds tasks that needs to be run using the google api client. - * A background thread will be blocking on this queue until new tasks are inserted. In which - * case, it will retrieve the new task, and process it. - */ - private final LinkedBlockingQueue mTaskQueue = new LinkedBlockingQueue - (); - - public GoogleApiClientManager(Context context, Handler handler, Api... apis){ - mContext = context; - mMainHandler = handler; - - final GoogleApiClient.Builder apiBuilder = new GoogleApiClient.Builder(context); - for(Api api: apis){ - apiBuilder.addApi(api); - } - - mGoogleApiClient = apiBuilder.build(); - } - - private void destroyBgHandler() { - if (mBgHandlerThread != null && mBgHandlerThread.isAlive()) { - mBgHandlerThread.quit(); - mBgHandlerThread.interrupt(); - mBgHandlerThread = null; - } - - mBgHandler = null; - } - - private void destroyDriverThread(){ - if(mDriverThread != null && mDriverThread.isAlive()){ - mDriverThread.interrupt(); - mDriverThread = null; - } - } - - private void initializeBgHandler(){ - if(mBgHandlerThread == null || mBgHandlerThread.isInterrupted()) { - mBgHandlerThread = new HandlerThread("GAC Manager Background Thread"); - mBgHandlerThread.start(); - mBgHandler = null; - } - - if(mBgHandler == null) { - mBgHandler = new Handler(mBgHandlerThread.getLooper()); - } - } - - private void initializeDriverThread(){ - if(mDriverThread == null || mDriverThread.isInterrupted()){ - mDriverThread = new Thread(mDriverRunnable, "GAC Manager Driver Thread"); - mDriverThread.start(); - } - } - - /** - * Adds a task to the google api client manager tasks queue. This task will be scheduled to - * run on the calling thread. - * @param task task making use of the google api client. - * @return true if the task was successfully added to the queue. - * @throws IllegalStateException is the start() method was not called. - */ - public boolean addTask(GoogleApiClientTask task){ - if(!isStarted()){ - throw new IllegalStateException("GoogleApiClientManager#start() was not called."); - } - - task.mRunOnBackgroundThread = false; - return mTaskQueue.offer(task); - } - - /** - * Adds a task to the google api client manager tasks queue. This task will be scheduled to - * run on a background thread. - * @param task task making use of the google api client. - * @return true if the task was successfully added to the queue. - * @throws IllegalStateException is the start() method was not called. - */ - public boolean addTaskToBackground(GoogleApiClientTask task){ - if(!isStarted()){ - throw new IllegalStateException("GoogleApiClientManager#start() was not called."); - } - - task.mRunOnBackgroundThread = true; - return mTaskQueue.offer(task); - } - - /** - * @return true the google api client manager was started. - */ - private boolean isStarted(){ - return mDriverThread != null && mDriverThread.isAlive() - && mBgHandlerThread != null && mBgHandlerThread.isAlive() - && mBgHandler != null && mBgHandler.getLooper() != null; - } - - /** - * Activates the google api client manager. - */ - public void start(){ - if(isGooglePlayServicesValid()) { - initializeDriverThread(); - initializeBgHandler(); - } - else{ - Log.e(TAG, "Google Play Services is unavailable."); - } - } - - private boolean isGooglePlayServicesValid(){ - // Check for the google play services is available - final int playStatus = GooglePlayServicesUtil - .isGooglePlayServicesAvailable(mContext); - return playStatus == ConnectionResult.SUCCESS; - } - - /** - * Release the resources used by this manager. - * After calling this method, start() needs to be called again to use that manager again. - */ - public void stop(){ - if(isGooglePlayServicesValid()) { - destroyBgHandler(); - destroyDriverThread(); - - mTaskQueue.clear(); - if (mGoogleApiClient.isConnected() || mGoogleApiClient.isConnecting()) { - mGoogleApiClient.disconnect(); - } - } - else{ - Log.e(TAG, "Google Play Services is unavailable."); - } - } - - /** - * Type for the google api client tasks. - */ - public abstract class GoogleApiClientTask implements Runnable { - - /** - * If true, this task will be scheduled to run on a background thread. - * Otherwise, it will run on the calling thread. - */ - private boolean mRunOnBackgroundThread = false; - - protected GoogleApiClient getGoogleApiClient(){ - return mGoogleApiClient; - } - - @Override - public void run(){ - if(!getGoogleApiClient().isConnected()){ - //Add the task back to the queue. - mTaskQueue.offer(this); - return; - } - - //Run the task - doRun(); - } - - protected abstract void doRun(); - - } -} diff --git a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/MavLinkMsgHandler.java b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/MavLinkMsgHandler.java index 3826c811b0..ef2e11b4da 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/MavLinkMsgHandler.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/MavLinkMsgHandler.java @@ -71,8 +71,7 @@ public void receiveData(MAVLinkMessage msg) { case msg_heartbeat.MAVLINK_MSG_ID_HEARTBEAT: msg_heartbeat msg_heart = (msg_heartbeat) msg; drone.setType(msg_heart.type); - drone.getState().setIsFlying( - ((msg_heartbeat) msg).system_status == MAV_STATE.MAV_STATE_ACTIVE); + checkIfFlying(msg_heart); processState(msg_heart); ApmModes newMode = ApmModes.getMode(msg_heart.custom_mode, drone.getType()); drone.getState().setMode(newMode); @@ -136,21 +135,33 @@ public void receiveData(MAVLinkMessage msg) { } } - public void processState(msg_heartbeat msg_heart) { + private void checkIfFlying(msg_heartbeat msg_heart) { + final byte systemStatus = msg_heart.system_status; + final boolean wasFlying = drone.getState().isFlying(); + + final boolean isFlying = systemStatus == MAV_STATE.MAV_STATE_ACTIVE + || (wasFlying + && (systemStatus == MAV_STATE.MAV_STATE_CRITICAL || systemStatus == MAV_STATE.MAV_STATE_EMERGENCY)); + + drone.getState().setIsFlying(isFlying); + } + + public void processState(msg_heartbeat msg_heart) { checkArmState(msg_heart); checkFailsafe(msg_heart); } private void checkFailsafe(msg_heartbeat msg_heart) { - boolean failsafe2 = msg_heart.system_status == (byte) MAV_STATE.MAV_STATE_CRITICAL; - if (failsafe2) { - drone.getState().setWarning("Failsafe"); + boolean failsafe2 = msg_heart.system_status == (byte) MAV_STATE.MAV_STATE_CRITICAL + || msg_heart.system_status == MAV_STATE.MAV_STATE_EMERGENCY; + + if (failsafe2) { + drone.getState().repeatWarning(); } } private void checkArmState(msg_heartbeat msg_heart) { - drone.getState() - .setArmed( + drone.getState().setArmed( (msg_heart.base_mode & (byte) MAV_MODE_FLAG.MAV_MODE_FLAG_SAFETY_ARMED) == (byte) MAV_MODE_FLAG.MAV_MODE_FLAG_SAFETY_ARMED); } } diff --git a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/WaypointManager.java b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/WaypointManager.java index cb3d2477fd..a9fd680f81 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/WaypointManager.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/WaypointManager.java @@ -1,15 +1,5 @@ package org.droidplanner.core.MAVLink; -import java.util.ArrayList; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; - -import org.droidplanner.core.drone.DroneInterfaces.OnTimeout; -import org.droidplanner.core.drone.DroneInterfaces.OnWaypointManagerListener; -import org.droidplanner.core.drone.DroneVariable; -import org.droidplanner.core.model.Drone; - import com.MAVLink.Messages.MAVLinkMessage; import com.MAVLink.common.msg_mission_ack; import com.MAVLink.common.msg_mission_count; @@ -18,393 +8,340 @@ import com.MAVLink.common.msg_mission_item_reached; import com.MAVLink.common.msg_mission_request; +import org.droidplanner.core.drone.DroneInterfaces; +import org.droidplanner.core.drone.DroneInterfaces.OnWaypointManagerListener; +import org.droidplanner.core.drone.DroneVariable; +import org.droidplanner.core.model.Drone; + +import java.util.ArrayList; +import java.util.List; + /** * Class to manage the communication of waypoints to the MAV. - * + *

* Should be initialized with a MAVLink Object, so the manager can send messages * via the MAV link. The function processMessage must be called with every new * MAV Message. - * */ -public class WaypointManager extends DroneVariable implements OnTimeout { - enum WaypointStates { - IDLE, READ_REQUEST, READING_WP, WRITING_WP_COUNT, WRITING_WP, WAITING_WRITE_ACK - } - - public enum WaypointEvent_Type { - WP_UPLOAD, WP_DOWNLOAD, WP_RETRY, WP_CONTINUE, WP_TIMED_OUT - } - - private int readIndex; - private int writeIndex; - private int retryIndex; - final private int maxRetry = 3; - private TimeOut timeOut; - private OnWaypointManagerListener wpEventListener; - - WaypointStates state = WaypointStates.IDLE; - - /** - * waypoint witch is currently being written - */ - - public WaypointManager(Drone drone) { - super(drone); - this.timeOut = new TimeOut(this); - } - - public WaypointManager(Drone myDrone, TimeOut timeOut) { - super(myDrone); - this.timeOut = timeOut; - } - - public void setWaypointManagerListener(OnWaypointManagerListener wpEventListener) { - this.wpEventListener = wpEventListener; - } - - /** - * Try to receive all waypoints from the MAV. - * - * If all runs well the callback will return the list of waypoints. - */ - public void getWaypoints() { - // ensure that WPManager is not doing anything else - if (state != WaypointStates.IDLE) - return; - - doBeginWaypointEvent(WaypointEvent_Type.WP_DOWNLOAD); - readIndex = -1; - timeOut.setTimeOutValue(3000); - timeOut.setTimeOutRetry(maxRetry); - state = WaypointStates.READ_REQUEST; - timeOut.setTimeOut(); - MavLinkWaypoint.requestWaypointsList(myDrone); - } - - /** - * Write a list of waypoints to the MAV. - * - * The callback will return the status of this operation - * - * @param data - * waypoints to be written - */ - - public void writeWaypoints(List data) { - // ensure that WPManager is not doing anything else - if (state != WaypointStates.IDLE) - return; - - if ((mission != null)) { - doBeginWaypointEvent(WaypointEvent_Type.WP_UPLOAD); - updateMsgIndexes(data); - mission.clear(); - mission.addAll(data); - writeIndex = 0; - timeOut.setTimeOutValue(3000); - timeOut.setTimeOutRetry(3); - state = WaypointStates.WRITING_WP_COUNT; - timeOut.setTimeOut(); - MavLinkWaypoint.sendWaypointCount(myDrone, mission.size()); - } - } - - private void updateMsgIndexes(List data) { - short index = 0; - for (msg_mission_item msg : data) { - msg.seq = index++; - } - } - - /** - * Sets the current waypoint in the MAV - * - * The callback will return the status of this operation - */ - public void setCurrentWaypoint(int i) { - if ((mission != null)) { - MavLinkWaypoint.sendSetCurrentWaypoint(myDrone, (short) i); - } - } - - /** - * Callback for when a waypoint has been reached - * - * @param wpNumber - * number of the completed waypoint - */ - public void onWaypointReached(int wpNumber) { - } - - /** - * Callback for a change in the current waypoint the MAV is heading for - * - * @param seq - * number of the updated waypoint - */ - private void onCurrentWaypointUpdate(short seq) { - } - - /** - * number of waypoints to be received, used when reading waypoints - */ - private short waypointCount; - /** - * list of waypoints used when writing or receiving - */ - private List mission = new ArrayList(); - - /** - * Try to process a Mavlink message if it is a mission related message - * - * @param msg - * Mavlink message to process - * @return Returns true if the message has been processed - */ - public boolean processMessage(MAVLinkMessage msg) { - switch (state) { - default: - case IDLE: - break; - case READ_REQUEST: - if (msg.msgid == msg_mission_count.MAVLINK_MSG_ID_MISSION_COUNT) { - waypointCount = ((msg_mission_count) msg).count; - mission.clear(); - timeOut.setTimeOut(); - MavLinkWaypoint.requestWayPoint(myDrone, mission.size()); - state = WaypointStates.READING_WP; - return true; - } - break; - case READING_WP: - if (msg.msgid == msg_mission_item.MAVLINK_MSG_ID_MISSION_ITEM) { - timeOut.setTimeOut(); - processReceivedWaypoint((msg_mission_item) msg); - doWaypointEvent(WaypointEvent_Type.WP_DOWNLOAD, readIndex + 1, waypointCount); - if (mission.size() < waypointCount) { - MavLinkWaypoint.requestWayPoint(myDrone, mission.size()); - } else { - timeOut.resetTimeOut(); - state = WaypointStates.IDLE; - MavLinkWaypoint.sendAck(myDrone); - myDrone.getMission().onMissionReceived(mission); - doEndWaypointEvent(WaypointEvent_Type.WP_DOWNLOAD); - } - return true; - } - break; - case WRITING_WP_COUNT: - state = WaypointStates.WRITING_WP; - case WRITING_WP: - if (msg.msgid == msg_mission_request.MAVLINK_MSG_ID_MISSION_REQUEST) { - timeOut.setTimeOut(); - processWaypointToSend((msg_mission_request) msg); - doWaypointEvent(WaypointEvent_Type.WP_UPLOAD, writeIndex + 1, mission.size()); - return true; - } - break; - case WAITING_WRITE_ACK: - if (msg.msgid == msg_mission_ack.MAVLINK_MSG_ID_MISSION_ACK) { - timeOut.resetTimeOut(); - myDrone.getMission().onWriteWaypoints((msg_mission_ack) msg); - state = WaypointStates.IDLE; - doEndWaypointEvent(WaypointEvent_Type.WP_UPLOAD); - return true; - } - break; - } - - if (msg.msgid == msg_mission_item_reached.MAVLINK_MSG_ID_MISSION_ITEM_REACHED) { - onWaypointReached(((msg_mission_item_reached) msg).seq); - return true; - } - if (msg.msgid == msg_mission_current.MAVLINK_MSG_ID_MISSION_CURRENT) { - onCurrentWaypointUpdate(((msg_mission_current) msg).seq); - return true; - } - return false; - } - - @Override - public void notifyTimeOut(int timeOutCount) { - processTimeOut(timeOutCount); - } - - public boolean processTimeOut(int mTimeOutCount) { - - // If max retry is reached, set state to IDLE. No more retry. - if (mTimeOutCount >= timeOut.getTimeOutRetry()) { - state = WaypointStates.IDLE; - doWaypointEvent(WaypointEvent_Type.WP_TIMED_OUT, retryIndex, maxRetry); - return false; - } - - retryIndex++; - doWaypointEvent(WaypointEvent_Type.WP_RETRY, retryIndex, maxRetry); - - timeOut.setTimeOut(false); - - switch (state) { - default: - case IDLE: - break; - case READ_REQUEST: - MavLinkWaypoint.requestWaypointsList(myDrone); - break; - case READING_WP: - if (mission.size() < waypointCount) { // request last lost WP - MavLinkWaypoint.requestWayPoint(myDrone, mission.size()); - } - break; - case WRITING_WP_COUNT: - MavLinkWaypoint.sendWaypointCount(myDrone, mission.size()); - break; - case WRITING_WP: - // Log.d("TIMEOUT", "re Write Msg: " + String.valueOf(writeIndex)); - if (writeIndex < mission.size()) { - myDrone.getMavClient().sendMavPacket(mission.get(writeIndex).pack()); - } - break; - case WAITING_WRITE_ACK: - myDrone.getMavClient().sendMavPacket(mission.get(mission.size() - 1).pack()); - break; - } - - return true; - } - - private void processWaypointToSend(msg_mission_request msg) { - /* - * Log.d("TIMEOUT", "Write Msg: " + String.valueOf(msg.seq)); +public class WaypointManager extends DroneVariable { + enum WaypointStates { + IDLE, READ_REQUEST, READING_WP, WRITING_WP_COUNT, WRITING_WP, WAITING_WRITE_ACK + } + + public enum WaypointEvent_Type { + WP_UPLOAD, WP_DOWNLOAD, WP_RETRY, WP_CONTINUE, WP_TIMED_OUT + } + + private static final long TIMEOUT = 15000; //ms + private static final int RETRY_LIMIT = 3; + + private int retryTracker = 0; + + private int readIndex; + private int writeIndex; + private int retryIndex; + private OnWaypointManagerListener wpEventListener; + + WaypointStates state = WaypointStates.IDLE; + + /** + * waypoint witch is currently being written + */ + + private final DroneInterfaces.Handler watchdog; + + private final Runnable watchdogCallback = new Runnable() { + @Override + public void run() { + if (processTimeOut(++retryTracker)) + watchdog.postDelayed(this, TIMEOUT); + } + }; + + public WaypointManager(Drone drone, DroneInterfaces.Handler handler) { + super(drone); + this.watchdog = handler; + } + + public void setWaypointManagerListener(OnWaypointManagerListener wpEventListener) { + this.wpEventListener = wpEventListener; + } + + private void startWatchdog() { + stopWatchdog(); + + retryTracker = 0; + this.watchdog.postDelayed(watchdogCallback, TIMEOUT); + } + + private void stopWatchdog() { + this.watchdog.removeCallbacks(watchdogCallback); + } + + /** + * Try to receive all waypoints from the MAV. + *

+ * If all runs well the callback will return the list of waypoints. + */ + public void getWaypoints() { + // ensure that WPManager is not doing anything else + if (state != WaypointStates.IDLE) + return; + + doBeginWaypointEvent(WaypointEvent_Type.WP_DOWNLOAD); + readIndex = -1; + state = WaypointStates.READ_REQUEST; + MavLinkWaypoint.requestWaypointsList(myDrone); + + startWatchdog(); + } + + /** + * Write a list of waypoints to the MAV. + *

+ * The callback will return the status of this operation + * + * @param data waypoints to be written + */ + + public void writeWaypoints(List data) { + // ensure that WPManager is not doing anything else + if (state != WaypointStates.IDLE) + return; + + if ((mission != null)) { + doBeginWaypointEvent(WaypointEvent_Type.WP_UPLOAD); + updateMsgIndexes(data); + mission.clear(); + mission.addAll(data); + writeIndex = 0; + state = WaypointStates.WRITING_WP_COUNT; + MavLinkWaypoint.sendWaypointCount(myDrone, mission.size()); + + startWatchdog(); + } + } + + private void updateMsgIndexes(List data) { + short index = 0; + for (msg_mission_item msg : data) { + msg.seq = index++; + } + } + + /** + * Sets the current waypoint in the MAV + *

+ * The callback will return the status of this operation + */ + public void setCurrentWaypoint(int i) { + if ((mission != null)) { + MavLinkWaypoint.sendSetCurrentWaypoint(myDrone, (short) i); + } + } + + /** + * Callback for when a waypoint has been reached + * + * @param wpNumber number of the completed waypoint + */ + public void onWaypointReached(int wpNumber) { + } + + /** + * Callback for a change in the current waypoint the MAV is heading for + * + * @param seq number of the updated waypoint + */ + private void onCurrentWaypointUpdate(short seq) { + } + + /** + * number of waypoints to be received, used when reading waypoints + */ + private short waypointCount; + /** + * list of waypoints used when writing or receiving + */ + private List mission = new ArrayList(); + + /** + * Try to process a Mavlink message if it is a mission related message + * + * @param msg Mavlink message to process + * @return Returns true if the message has been processed + */ + public boolean processMessage(MAVLinkMessage msg) { + switch (state) { + default: + case IDLE: + break; + + case READ_REQUEST: + if (msg.msgid == msg_mission_count.MAVLINK_MSG_ID_MISSION_COUNT) { + waypointCount = ((msg_mission_count) msg).count; + mission.clear(); + startWatchdog(); + MavLinkWaypoint.requestWayPoint(myDrone, mission.size()); + state = WaypointStates.READING_WP; + return true; + } + break; + + case READING_WP: + if (msg.msgid == msg_mission_item.MAVLINK_MSG_ID_MISSION_ITEM) { + startWatchdog(); + processReceivedWaypoint((msg_mission_item) msg); + doWaypointEvent(WaypointEvent_Type.WP_DOWNLOAD, readIndex + 1, waypointCount); + if (mission.size() < waypointCount) { + MavLinkWaypoint.requestWayPoint(myDrone, mission.size()); + } else { + stopWatchdog(); + state = WaypointStates.IDLE; + MavLinkWaypoint.sendAck(myDrone); + myDrone.getMission().onMissionReceived(mission); + doEndWaypointEvent(WaypointEvent_Type.WP_DOWNLOAD); + } + return true; + } + break; + + case WRITING_WP_COUNT: + state = WaypointStates.WRITING_WP; + case WRITING_WP: + if (msg.msgid == msg_mission_request.MAVLINK_MSG_ID_MISSION_REQUEST) { + startWatchdog(); + processWaypointToSend((msg_mission_request) msg); + doWaypointEvent(WaypointEvent_Type.WP_UPLOAD, writeIndex + 1, mission.size()); + return true; + } + break; + + case WAITING_WRITE_ACK: + if (msg.msgid == msg_mission_ack.MAVLINK_MSG_ID_MISSION_ACK) { + stopWatchdog(); + myDrone.getMission().onWriteWaypoints((msg_mission_ack) msg); + state = WaypointStates.IDLE; + doEndWaypointEvent(WaypointEvent_Type.WP_UPLOAD); + return true; + } + break; + } + + if (msg.msgid == msg_mission_item_reached.MAVLINK_MSG_ID_MISSION_ITEM_REACHED) { + onWaypointReached(((msg_mission_item_reached) msg).seq); + return true; + } + if (msg.msgid == msg_mission_current.MAVLINK_MSG_ID_MISSION_CURRENT) { + onCurrentWaypointUpdate(((msg_mission_current) msg).seq); + return true; + } + return false; + } + + public boolean processTimeOut(int mTimeOutCount) { + + // If max retry is reached, set state to IDLE. No more retry. + if (mTimeOutCount >= RETRY_LIMIT) { + state = WaypointStates.IDLE; + doWaypointEvent(WaypointEvent_Type.WP_TIMED_OUT, retryIndex, RETRY_LIMIT); + return false; + } + + retryIndex++; + doWaypointEvent(WaypointEvent_Type.WP_RETRY, retryIndex, RETRY_LIMIT); + + switch (state) { + default: + case IDLE: + break; + + case READ_REQUEST: + MavLinkWaypoint.requestWaypointsList(myDrone); + break; + + case READING_WP: + if (mission.size() < waypointCount) { // request last lost WP + MavLinkWaypoint.requestWayPoint(myDrone, mission.size()); + } + break; + + case WRITING_WP_COUNT: + MavLinkWaypoint.sendWaypointCount(myDrone, mission.size()); + break; + + case WRITING_WP: + // Log.d("TIMEOUT", "re Write Msg: " + String.valueOf(writeIndex)); + if (writeIndex < mission.size()) { + myDrone.getMavClient().sendMavPacket(mission.get(writeIndex).pack()); + } + break; + + case WAITING_WRITE_ACK: + myDrone.getMavClient().sendMavPacket(mission.get(mission.size() - 1).pack()); + break; + } + + return true; + } + + private void processWaypointToSend(msg_mission_request msg) { + /* + * Log.d("TIMEOUT", "Write Msg: " + String.valueOf(msg.seq)); */ - writeIndex = msg.seq; - msg_mission_item item = mission.get(writeIndex); - item.target_system = myDrone.getSysid(); - item.target_component = myDrone.getCompid(); - myDrone.getMavClient().sendMavPacket(item.pack()); - - if (writeIndex + 1 >= mission.size()) { - state = WaypointStates.WAITING_WRITE_ACK; - } - } - - private void processReceivedWaypoint(msg_mission_item msg) { + writeIndex = msg.seq; + msg_mission_item item = mission.get(writeIndex); + item.target_system = myDrone.getSysid(); + item.target_component = myDrone.getCompid(); + myDrone.getMavClient().sendMavPacket(item.pack()); + + if (writeIndex + 1 >= mission.size()) { + state = WaypointStates.WAITING_WRITE_ACK; + } + } + + private void processReceivedWaypoint(msg_mission_item msg) { /* * Log.d("TIMEOUT", "Read Last/Curr: " + String.valueOf(readIndex) + "/" * + String.valueOf(msg.seq)); */ - // in case of we receive the same WP again after retry - if (msg.seq <= readIndex) - return; - - readIndex = msg.seq; + // in case of we receive the same WP again after retry + if (msg.seq <= readIndex) + return; + + readIndex = msg.seq; + + mission.add(msg); + } + + private void doBeginWaypointEvent(WaypointEvent_Type wpEvent) { + retryIndex = 0; + + if (wpEventListener == null) + return; + + wpEventListener.onBeginWaypointEvent(wpEvent); + } + + private void doEndWaypointEvent(WaypointEvent_Type wpEvent) { + if (retryIndex > 0)// if retry successful, notify that we now continue + doWaypointEvent(WaypointEvent_Type.WP_CONTINUE, retryIndex, RETRY_LIMIT); + + retryIndex = 0; + + if (wpEventListener == null) + return; + + wpEventListener.onEndWaypointEvent(wpEvent); + } + + private void doWaypointEvent(WaypointEvent_Type wpEvent, int index, int count) { + retryIndex = 0; + + if (wpEventListener == null) + return; - mission.add(msg); - } - - private void doBeginWaypointEvent(WaypointEvent_Type wpEvent) { - retryIndex = 0; - - if (wpEventListener == null) - return; - - wpEventListener.onBeginWaypointEvent(wpEvent); - } - - private void doEndWaypointEvent(WaypointEvent_Type wpEvent) { - if (retryIndex > 0)// if retry successful, notify that we now continue - doWaypointEvent(WaypointEvent_Type.WP_CONTINUE, retryIndex, maxRetry); - - retryIndex = 0; - - if (wpEventListener == null) - return; - - wpEventListener.onEndWaypointEvent(wpEvent); - } - - private void doWaypointEvent(WaypointEvent_Type wpEvent, int index, int count) { - retryIndex = 0; - - if (wpEventListener == null) - return; - - wpEventListener.onWaypointEvent(wpEvent, index, count); - } - - private class TimeOut { - - private Timer timeOutTimer; - private int timeOutCount; - private long timeOut; - private int timeOutRetry; - private OnTimeout listener; - - public TimeOut(OnTimeout listener) { - this.listener = listener; - } - - public void setTimeOutValue(long timeout_ms) { - this.timeOut = timeout_ms; - } - - public void setTimeOutRetry(int timeout_retry) { - this.timeOutRetry = timeout_retry; - } - - public int getTimeOutRetry() { - if (this.timeOutRetry <= 0) - return 3; // default value - - return this.timeOutRetry; - } - - public synchronized void resetTimeOut() { - if (timeOutTimer != null) { - timeOutTimer.cancel(); - timeOutTimer = null; - /* - * Log.d("TIMEOUT", "reset " + String.valueOf(timeOutTimer)); - */ - } - } - - public void setTimeOut() { - setTimeOut(this.timeOut, true); - } - - public void setTimeOut(boolean resetTimeOutCount) { - setTimeOut(this.timeOut, resetTimeOutCount); - } - - public synchronized void setTimeOut(long timeout_ms, boolean resetTimeOutCount) { - /* - * Log.d("TIMEOUT", "set " + String.valueOf(timeout_ms)); - */ - resetTimeOut(); - if (resetTimeOutCount) - timeOutCount = 0; - - if (timeOutTimer == null) { - timeOutTimer = new Timer(); - timeOutTimer.schedule(new TimerTask() { - @Override - public void run() { - if (timeOutTimer != null) { - resetTimeOut(); - timeOutCount++; - - /* - * Log.d("TIMEOUT", "timed out"); - */ - - listener.notifyTimeOut(timeOutCount); - } - } - }, timeout_ms); // delay in milliseconds - } - } - - } + wpEventListener.onWaypointEvent(wpEvent, index, count); + } } diff --git a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/MavLinkROI.java b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/command/doCmd/MavLinkDoCmds.java similarity index 57% rename from dependencyLibs/Core/src/org/droidplanner/core/MAVLink/MavLinkROI.java rename to dependencyLibs/Core/src/org/droidplanner/core/MAVLink/command/doCmd/MavLinkDoCmds.java index 77253bf7a3..6324a4b88e 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/MavLinkROI.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/command/doCmd/MavLinkDoCmds.java @@ -1,6 +1,7 @@ -package org.droidplanner.core.MAVLink; +package org.droidplanner.core.MAVLink.command.doCmd; import org.droidplanner.core.helpers.coordinates.Coord3D; +import org.droidplanner.core.helpers.units.Altitude; import org.droidplanner.core.mission.commands.EpmGripper; import org.droidplanner.core.model.Drone; @@ -8,8 +9,11 @@ import com.MAVLink.common.msg_command_long; import com.MAVLink.enums.MAV_CMD; -public class MavLinkROI { +public class MavLinkDoCmds { public static void setROI(Drone drone, Coord3D coord) { + if(drone == null) + return; + msg_command_long msg = new msg_command_long(); msg.target_system = drone.getSysid(); msg.target_component = drone.getCompid(); @@ -23,19 +27,16 @@ public static void setROI(Drone drone, Coord3D coord) { } public static void resetROI(Drone drone) { - msg_command_long msg = new msg_command_long(); - msg.target_system = drone.getSysid(); - msg.target_component = drone.getCompid(); - msg.command = MAV_CMD.MAV_CMD_DO_SET_ROI; - - msg.param5 = (float) 0.0; - msg.param6 = (float) 0.0; - msg.param7 = (float) 0.0; + if(drone == null) + return; - drone.getMavClient().sendMavPacket(msg.pack()); + setROI(drone, new Coord3D(0, 0, new Altitude(0))); } public static void triggerCamera(Drone drone){ + if(drone == null) + return; + msg_digicam_control msg = new msg_digicam_control(); msg.target_system = drone.getSysid(); msg.target_component = drone.getCompid(); @@ -44,6 +45,9 @@ public static void triggerCamera(Drone drone){ } public static void empCommand(Drone drone, boolean release) { + if(drone == null) + return; + msg_command_long msg = new msg_command_long(); msg.target_system = drone.getSysid(); msg.target_component = drone.getCompid(); @@ -52,5 +56,25 @@ public static void empCommand(Drone drone, boolean release) { drone.getMavClient().sendMavPacket(msg.pack()); } + + /** + * Set a Relay pin’s voltage high or low + * @param drone target vehicle + * @param relayNumber + * @param enabled true for relay to be on, false for relay to be off. + */ + public static void setRelay(Drone drone, int relayNumber, boolean enabled){ + if(drone == null) + return; + + msg_command_long msg = new msg_command_long(); + msg.target_system = drone.getSysid(); + msg.target_component = drone.getCompid(); + msg.command = MAV_CMD.MAV_CMD_DO_SET_RELAY; + msg.param1 = relayNumber; + msg.param2 = enabled ? 1 : 0; + + drone.getMavClient().sendMavPacket(msg.pack()); + } } diff --git a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/connection/MavLinkConnection.java b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/connection/MavLinkConnection.java index 82f6e99839..f6f5bc88cb 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/connection/MavLinkConnection.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/connection/MavLinkConnection.java @@ -4,8 +4,15 @@ import com.MAVLink.Parser; import org.droidplanner.core.model.Logger; +import org.droidplanner.core.util.Pair; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; @@ -35,13 +42,25 @@ public abstract class MavLinkConnection { * ConcurrentSkipListSet because the object will be accessed from multiple * threads concurrently. */ - private final ConcurrentHashMap mListeners = new ConcurrentHashMap(); + private final ConcurrentHashMap mListeners = new ConcurrentHashMap<>(); + + /** + * Stores the list of log files to be written to. + */ + private final ConcurrentHashMap> loggingOutStreams = new + ConcurrentHashMap<>(); /** * Queue the set of packets to send via the mavlink connection. A thread * will be blocking on it until there's element(s) available to send. */ - private final LinkedBlockingQueue mPacketsToSend = new LinkedBlockingQueue(); + private final LinkedBlockingQueue mPacketsToSend = new LinkedBlockingQueue<>(); + + /** + * Queue the set of packets to log. A thread will be blocking on it until + * there's element(s) available for logging. + */ + private final LinkedBlockingQueue mPacketsToLog = new LinkedBlockingQueue<>(); private final AtomicInteger mConnectionStatus = new AtomicInteger(MAVLINK_DISCONNECTED); private final AtomicLong mConnectionTime = new AtomicLong(-1); @@ -79,17 +98,23 @@ public void run() { @Override public void run() { Thread sendingThread = null; + Thread loggingThread = null; try { final long connectionTime = System.currentTimeMillis(); mConnectionTime.set(connectionTime); reportConnect(connectionTime); - // Launch the 'Sending' threads + // Launch the 'Sending' thread mLogger.logInfo(TAG, "Starting sender thread."); sendingThread = new Thread(mSendingTask, "MavLinkConnection-Sending Thread"); sendingThread.start(); + //Launch the 'Logging' thread + mLogger.logInfo(TAG, "Starting logging thread."); + loggingThread = new Thread(mLoggingTask, "MavLinkConnection-Logging Thread"); + loggingThread.start(); + final Parser parser = new Parser(); parser.stats.mavlinkResetStats(); @@ -110,6 +135,10 @@ public void run() { sendingThread.interrupt(); } + if (loggingThread != null && loggingThread.isAlive()) { + loggingThread.interrupt(); + } + disconnect(); mLogger.logInfo(TAG, "Exiting manager thread."); } @@ -123,6 +152,7 @@ private void handleData(Parser parser, int bufferSize, byte[] buffer) { for (int i = 0; i < bufferSize; i++) { MAVLinkPacket receivedPacket = parser.mavlink_parse_char(buffer[i] & 0x00ff); if (receivedPacket != null) { + queueToLog(receivedPacket); reportReceivedPacket(receivedPacket); } } @@ -137,11 +167,11 @@ private void handleData(Parser parser, int bufferSize, byte[] buffer) { public void run() { try { while (mConnectionStatus.get() == MAVLINK_CONNECTED) { - final MAVLinkPacket packet = mPacketsToSend.take(); - byte[] buffer = packet.encodePacket(); + byte[] buffer = mPacketsToSend.take(); try { sendBuffer(buffer); + queueToLog(buffer); } catch (IOException e) { reportComError(e.getMessage()); mLogger.logErr(TAG, e); @@ -155,6 +185,61 @@ public void run() { } }; + /** + * Blocks until there's packets to log, then dispatch them. + */ + private final Runnable mLoggingTask = new Runnable() { + + @Override + public void run() { + final ByteBuffer logBuffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); + logBuffer.order(ByteOrder.BIG_ENDIAN); + + try { + while (mConnectionStatus.get() == MAVLINK_CONNECTED) { + + final byte[] packetData = mPacketsToLog.take(); + + logBuffer.clear(); + logBuffer.putLong(System.currentTimeMillis() * 1000); + + for (Map.Entry> entry : loggingOutStreams + .entrySet()) { + final Pair logInfo = entry.getValue(); + final String loggingFilePath = logInfo.first; + try { + BufferedOutputStream logWriter = logInfo.second; + if (logWriter == null) { + logWriter = new BufferedOutputStream(new FileOutputStream(loggingFilePath)); + loggingOutStreams.put(entry.getKey(), Pair.create(loggingFilePath, logWriter)); + } + + logWriter.write(logBuffer.array()); + logWriter.write(packetData); + } catch (IOException e) { + mLogger.logErr(TAG, "IO Exception while writing to " + loggingFilePath, e); + } + } + } + } catch (InterruptedException e) { + final String errorMessage = e.getMessage(); + if (errorMessage != null) + mLogger.logVerbose(TAG, errorMessage); + } finally { + for (Pair entry : loggingOutStreams.values()) { + final String loggingFilePath = entry.first; + try { + entry.second.close(); + } catch (IOException e) { + mLogger.logErr(TAG, "IO Exception while closing " + loggingFilePath, e); + } + } + + loggingOutStreams.clear(); + } + } + }; + protected final Logger mLogger = initLogger(); private Thread mConnectThread; @@ -223,11 +308,50 @@ public int getConnectionStatus() { } public void sendMavPacket(MAVLinkPacket packet) { - if (!mPacketsToSend.offer(packet)) { + final byte[] packetData = packet.encodePacket(); + if (!mPacketsToSend.offer(packetData)) { mLogger.logErr(TAG, "Unable to send mavlink packet. Packet queue is full!"); } } + private void queueToLog(MAVLinkPacket packet) { + if (packet != null) + queueToLog(packet.encodePacket()); + } + + private void queueToLog(byte[] packetData) { + if (packetData != null) { + if (!mPacketsToLog.offer(packetData)) { + mLogger.logErr(TAG, "Unable to log mavlink packet. Queue is full!"); + } + } + } + + public void addLoggingPath(String tag, String loggingPath) { + if (tag == null || tag.length() == 0 || loggingPath == null || loggingPath.length() == 0) + return; + + if (!loggingOutStreams.contains(tag)) + loggingOutStreams.put(tag, Pair.create(loggingPath, null)); + } + + public void removeLoggingPath(String tag) { + if (tag == null || tag.length() == 0) + return; + + Pair logInfo = loggingOutStreams.remove(tag); + if (logInfo != null) { + BufferedOutputStream outStream = logInfo.second; + if (outStream != null) { + try { + outStream.close(); + } catch (IOException e) { + mLogger.logErr(TAG, "IO Exception while closing " + logInfo.first, e); + } + } + } + } + /** * Adds a listener to the mavlink connection. * @@ -311,8 +435,8 @@ protected void reportComError(String errMsg) { } } - protected void reportConnecting(){ - for(MavLinkConnectionListener listener: mListeners.values()){ + protected void reportConnecting() { + for (MavLinkConnectionListener listener : mListeners.values()) { listener.onStartingConnection(); } } diff --git a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/connection/MavLinkConnectionTypes.java b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/connection/MavLinkConnectionTypes.java index 2d3e44a5c7..677007457a 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/connection/MavLinkConnectionTypes.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/MAVLink/connection/MavLinkConnectionTypes.java @@ -8,22 +8,22 @@ public class MavLinkConnectionTypes { /** * Bluetooth mavlink connection. */ - public static final int MAVLINK_CONNECTION_BLUETOOTH = 0; + public static final int MAVLINK_CONNECTION_BLUETOOTH = 3; /** * USP mavlink connection. */ - public static final int MAVLINK_CONNECTION_USB = 1; + public static final int MAVLINK_CONNECTION_USB = 0; /** * UDP mavlink connection. */ - public static final int MAVLINK_CONNECTION_UDP = 2; + public static final int MAVLINK_CONNECTION_UDP = 1; /** * TCP mavlink connection. */ - public static final int MAVLINK_CONNECTION_TCP = 3; + public static final int MAVLINK_CONNECTION_TCP = 2; public static String getConnectionTypeLabel(int connectionType){ switch(connectionType){ diff --git a/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneEvents.java b/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneEvents.java index 574367ea21..19ef6f5fef 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneEvents.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneEvents.java @@ -8,28 +8,8 @@ public class DroneEvents extends DroneVariable { - private final ConcurrentLinkedQueue eventsQueue = new ConcurrentLinkedQueue(); - - private final DroneInterfaces.Handler handler; - - private final Runnable eventsDispatcher = new Runnable() { - @Override - public void run() { - do { - handler.removeCallbacks(this); - final DroneEventsType event = eventsQueue.poll(); - if (event != null && !droneListeners.isEmpty()) { - for (OnDroneListener listener : droneListeners) { - listener.onDroneEvent(event, myDrone); - } - } - }while(!eventsQueue.isEmpty()); - } - }; - - public DroneEvents(Drone myDrone, DroneInterfaces.Handler handler) { + public DroneEvents(Drone myDrone) { super(myDrone); - this.handler = handler; } private final ConcurrentLinkedQueue droneListeners = new ConcurrentLinkedQueue(); @@ -45,7 +25,10 @@ public void removeDroneListener(OnDroneListener listener) { } public void notifyDroneEvent(DroneEventsType event) { - eventsQueue.offer(event); - handler.post(eventsDispatcher); + if (event != null && !droneListeners.isEmpty()) { + for (OnDroneListener listener : droneListeners) { + listener.onDroneEvent(event, myDrone); + } + } } } diff --git a/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneImpl.java b/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneImpl.java index 13f790dedc..8e6e0834f4 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneImpl.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneImpl.java @@ -64,10 +64,11 @@ public DroneImpl(MAVLinkStreams.MAVLinkOutputStream mavClient, DroneInterfaces.C this.MavClient = mavClient; this.preferences = pref; - events = new DroneEvents(this, handler); + events = new DroneEvents(this); state = new State(this, clock, handler, warningParser); heartbeat = new HeartBeat(this, handler); parameters = new Parameters(this, handler); + this.waypointManager = new WaypointManager(this, handler); RC = new RC(this); GPS = new GPS(this); @@ -84,7 +85,6 @@ public DroneImpl(MAVLinkStreams.MAVLinkOutputStream mavClient, DroneInterfaces.C this.navigation = new Navigation(this); this.guidedPoint = new GuidedPoint(this); this.calibrationSetup = new Calibration(this); - this.waypointManager = new WaypointManager(this); this.mag = new Magnetometer(this); this.footprints = new Camera(this); diff --git a/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneInterfaces.java b/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneInterfaces.java index 60191ba720..19313f3140 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneInterfaces.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/drone/DroneInterfaces.java @@ -251,7 +251,7 @@ public interface OnParameterManagerListener { public void onParameterReceived(Parameter parameter, int index, int count); - public void onEndReceivingParameters(List parameter); + public void onEndReceivingParameters(); } public interface OnWaypointManagerListener { @@ -262,12 +262,6 @@ public interface OnWaypointManagerListener { public void onEndWaypointEvent(WaypointManager.WaypointEvent_Type wpEvent); } - public interface OnTimeout { - - public void notifyTimeOut(int timeOutCount); - - } - public interface Clock { long elapsedRealtime(); diff --git a/dependencyLibs/Core/src/org/droidplanner/core/drone/profiles/Parameters.java b/dependencyLibs/Core/src/org/droidplanner/core/drone/profiles/Parameters.java index f70bafb86f..1a19202384 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/drone/profiles/Parameters.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/drone/profiles/Parameters.java @@ -1,8 +1,9 @@ package org.droidplanner.core.drone.profiles; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.droidplanner.core.MAVLink.MavLinkParameters; import org.droidplanner.core.drone.DroneInterfaces; @@ -26,11 +27,12 @@ */ public class Parameters extends DroneVariable implements OnDroneListener { - private static final int TIMEOUT = 1000; + private static final int TIMEOUT = 1000; //milliseconds private int expectedParams; - private final HashMap parameters = new HashMap(); + private final Map paramsRollCall = new HashMap<>(); + private final ConcurrentHashMap parameters = new ConcurrentHashMap<>(); private DroneInterfaces.OnParameterManagerListener parameterListener; @@ -42,8 +44,6 @@ public void run() { } }; - public final ArrayList parameterList = new ArrayList(); - public Parameters(Drone myDrone, Handler handler) { super(myDrone); this.watchdog = handler; @@ -52,16 +52,17 @@ public Parameters(Drone myDrone, Handler handler) { public void refreshParameters() { parameters.clear(); - parameterList.clear(); + paramsRollCall.clear(); if (parameterListener != null) parameterListener.onBeginReceivingParameters(); + MavLinkParameters.requestParametersList(myDrone); resetWatchdog(); } - public List getParametersList(){ - return parameterList; + public Map getParameters(){ + return parameters; } /** @@ -82,8 +83,9 @@ public boolean processMessage(MAVLinkMessage msg) { private void processReceivedParam(msg_param_value m_value) { // collect params in parameter list Parameter param = new Parameter(m_value); - parameters.put((int) m_value.param_index, param); + paramsRollCall.put((int) m_value.param_index, true); + parameters.put(param.name.toLowerCase(Locale.US), param); expectedParams = m_value.param_count; // update listener @@ -92,15 +94,11 @@ private void processReceivedParam(msg_param_value m_value) { // Are all parameters here? Notify the listener with the parameters if (parameters.size() >= m_value.param_count) { - parameterList.clear(); - for (int key : parameters.keySet()) { - parameterList.add(parameters.get(key)); - } killWatchdog(); myDrone.notifyDroneEvent(DroneEventsType.PARAMETERS_DOWNLOADED); if (parameterListener != null) { - parameterListener.onEndReceivingParameters(parameterList); + parameterListener.onEndReceivingParameters(); } } else { resetWatchdog(); @@ -110,7 +108,8 @@ private void processReceivedParam(msg_param_value m_value) { private void reRequestMissingParams(int howManyParams) { for (int i = 0; i < howManyParams; i++) { - if (!parameters.containsKey(i)) { + Boolean isPresent = paramsRollCall.get(i); + if (isPresent == null || !isPresent) { MavLinkParameters.readParameter(myDrone, i); } } @@ -120,23 +119,15 @@ public void sendParameter(Parameter parameter) { MavLinkParameters.sendParameter(myDrone, parameter); } - public void ReadParameter(String name) { + public void readParameter(String name) { MavLinkParameters.readParameter(myDrone, name); } public Parameter getParameter(String name) { - for (int key : parameters.keySet()) { - if (parameters.get(key).name.equalsIgnoreCase(name)) - return parameters.get(key); - } - return null; - } - - public Parameter getLastParameter() { - if (parameters.size() > 0) - return parameters.get(parameters.size() - 1); + if(name == null || name.length() == 0) + return null; - return null; + return parameters.get(name.toLowerCase(Locale.US)); } private void onParameterStreamStopped() { @@ -161,6 +152,7 @@ public void onDroneEvent(DroneEventsType event, Drone drone) { refreshParameters(); } break; + case DISCONNECTED: case HEARTBEAT_TIMEOUT: killWatchdog(); diff --git a/dependencyLibs/Core/src/org/droidplanner/core/drone/variables/State.java b/dependencyLibs/Core/src/org/droidplanner/core/drone/variables/State.java index 40b9437b5a..ace32abd63 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/drone/variables/State.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/drone/variables/State.java @@ -80,6 +80,14 @@ public void setWarning(String newFailsafe) { this.watchdog.postDelayed(watchdogCallback, failsafeOnScreenTimeout); } + public void repeatWarning(){ + if(warning == null || warning.length() == 0) + return; + + watchdog.removeCallbacks(watchdogCallback); + this.watchdog.postDelayed(watchdogCallback, failsafeOnScreenTimeout); + } + public void setArmed(boolean newState) { if (this.armed != newState) { this.armed = newState; diff --git a/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/Follow.java b/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/Follow.java index 56120c3108..f6bf18e39f 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/Follow.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/Follow.java @@ -12,6 +12,8 @@ public class Follow implements OnDroneListener, LocationReceiver { + private Location lastLocation; + /** * Set of return value for the 'toggleFollowMeState' method. */ @@ -59,8 +61,10 @@ public void toggleFollowMeState() { } private void enableFollowMe() { - followAlgorithm.enableFollow(); + lastLocation = null; + locationFinder.enableLocationUpdates(); + followAlgorithm.enableFollow(); state = FollowStates.FOLLOW_START; drone.notifyDroneEvent(DroneEventsType.FOLLOW_START); @@ -70,6 +74,8 @@ private void disableFollowMe() { followAlgorithm.disableFollow(); locationFinder.disableLocationUpdates(); + lastLocation = null; + if (isEnabled()) { state = FollowStates.FOLLOW_END; drone.notifyDroneEvent(DroneEventsType.FOLLOW_STOP); @@ -97,10 +103,11 @@ public void onDroneEvent(DroneEventsType event, Drone drone) { } @Override - public void onLocationChanged(Location location) { + public void onLocationUpdate(Location location) { if (location.isAccurate()) { state = FollowStates.FOLLOW_RUNNING; - followAlgorithm.processNewLocation(location); + lastLocation = location; + followAlgorithm.onLocationReceived(location); } else { state = FollowStates.FOLLOW_START; } @@ -108,6 +115,11 @@ public void onLocationChanged(Location location) { drone.notifyDroneEvent(DroneEventsType.FOLLOW_UPDATE); } + @Override + public void onLocationUnavailable() { + disableFollowMe(); + } + public void setAlgorithm(FollowAlgorithm algorithm) { if(followAlgorithm != null && followAlgorithm != algorithm){ followAlgorithm.disableFollow(); @@ -116,6 +128,9 @@ public void setAlgorithm(FollowAlgorithm algorithm) { followAlgorithm = algorithm; if(isEnabled()){ followAlgorithm.enableFollow(); + + if(lastLocation != null) + followAlgorithm.onLocationReceived(lastLocation); } drone.notifyDroneEvent(DroneEventsType.FOLLOW_CHANGE_TYPE); } diff --git a/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/FollowAlgorithm.java b/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/FollowAlgorithm.java index 1b7118c25e..e10fd9d0f0 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/FollowAlgorithm.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/FollowAlgorithm.java @@ -52,7 +52,7 @@ protected ROIEstimator getROIEstimator() { public final void onLocationReceived(Location location) { if (isFollowEnabled.get()) { - roiEstimator.onLocationChanged(location); + roiEstimator.onLocationUpdate(location); processNewLocation(location); } } diff --git a/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/FollowGuidedScan.java b/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/FollowGuidedScan.java index bb840bc39e..bf131bb99c 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/FollowGuidedScan.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/gcs/follow/FollowGuidedScan.java @@ -1,6 +1,6 @@ package org.droidplanner.core.gcs.follow; -import org.droidplanner.core.MAVLink.MavLinkROI; +import org.droidplanner.core.MAVLink.command.doCmd.MavLinkDoCmds; import org.droidplanner.core.drone.DroneInterfaces; import org.droidplanner.core.gcs.roi.ROIEstimator; import org.droidplanner.core.helpers.coordinates.Coord2D; @@ -77,7 +77,7 @@ public GuidedROIEstimator(Drone drone, DroneInterfaces.Handler handler) { void updateROITarget(Coord3D roiTarget){ this.roiTarget = roiTarget; - onLocationChanged(null); + onLocationUpdate(null); } @Override @@ -91,7 +91,7 @@ protected void updateROI(){ System.out.println("ROI Target: " + roiTarget.toString()); //Track the target until told otherwise. - MavLinkROI.setROI(drone, roiTarget); + MavLinkDoCmds.setROI(drone, roiTarget); watchdog.postDelayed(watchdogCallback, TIMEOUT); } } diff --git a/dependencyLibs/Core/src/org/droidplanner/core/gcs/location/Location.java b/dependencyLibs/Core/src/org/droidplanner/core/gcs/location/Location.java index 922bb81d27..6165451c3d 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/gcs/location/Location.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/gcs/location/Location.java @@ -5,7 +5,9 @@ public class Location { public interface LocationReceiver { - public void onLocationChanged(Location location); + public void onLocationUpdate(Location location); + + public void onLocationUnavailable(); } public interface LocationFinder { diff --git a/dependencyLibs/Core/src/org/droidplanner/core/gcs/roi/ROIEstimator.java b/dependencyLibs/Core/src/org/droidplanner/core/gcs/roi/ROIEstimator.java index ee115dc784..5f2dd1fc44 100644 --- a/dependencyLibs/Core/src/org/droidplanner/core/gcs/roi/ROIEstimator.java +++ b/dependencyLibs/Core/src/org/droidplanner/core/gcs/roi/ROIEstimator.java @@ -1,6 +1,6 @@ package org.droidplanner.core.gcs.roi; -import org.droidplanner.core.MAVLink.MavLinkROI; +import org.droidplanner.core.MAVLink.command.doCmd.MavLinkDoCmds; import org.droidplanner.core.drone.DroneInterfaces.Handler; import org.droidplanner.core.gcs.location.Location; import org.droidplanner.core.gcs.location.Location.LocationReceiver; @@ -40,17 +40,18 @@ public ROIEstimator(Drone drone, Handler handler) { } public void enableFollow() { + MavLinkDoCmds.resetROI(drone); isFollowEnabled.set(true); } public void disableFollow() { disableWatchdog(); isFollowEnabled.set(false); - MavLinkROI.resetROI(drone); + MavLinkDoCmds.resetROI(drone); } @Override - public final void onLocationChanged(Location location) { + public final void onLocationUpdate(Location location) { if(!isFollowEnabled.get()) return; @@ -61,6 +62,11 @@ public final void onLocationChanged(Location location) { updateROI(); } + @Override + public void onLocationUnavailable() { + disableWatchdog(); + } + protected void disableWatchdog(){ watchdog.removeCallbacks(watchdogCallback); } @@ -77,7 +83,7 @@ protected void updateROI() { * (System.currentTimeMillis() - timeOfLastLocation) / 1000f; Coord2D goCoord = GeoTools.newCoordFromBearingAndDistance(gcsCoord, bearing, distanceTraveledSinceLastPoint); if (distanceTraveledSinceLastPoint > 0.0) { - MavLinkROI.setROI(drone, new Coord3D(goCoord.getLat(), goCoord.getLng(), new Altitude(0.0))); + MavLinkDoCmds.setROI(drone, new Coord3D(goCoord.getLat(), goCoord.getLng(), new Altitude(0.0))); } watchdog.postDelayed(watchdogCallback, TIMEOUT);