Flypad Usage Example (Android)


#1

Looking for info on how to integrate the Flypad into your personal application? Here’s a working example:

First we need to establish an interface:

package com.example.interfaces;

import com.example.utility.FlypadInfo;
/**
 * Parrot Flypad Listener Interface - SMS - 11/23/17
 */
public interface FlypadListener {

    void onFlypadConnected();
    void onFlypadDisconnected();

    void onFlypadBatteryLevelChanged(final short batteryLevel);

    void onFlypadAxisValuesChanged(final float leftX, final float leftY, final float rightX, final float rightY);

    void onFlypadButtonChanged(final FlypadInfo.FlypadButton button, final FlypadInfo.ONOFF_SWITCH state);
}

#2

Next we need a value object for keeping track of all the various aspects of the Flypad. This VO exposes a default set of Axis and Button mappings, button and axis values, and other metadata related to the Flypad:

package com.example.utility;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

import com.example.interfaces.FlypadListener;

import java.util.ArrayList;

import static com.example.utility.FlypadInfo.FlypadButton.LEFT_THUMB;
import static com.example.utility.FlypadInfo.FlypadButton.RIGHT_THUMB;
/**
 * Parrot Flypad Value Object - SMS - 11/23/17
 */
public class FlypadInfo {

    private static final String CLASS_NAME = FlypadInfo.class.getSimpleName();
    private static boolean DEBUG = true;

    public enum ONOFF_SWITCH {
        DISABLED,
        ENABLED,
        UNKNOWN
    }

    public enum FlypadButton {
        LEFT_THUMB,
        RIGHT_THUMB,
        ONE,
        TWO,
        A,
        B,
        UP_DOWN,
        LEFT_TOP,
        RIGHT_TOP,
        LEFT_BOTTOM,
        RIGHT_BOTTOM
    }

    public enum FlypadAxis {
        LEFT_X,
        LEFT_Y,
        RIGHT_X,
        RIGHT_Y
    }

    public enum FlypadButtonAction {
        NO_ACTION,              // 0
        EMERGENCY,              // 1
        TAKEOFF_OR_LAND,        // 2
        OPEN_SETTINGS,          // 3
        RECORD_VIDEO,           // 4
        TAKE_PICTURE,           // 5
        ANIMATION,              // 6
        FLAT_TRIM,              // 7
        GO_HOME,                // 8
        TOGGLE_HOVER_LOCK,      // 9
        TOGGLE_MAP,             // 10
        TOGGLE_BANKED_TURNS,    // 11
        TOGGLE_TRACK_ME,        // 12
        TOGGLE_HEAD_MOVEMENT,   // 13
        CENTER_FIELD_OF_VIEW,   // 14
        CHANGE_HOME_TYPE,       // 15
        ZOOM_IN,                // 16
        ZOOM_OUT                // 17

    }

    public enum FlypadAxisAction {
        NO_ACTION,              // 0
        ROLL,                   // 1
        PITCH,                  // 2
        YAW,                    // 3
        GAZ,                    // 4
        CAMERA_PAN,             // 5
        CAMERA_TILT             // 6
    }


    public class FlypadButtonMapping {
        private FlypadButton button;
        private FlypadButtonAction action;

        public FlypadButtonMapping(final FlypadButton button, final FlypadButtonAction action) {
            this.button = button;
            this.action = action;
        }

        public FlypadButton getButton() {
            return button;
        }

        public FlypadButtonAction getAction() {
            return action;
        }

        public String getKey() {
            return "FLYPAD_" + button.name();
        }

        public String getTitle() {
            return toProper(button.name()).replace("_", " ");
        }
    }

    public class FlypadAxisMapping {
        private FlypadAxis axis;
        private FlypadAxisAction action;

        public FlypadAxisMapping(final FlypadAxis axis, final FlypadAxisAction action) {
            this.axis = axis;
            this.action = action;
        }

        public FlypadAxis getAxis() {
            return axis;
        }

        public FlypadAxisAction getAction() {
            return action;
        }

        public String getKey() {
            return "FLYPAD_" + axis.name();
        }

