package org.droidplanner.services.android.impl.core.gcs.follow; import android.os.Handler; import com.o3dr.services.android.lib.drone.action.ControlActions; import com.o3dr.services.android.lib.drone.attribute.AttributeEvent; import com.o3dr.services.android.lib.gcs.follow.FollowLocationSource; import com.o3dr.services.android.lib.model.action.Action; import org.droidplanner.services.android.impl.core.drone.DroneInterfaces.DroneEventsType; import org.droidplanner.services.android.impl.core.drone.DroneInterfaces.OnDroneListener; import org.droidplanner.services.android.impl.core.drone.autopilot.MavLinkDrone; import org.droidplanner.services.android.impl.core.drone.autopilot.apm.solo.ArduSolo; import org.droidplanner.services.android.impl.core.drone.manager.MavLinkDroneManager; import org.droidplanner.services.android.impl.core.drone.variables.GuidedPoint; import org.droidplanner.services.android.impl.core.drone.variables.State; import org.droidplanner.services.android.impl.core.gcs.location.Location; import org.droidplanner.services.android.impl.core.gcs.location.Location.LocationFinder; import org.droidplanner.services.android.impl.core.gcs.location.Location.LocationReceiver; import timber.log.Timber; public class Follow implements OnDroneListener<MavLinkDrone>, LocationReceiver { private static final String TAG = Follow.class.getSimpleName(); private Location lastLocation; private FollowLocationSource mLocationSource; /** * Set of return value for the 'toggleFollowMeState' method. */ public enum FollowStates { FOLLOW_INVALID_STATE, FOLLOW_DRONE_NOT_ARMED, FOLLOW_DRONE_DISCONNECTED, FOLLOW_START, FOLLOW_RUNNING, FOLLOW_END } private FollowStates state = FollowStates.FOLLOW_INVALID_STATE; private final MavLinkDroneManager droneMgr; private final LocationFinder locationFinder; private FollowAlgorithm followAlgorithm; private final LocationRelay mLocationRelay; public Follow(MavLinkDroneManager droneMgr, Handler handler, LocationFinder locationFinder) { this.droneMgr = droneMgr; final MavLinkDrone drone = droneMgr.getDrone(); if(drone != null) drone.addDroneListener(this); followAlgorithm = (drone instanceof ArduSolo)? FollowAlgorithm.FollowModes.SOLO_SHOT.getAlgorithmType(droneMgr, handler): FollowAlgorithm.FollowModes.LEASH.getAlgorithmType(droneMgr, handler); this.locationFinder = locationFinder; mLocationRelay = new LocationRelay(); } public void enableFollowMe(FollowLocationSource source) { if (!isEnabled()) { final MavLinkDrone drone = droneMgr.getDrone(); final State droneState = (drone != null) ? drone.getState() : null; if (droneState == null) { Timber.w("No drone for enableFollowMe(%s)", source); state = FollowStates.FOLLOW_INVALID_STATE; return; } if (droneMgr.isConnected()) { if (droneState.isArmed()) { GuidedPoint.changeToGuidedMode(drone, null); state = FollowStates.FOLLOW_START; followAlgorithm.enableFollow(); droneMgr.onAttributeEvent(AttributeEvent.FOLLOW_START, null); } else { state = FollowStates.FOLLOW_DRONE_NOT_ARMED; } } else { state = FollowStates.FOLLOW_DRONE_DISCONNECTED; } } setLocationSource(source); } public void disableFollowMe() { Timber.i("disableFollowMe(): state=%s", this.state); followAlgorithm.disableFollow(); setLocationSource(FollowLocationSource.NONE); lastLocation = null; if (isEnabled()) { state = FollowStates.FOLLOW_END; droneMgr.onAttributeEvent(AttributeEvent.FOLLOW_STOP, null); } final MavLinkDrone drone = droneMgr.getDrone(); // Send a brake command only on APM Follow, Solo Shot follow braking is handled by its Shot Manager onboard if (GuidedPoint.isGuidedMode(drone) && followAlgorithm.getType() != FollowAlgorithm.FollowModes.SOLO_SHOT) { droneMgr.getDrone().executeAsyncAction(new Action(ControlActions.ACTION_SEND_BRAKE_VEHICLE), null); } } public boolean isEnabled() { return state == FollowStates.FOLLOW_RUNNING || state == FollowStates.FOLLOW_START; } @Override public void onDroneEvent(DroneEventsType event, MavLinkDrone drone) { switch (event) { case MODE: if (isEnabled() && !GuidedPoint.isGuidedMode(drone)) { Timber.i("Follow enabled, but current mode is not guided. Disable follow"); disableFollowMe(); } break; case HEARTBEAT_TIMEOUT: case DISCONNECTED: if(isEnabled()) { disableFollowMe(); } break; } } public void onFollowNewLocation(android.location.Location location) { Timber.d("onFollowNewLocation(%s)", location); Location loc = mLocationRelay.toGcsLocation(location); if((loc != null) && (mLocationSource == FollowLocationSource.CLIENT_SPECIFIED)) { onLocationUpdate(loc); } } @Override public void onLocationUpdate(Location location) { Timber.d("onLocationUpdate(): lat/lng=%.4f/%.4f accurate=%s", location.getCoord().getLatitude(), location.getCoord().getLongitude(), location.isAccurate() ); if (location.isAccurate()) { state = FollowStates.FOLLOW_RUNNING; lastLocation = location; Timber.d("Sending location to followAlgorithm " + followAlgorithm); followAlgorithm.onLocationReceived(location); } else { Timber.d("Location not accurate"); state = FollowStates.FOLLOW_START; } droneMgr.onAttributeEvent(AttributeEvent.FOLLOW_UPDATE, null); } @Override public void onLocationUnavailable() { disableFollowMe(); } public void setAlgorithm(FollowAlgorithm algorithm) { Timber.i("setAlgorithm(): algo=" + algorithm); if(followAlgorithm != null && followAlgorithm != algorithm) { Timber.i("%s.disableFollow()", followAlgorithm); followAlgorithm.disableFollow(); } followAlgorithm = algorithm; if(isEnabled()){ Timber.i("%s.enableFollow()", followAlgorithm); followAlgorithm.enableFollow(); if(lastLocation != null) followAlgorithm.onLocationReceived(lastLocation); } droneMgr.onAttributeEvent(AttributeEvent.FOLLOW_UPDATE, null); } public FollowAlgorithm getFollowAlgorithm() { return followAlgorithm; } public FollowStates getState() { return state; } private void setLocationSource(FollowLocationSource source) { if(!isEnabled()) return; if(mLocationSource != source) { switch(source) { case CLIENT_SPECIFIED: { Timber.d("Switch to client-specified locations"); locationFinder.disableLocationUpdates(TAG); mLocationRelay.onFollowStart(); break; } case INTERNAL: { Timber.d("Switch to internal locations"); locationFinder.enableLocationUpdates(TAG, this); break; } case NONE: default:{ locationFinder.disableLocationUpdates(TAG); break; } } mLocationSource = source; } } }