        public String getTitle() {
            return toProper(axis.name()).replace("_", " ");
        }
    }

    private final Context ctx;

    private String name;
    private String serial;
    private String hardwareVersion;
    private String firmwareVersion;
    private String softwareVersion;

    private short batteryLevel;

    private short axisLeftY;
    private short axisLeftX;
    private short axisRightY;
    private short axisRightX;

    private boolean buttonLeftThumb;
    private boolean buttonRightThumb;

    private boolean buttonOne;
    private boolean buttonTwo;

    private boolean buttonUpDown;

    private boolean buttonA;
    private boolean buttonB;

    private boolean buttonLeftTop;
    private boolean buttonLeftBottom;
    private boolean buttonRightTop;
    private boolean buttonRightBottom;

    private ArrayList<FlypadAxisMapping> axisMappings;
    private ArrayList<FlypadButtonMapping> buttonMappings;

    private ArrayList<FlypadListener> flypadListeners;

    public FlypadInfo(final Context ctx) {
        this(ctx, null);
    }
    public FlypadInfo(final Context ctx, final ArrayList<FlypadListener> flypadListeners) {
        this.ctx = ctx;
        this.flypadListeners = flypadListeners;

        axisMappings = buildAxisMappings();
        buttonMappings = buildButtonMappings();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if (!name.equals(this.name)) {
            this.name = name;
            if (DEBUG) Log.i(CLASS_NAME, "name=" + name);
        }
    }

    public String getSerial() {
        return serial;
    }

    public void setSerial(String serial) {
        if (!serial.equals(this.serial)) {
            this.serial = serial;
            if (DEBUG) Log.i(CLASS_NAME, "serial=" + serial);
        }
    }

    public String getHardwareVersion() {
        return hardwareVersion;
    }

    public void setHardwareVersion(String hardwareVersion) {
        if (!hardwareVersion.equals(this.hardwareVersion)) {
            this.hardwareVersion = hardwareVersion;
            if (DEBUG) Log.i(CLASS_NAME, "hardwareVersion=" + hardwareVersion);
        }
    }

    public String getFirmwareVersion() {
        return firmwareVersion;
    }

    public void setFirmwareVersion(String firmwareVersion) {
        if (!firmwareVersion.equals(this.firmwareVersion)) {
            this.firmwareVersion = firmwareVersion;
            if (DEBUG) Log.i(CLASS_NAME, "firmwareVersion=" + firmwareVersion);
        }
    }

    public String getSoftwareVersion() {
        return softwareVersion;
    }

    public void setSoftwareVersion(String softwareVersion) {
        if (!softwareVersion.equals(this.softwareVersion)) {
            this.softwareVersion = softwareVersion;
            if (DEBUG) Log.i(CLASS_NAME, "softwareVersion=" + softwareVersion);
        }
    }

    public short getBatteryLevel() {
        return batteryLevel;
    }

    public void setBatteryLevel(short batteryLevel) {
        if (batteryLevel != this.batteryLevel) {
            this.batteryLevel = batteryLevel;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadBatteryLevelChanged(batteryLevel);
            }

            if (DEBUG) Log.i(CLASS_NAME, "batteryLevel=" + batteryLevel);
        }
    }

    public void setAxes(final short axisLeftX, final short axisLeftY, final short axisRightX, final short axisRightY) {

        boolean changed = false;

        if (axisLeftX != this.axisLeftX) {
            this.axisLeftX = axisLeftX;
            changed = true;
            if (DEBUG) Log.i(CLASS_NAME, "axisLeftX=" + axisLeftX);
        }

        if (axisLeftY != this.axisLeftY) {
            this.axisLeftY = axisLeftY;
            changed = true;
            if (DEBUG) Log.i(CLASS_NAME, "axisLeftY=" + axisLeftY);
        }

        if (axisRightX != this.axisRightX) {
            this.axisRightX = axisRightX;
            changed = true;
            if (DEBUG) Log.i(CLASS_NAME, "axisRightX=" + axisRightX);
        }

        if (axisRightY != this.axisRightY) {
            this.axisRightY = axisRightY;
            changed = true;
            if (DEBUG) Log.i(CLASS_NAME, "axisRightY=" + axisRightY);
        }

        if (changed) {
            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadAxisValuesChanged(this.axisLeftX * .01f, this.axisLeftY * .01f, this.axisRightX * .01f, this.axisRightY * .01f);
            }
        }
    }

    public void setButtons(final boolean buttonA,
                           final boolean buttonB,
                           final boolean buttonUpDown,
                           final boolean buttonOne,
                           final boolean buttonTwo,
                           final boolean buttonLeftBottom,
                           final boolean buttonRightBottom,
                           final boolean buttonLeftTop,
                           final boolean buttonRightTop,
                           final boolean buttonLeftThumb,
                           final boolean buttonRightThumb) {

        if (buttonA != this.buttonA) {
            this.buttonA = buttonA;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(FlypadButton.A, buttonA ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonA=" + buttonA);
        }

        if (buttonB != this.buttonB) {
            this.buttonB = buttonB;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(FlypadButton.B, buttonB ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonB=" + buttonB);
        }

        if (buttonUpDown != this.buttonUpDown) {
            this.buttonUpDown = buttonUpDown;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(FlypadButton.UP_DOWN, buttonUpDown ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonUpDown=" + buttonUpDown);
        }

        if (buttonOne != this.buttonOne) {
            this.buttonOne = buttonOne;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(FlypadButton.ONE, buttonOne ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonOne=" + buttonOne);
        }

        if (buttonTwo != this.buttonTwo) {
            this.buttonTwo = buttonTwo;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(FlypadButton.TWO, buttonTwo ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonTwo=" + buttonTwo);
        }

        if (buttonLeftBottom != this.buttonLeftBottom) {
            this.buttonLeftBottom = buttonLeftBottom;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(FlypadButton.LEFT_BOTTOM, buttonLeftBottom ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonLeftBottom=" + buttonLeftBottom);
        }

        if (buttonRightBottom != this.buttonRightBottom) {
            this.buttonRightBottom = buttonRightBottom;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(FlypadButton.RIGHT_BOTTOM, buttonRightBottom ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonRightBottom=" + buttonRightBottom);
        }

        if (buttonLeftTop != this.buttonLeftTop) {
            this.buttonLeftTop = buttonLeftTop;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(FlypadButton.LEFT_TOP, buttonLeftTop ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonLeftTop=" + buttonLeftTop);
        }

        if (buttonRightTop != this.buttonRightTop) {
            this.buttonRightTop = buttonRightTop;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(FlypadButton.RIGHT_TOP, buttonRightTop ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonRightTop=" + buttonRightTop);
        }

        if (buttonLeftThumb != this.buttonLeftThumb) {
            this.buttonLeftThumb = buttonLeftThumb;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(LEFT_THUMB, buttonLeftThumb ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonLeftThumb=" + buttonLeftThumb);
        }

        if (buttonRightThumb != this.buttonRightThumb) {
            this.buttonRightThumb = buttonRightThumb;

            for (FlypadListener flypadListener : flypadListeners) {
                flypadListener.onFlypadButtonChanged(RIGHT_THUMB, buttonRightThumb ? ONOFF_SWITCH.ENABLED : ONOFF_SWITCH.DISABLED);
            }

            if (DEBUG) Log.i(CLASS_NAME, "buttonRightThumb=" + buttonRightThumb);
        }
    }

    public ArrayList<FlypadAxisMapping> getAxisMappings() {
        return axisMappings;
    }

    public ArrayList<FlypadButtonMapping> getButtonMappings() {
        return buttonMappings;
    }

    public FlypadAxisMapping getAxisMappingByAxis(final FlypadAxis axis) {
        for (FlypadAxisMapping mapping : axisMappings) {
            if (mapping.getAxis() == axis) {
                return mapping;
            }
        }

        return null;
    }

    public FlypadButtonMapping getButtonMappingByButton(final FlypadButton button) {
        for (FlypadButtonMapping mapping : buttonMappings) {
            if (mapping.getButton() == button) {
                return mapping;
            }
        }

        return null;
    }

    public void refreshMappings() {
        axisMappings = buildAxisMappings();
        buttonMappings = buildButtonMappings();
    }

    private ArrayList<FlypadButtonMapping> buildButtonMappings() {
        final ArrayList<FlypadButtonMapping> mappings = new ArrayList<>();
        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);

        for (FlypadButton button : FlypadButton.values()) {
            final String buttonName = "FLYPAD_" + button.name();
            final String actionName = prefs.getString(buttonName, getFlypadButtonDefaultActionValue(buttonName));

            for (FlypadButtonAction action : FlypadButtonAction.values()) {
                if (action.name().equals(actionName)) {
                    mappings.add(new FlypadButtonMapping(button, action));
                    break;
                }
            }
        }

        return mappings;
    }

    private ArrayList<FlypadAxisMapping> buildAxisMappings() {
        final ArrayList<FlypadAxisMapping> mappings = new ArrayList<>();
        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);

        for (FlypadAxis axis : FlypadAxis.values()) {
            final String axisName = "FLYPAD_" + axis.name();
            final String actionName = prefs.getString(axisName, getFlypadAxisDefaultActionValue(axisName));

            for (FlypadAxisAction action : FlypadAxisAction.values()) {
                if (action.name().equals(actionName)) {
                    mappings.add(new FlypadAxisMapping(axis, action));
                    break;
                }
            }
        }

        return mappings;
    }

    public static String[] getFlypadButtonEntries(final boolean formatted) {

        final ArrayList<String> entries = new ArrayList<>();

        for (FlypadButton button : FlypadButton.values()) {
            entries.add(formatted ? toProper(button.name().replace("_", " ")) : button.name());
        }

        return entries.toArray(new String[entries.size()]);
    }

    public static String[] getFlypadAxisEntries(final boolean formatted) {

        final ArrayList<String> entries = new ArrayList<>();

        for (FlypadAxis axis : FlypadAxis.values()) {
            entries.add(formatted ? toProper(axis.name().replace("_", " ")) : axis.name());
        }

        return entries.toArray(new String[entries.size()]);
    }


    public static String[] getFlypadButtonActionEntries(final boolean formatted) {

        final ArrayList<String> entries = new ArrayList<>();

        for (FlypadButtonAction action : FlypadButtonAction.values()) {
            entries.add(formatted ? toProper(action.name().replace("_", " ")) : action.name());
        }

        return entries.toArray(new String[entries.size()]);
    }


    public static String[] getFlypadAxisActionEntries(final boolean formatted) {

        final ArrayList<String> entries = new ArrayList<>();

        for (FlypadAxisAction action : FlypadAxisAction.values()) {
            entries.add(formatted ? toProper(action.name().replace("_", " ")) : action.name());
        }

        return entries.toArray(new String[entries.size()]);
    }

    public static String getFlypadButtonDefaultActionValue(final String buttonName) {

        FlypadButton button = RIGHT_THUMB;

        for (FlypadButton btn : FlypadButton.values()) {
            if (btn.name().equals(buttonName)) {
                button = btn;
                break;
            }
        }

        switch (button) {
            case LEFT_THUMB:
                return FlypadButtonAction.values()[11].name();
            case RIGHT_THUMB:
                return FlypadButtonAction.values()[9].name();
            case ONE:
                return FlypadButtonAction.values()[10].name();
            case TWO:
                return FlypadButtonAction.values()[3].name();
            case A:
                return FlypadButtonAction.values()[5].name();
            case B:
                return FlypadButtonAction.values()[4].name();
            case UP_DOWN:
                return FlypadButtonAction.values()[2].name();
            case LEFT_TOP:
                return FlypadButtonAction.values()[14].name();
            case RIGHT_TOP:
                return FlypadButtonAction.values()[12].name();
            case LEFT_BOTTOM:
                return FlypadButtonAction.values()[15].name();
            case RIGHT_BOTTOM:
                return FlypadButtonAction.values()[6].name();
            default:
                return FlypadButtonAction.values()[0].name();
        }
    }

    public static String getFlypadAxisDefaultActionValue(final String axisName) {
        FlypadAxis axis = FlypadAxis.LEFT_X;

        for (FlypadAxis ax : FlypadAxis.values()) {
            if (ax.name().equals(axisName)) {
                axis = ax;
                break;
            }
        }

        switch (axis) {
            case LEFT_X:
                return FlypadAxisAction.values()[3].name();
            case LEFT_Y:
                return FlypadAxisAction.values()[4].name();
            case RIGHT_X:
                return FlypadAxisAction.values()[1].name();
            case RIGHT_Y:
                return FlypadAxisAction.values()[2].name();
            default:
                return FlypadAxisAction.values()[0].name();
        }
    }

    private static String toProper(String s) {

        final String ACTIONABLE_DELIMITERS = " '-/_"; // these cause the character following to be capitalized

        StringBuilder sb = new StringBuilder();
        boolean capNext = true;

        for (char c : s.toCharArray()) {
            c = (capNext)
                    ? Character.toUpperCase(c)
                    : Character.toLowerCase(c);
            sb.append(c);
            capNext = (ACTIONABLE_DELIMITERS.indexOf((int) c) >= 0); // explicit cast not needed
        }
        return sb.toString();
    }
}

#3

Now that we have an interface for describing events and a value object for representing various properties we need a helper class for managing connectivity between the Flypad and the device:

package com.example.utility;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.Log;

import com.example.interfaces.FlypadListener;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
/**
 * Parrot Flypad Helper - SMS - 11/23/17
 */
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class FlypadHelper {

    private static final String CLASS_NAME = FlypadHelper.class.getSimpleName();
    private static DEBUG = true;

    // indicates the state of our service:
    public enum State {
        SCANNING,
        CONNECTING,
        CONNECTED,
        DISCONNECTED,
        UNKNOWN
    }

    private final static UUID PRIMARY_SERVICE_UUID = UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb");
    private final static UUID CONTROL_SURFACE_INFO_UUID = UUID.fromString("9e35fa00-4344-44d4-a2e2-0c7f6046878b");

    private final static UUID CONTROL_SURFACE_NOTIFY_UUID = UUID.fromString("9e35fa01-4344-44d4-a2e2-0c7f6046878b");
    private final static UUID CONTROL_SURFACE_NOTIFY_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    private final static UUID SERIAL_NUMBER = UUID.fromString("00002a25-0000-1000-8000-00805f9b34fb");
    private final static UUID HARDWARE_VERSION = UUID.fromString("00002a27-0000-1000-8000-00805f9b34fb");
    private final static UUID FIRMWARE_VERSION = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb");
    private final static UUID SOFTWARE_VERSION = UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb");

//    private final static UUID BATTERY_STATUS_UUID = UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");

    private final Context ctx;
    private final BluetoothAdapter bluetoothAdapter;

    private BluetoothGatt bluetoothGatt;
    private BluetoothGattCharacteristic notifyCharacteristic;
    private BluetoothGattDescriptor notifyDescriptor;

    private State state = State.DISCONNECTED;

    private List<BluetoothGattCharacteristic> characteristics;

    private ArrayList<FlypadListener> flypadListeners = new ArrayList<>();
    private FlypadInfo flypadInfo;

    public FlypadHelper(final Context ctx) {
        if (DEBUG) Log.i(CLASS_NAME, "Constructor");

        this.ctx = ctx;
        flypadInfo = new FlypadInfo(ctx, flypadListeners);

        final BluetoothManager bluetoothManager = (BluetoothManager) ctx.getSystemService(Context.BLUETOOTH_SERVICE);
        bluetoothAdapter = bluetoothManager.getAdapter();
    }
    
    public void destroy() {
        if (DEBUG) Log.i(CLASS_NAME, "destroy");

        stopLeScan();

        if (bluetoothAdapter != null && bluetoothAdapter.isEnabled() && bluetoothGatt != null) {
            if (notifyCharacteristic != null && notifyDescriptor != null) {
                if (DEBUG) Log.d(CLASS_NAME, "disabling notifications");

                bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, false);
                notifyDescriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
                bluetoothGatt.writeDescriptor(notifyDescriptor);
            }

            if (DEBUG) Log.d(CLASS_NAME,"deallocating gatt");
            bluetoothGatt.disconnect();
            bluetoothGatt.close();
        }
    }

    public FlypadInfo getFlypadInfo() {
        return flypadInfo;
    }

    public State getState() { return state; }

    public void startLeScan() {
        if (bluetoothAdapter != null && bluetoothAdapter.isEnabled() && state == State.DISCONNECTED) {
            bluetoothAdapter.startLeScan(leScanCallback);
            state = State.SCANNING;
        }
    }

    public void stopLeScan() {
        if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {
            bluetoothAdapter.stopLeScan(leScanCallback);
            if (state == State.SCANNING) state = State.DISCONNECTED;
        }
    }

    public synchronized boolean addFlypadListener(FlypadListener flypadListener) {
        return !flypadListeners.contains(flypadListener) && flypadListeners.add(flypadListener);
    }

    public synchronized boolean removeFlypadListener(FlypadListener flypadListener) {
        return flypadListeners.remove(flypadListener);
    }

    private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {

            if (device != null && device.getName() != null && device.getName().toLowerCase().contains("flypad")) {
                final String msg = String.format(Locale.US, "Found %s - %s",device.getName(), device.getAddress());
                Log.i(CLASS_NAME, msg);

                flypadInfo.setName(device.getName());

                bluetoothAdapter.stopLeScan(leScanCallback);

                state = State.CONNECTING;

                bluetoothGatt = device.connectGatt(ctx, false, btleGattCallback);
            }
        }
    };

    private final BluetoothGattCallback btleGattCallback = new BluetoothGattCallback() {

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicRead(gatt, characteristic, status);

            final String value = new String(characteristic.getValue());

            if (characteristic.getUuid().equals(SERIAL_NUMBER)) flypadInfo.setSerial(value);
            if (characteristic.getUuid().equals(HARDWARE_VERSION)) flypadInfo.setHardwareVersion(value);
            if (characteristic.getUuid().equals(SOFTWARE_VERSION)) flypadInfo.setSoftwareVersion(value);
            if (characteristic.getUuid().equals(FIRMWARE_VERSION)) flypadInfo.setFirmwareVersion(value);

            characteristics.remove(0);

            if (characteristics.size() > 0) {
                gatt.readCharacteristic(characteristics.get(0));
            } else {
                if (DEBUG) Log.d(CLASS_NAME, "enabling notifications");

                final BluetoothGattService service = gatt.getService(CONTROL_SURFACE_INFO_UUID);

                notifyCharacteristic = service.getCharacteristic(CONTROL_SURFACE_NOTIFY_UUID);
                notifyDescriptor = notifyCharacteristic.getDescriptor(CONTROL_SURFACE_NOTIFY_DESCRIPTOR_UUID);

                bluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true);
                notifyDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                bluetoothGatt.writeDescriptor(notifyDescriptor);

                state = State.CONNECTED;

                for (FlypadListener flypadListener : flypadListeners) {
                    flypadListener.onFlypadConnected();
                }
            }
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);

            final byte[] response = characteristic.getValue();

            flypadInfo.setBatteryLevel(response[0]);
            flypadInfo.setAxes(decodeAxis(response[5]), decodeAxis(response[6]), decodeAxis(response[3]), decodeAxis(response[4]));
            
            flypadInfo.setButtons((response[1] & 16) == 16,
                                  (response[1] & 8) == 8,
                                  (response[1] & 1) == 1, 
                                  (response[1] & 2) == 2,
                                  (response[1] & 4) == 4,
                                  (response[2] & 1) == 1, 
                                  (response[1] & 64) == 64,
                                  (response[1] & 128) == 128,
                                  (response[1] & 32) == 32,
                                  (response[2] & 2) == 2,
                                  (response[2] & 4) == 4);
        }

        private short decodeAxis(final byte value) {
           if (value >  -1) {
               return (short) ((128 - value) * -1 * 0.78125f);
           } else {
               return (short) ((128 + value) * 0.78740157480315f);
           }
        }

        @Override
        public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
            super.onConnectionStateChange(gatt, status, newState);

            if (DEBUG) Log.d(CLASS_NAME, "onConnectionStateChange newState=" + newState);

            if (newState == BluetoothProfile.STATE_CONNECTED) {
                bluetoothGatt.discoverServices();
            }

            if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                state = State.DISCONNECTED;

                for (FlypadListener flypadListener : flypadListeners) {
                    flypadListener.onFlypadDisconnected();
                }
            }
        }

        @Override
        public void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
            super.onServicesDiscovered(gatt, status);

            if (DEBUG) Log.d(CLASS_NAME, "onServicesDiscovered");

//            for (BluetoothGattService service : bluetoothGatt.getServices()) {
//                if (DEBUG) {
//                    Log.d(CLASS_NAME, "service=" + service.getUuid().toString() + " type=" + service.getType());
//                    for (BluetoothGattCharacteristic bgc : service.getCharacteristics()) {
//                        Log.d(CLASS_NAME, "     characteristic=" + bgc.getUuid());
//                        for (BluetoothGattDescriptor desc : bgc.getDescriptors()) {
//                            Log.d(CLASS_NAME, "          descriptor=" + desc.getUuid());
//                        }
//                    }
//                }
//            }

            final BluetoothGattService service = gatt.getService(PRIMARY_SERVICE_UUID);
            if (service == null) return;

            characteristics = service.getCharacteristics();

            gatt.readCharacteristic(characteristics.get(0));
        }

        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);

            if (DEBUG) Log.d(CLASS_NAME, "onDescriptorWrite");
        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);

            if (DEBUG) Log.d(CLASS_NAME, "onCharacteristicWrite");
        }
    };
}

#4

And then bring it all together with simple activity:

package com.example.activities;

import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.Log;

import com.example.interfaces.FlypadListener;
import com.example.utility.FlypadHelper;
import com.example.utility.FlypadInfo;

import java.util.Locale;
/**
 * Parrot Flypad Activity Example - SMS - 11/23/17
 */
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class ExampleActivity extends Activity
                             implements FlypadListener {


    private static String CLASS_NAME = "ExampleActivity";
    private static boolean DEBUG = false;

    private FlypadHelper helper = null;
    private FlypadInfo info = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        if (DEBUG) Log.d(CLASS_NAME, "onCreate");

        helper = new FlypadHelper(this);
        info = helper.getFlypadInfo();

        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onDestroy() {
        if (DEBUG) Log.d(CLASS_NAME, "onDestroy");
        super.onDestroy();

        helper.destroy();
    }

    @Override
    protected void onResume() {
        if (DEBUG) Log.d(CLASS_NAME, "onResume");
        super.onResume();

        info.refreshMappings();
        helper.addFlypadListener(this);

        if (helper.getState() != FlypadHelper.State.CONNECTED) {
            Log.i(CLASS_NAME, "Starting LE scan. . .");
            helper.startLeScan();
        }
    }

    @Override
    protected void onPause() {
        if (DEBUG) Log.d(CLASS_NAME, "onPause");
        super.onPause();

        helper.removeFlypadListener(this);
    }

    @Override
    public void onFlypadConnected() {
        if (DEBUG) Log.d(CLASS_NAME, "onFlypadConnected");

        Log.i(CLASS_NAME, "Flypad name=" + info.getName());

        for (FlypadInfo.FlypadAxisMapping mapping : info.getAxisMappings()) {
            Log.i(CLASS_NAME, mapping.getAxis() + " mapped to " + mapping.getAction());
        }

        for (FlypadInfo.FlypadButtonMapping mapping : info.getButtonMappings()) {
            Log.i(CLASS_NAME, mapping.getButton() + " mapped to " + mapping.getAction());
        }
    }

    @Override
    public void onFlypadDisconnected() {
        if (DEBUG) Log.d(CLASS_NAME, "onFlypadDisconnected");

        Log.i(CLASS_NAME, "Starting LE scan. . .");
        helper.startLeScan();
    }

    @Override
    public void onFlypadBatteryLevelChanged(short batteryLevel) {
        if (DEBUG) Log.d(CLASS_NAME, "onFlypadBatteryLevelChanged");

        Log.i(CLASS_NAME, "Battery level=" + batteryLevel);
    }

    @Override
    public void onFlypadAxisValuesChanged(float leftX, float leftY, float rightX, float rightY) {
        if (DEBUG) Log.d(CLASS_NAME, "onFlypadAxisValuesChanged");

        Log.i(CLASS_NAME, String.format(Locale.US, "lx=%.2f ly=%.2f rx=%.2f ry=%.2f", leftX, leftY, rightX, rightY));
    }

    @Override
    public void onFlypadButtonChanged(FlypadInfo.FlypadButton button, FlypadInfo.ONOFF_SWITCH state) {
        if (DEBUG) Log.d(CLASS_NAME, "onFlypadButtonChanged");

        Log.i(CLASS_NAME, button.name() + " " + state.name());
    }
}

With sample output:

11-30 19:25:08.408 5487-5487/~ I/ExampleActivity: Starting LE scan. . .
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: Flypad name=FLYPAD_221915

11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: LEFT_X mapped to CAMERA_PAN
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: LEFT_Y mapped to CAMERA_TILT
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: RIGHT_X mapped to ROLL
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: RIGHT_Y mapped to PITCH

11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: LEFT_THUMB mapped to TOGGLE_BANKED_TURNS
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: RIGHT_THUMB mapped to TOGGLE_HOVER_LOCK
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: ONE mapped to TOGGLE_MAP
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: TWO mapped to OPEN_SETTINGS
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: A mapped to TAKE_PICTURE
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: B mapped to RECORD_VIDEO
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: UP_DOWN mapped to TAKEOFF_OR_LAND
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: LEFT_TOP mapped to CENTER_FIELD_OF_VIEW
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: RIGHT_TOP mapped to TOGGLE_TRACK_ME
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: LEFT_BOTTOM mapped to ZOOM_OUT
11-30 19:25:42.566 5487-5500/~ I/ExampleActivity: RIGHT_BOTTOM mapped to ZOOM_IN

11-30 19:25:42.661 5487-5503/~ I/ExampleActivity: Battery level=80
11-30 19:25:44.953 5487-5529/~ I/ExampleActivity: Battery level=70

11-30 19:26:19.284 5487-5514/~ I/ExampleActivity: lx=0.04 ly=0.00 rx=0.00 ry=0.00
11-30 19:26:19.325 5487-5518/~ I/ExampleActivity: lx=0.09 ly=0.00 rx=0.00 ry=0.00
11-30 19:26:19.377 5487-5542/~ I/ExampleActivity: lx=0.11 ly=0.00 rx=0.00 ry=0.00
11-30 19:26:19.422 5487-5500/~ I/ExampleActivity: lx=0.14 ly=0.00 rx=0.00 ry=0.00
11-30 19:26:19.472 5487-5511/~ I/ExampleActivity: lx=0.18 ly=0.00 rx=0.00 ry=0.00
11-30 19:26:19.572 5487-5529/~ I/ExampleActivity: lx=0.21 ly=0.00 rx=0.00 ry=0.00
11-30 19:26:19.957 5487-5529/~ I/ExampleActivity: lx=0.07 ly=0.00 rx=0.00 ry=0.00
11-30 19:26:20.058 5487-5503/~ I/ExampleActivity: lx=0.00 ly=0.00 rx=0.00 ry=0.00

11-30 19:26:34.931 5487-5501/~ I/ExampleActivity: A ENABLED
11-30 19:26:35.411 5487-5501/~ I/ExampleActivity: A DISABLED

#5

Now, if anyone can share the characteristic, descriptor, and write values for getting its rumble function to operate we can close the book and have this thing fully mapped out.

Shell


#7

Thank you @synman, very very useful indeed!
Any chance to have the same for iOS?
Thanks!


#8

Nope, but all the BLE specs should transfer over.