Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
37
Help us understand the problem. What is going on with this article?
@araiyusuke

自動車部品メーカーのデンソーが作ったBLEリモコン、KKP(くるくるピ)の開発環境構築、接続についてまとめてみた。

More than 5 years have passed since last update.

KKP(くるくるピ)

Spin_n__Click_ポータルサイト__.png

KKP(くるくるピ)とは?

自動車部品メーカーのデンソーが作ったBLEのリモコン。車のハンドルに取り付けることでスマホをタッチせずに親指を使うことでスマホを遠隔操作できる。BLEなので通信距離は、10mぐらい、従来のBluetooth2~3よりも接続に必要な時間が短く、瞬時に繋がる。BLEで接続するため、消費電力が少ないので、電池交換は1年に1回ぐらい。くるくるピの名前の由来は、おそらくダイヤル(ロータリー)スイッチがクルクル回るから?Yahoo!カーナビも対応予定があるそうです。

kkp.png

利用シーン

スマホを手に持って操作できないシーンでくるくるピを使おう。

  • 車のハンドルに取り付けて、スマホを触らずに音楽再生やカーナビアプリを操作。
  • タブレットを見ながら、プレゼン資料をスライド操作
  • ランニング中にスマホを操作する

注意点

BLEは比較的新しい技術のため、OSや端末の相性がある。またKKPに対応したアプリでないと操作することができない。

アプリ開発者目線

面倒くさいBLEの通信処理や、ハードウェアやBLEの知識(ペリフェラルやアドバタイズ)がないアプリ開発者でも、UIEvolutionと共同開発して作られたUIE MultiAccessを使うことで、簡単にBluetoothの通信処理や、フォーカス、音声アシストをアプリに組み込むことが出来る。

iOSエミュレータ

iOSで動くエミュレータアプリがあるので、手元にKKPがなくてもUIE MultiAccessを使ったアプリのデバッグが可能かも?。iOSの端末がないので未確認です。

KKP(くるくるピ)は、どこで購入できるの?

楽天(下記URL)から購入することが出来ます。2016年2月2日現在は、税込 5,400 円(送料別)

Spin n' Click(スピン アンド クリック)

Screenshot_2016-02-02-17-08-14.jpeg

Spin n' Click(スピン アンド クリック)とは?

スマホにインストールされているアプリ一覧から、KKPでコントロールすることが出来る、KKP対応アプリ(サードパーティー製アプリ)だけをフィルタリングして、ランチャー形式で表示してくれるアプリ。もちろん、Spin n' Clickは、KKPで操作することが可能。Spin n' Clickからサードパーティのアプリを起動する際は、BLEの接続を維持したまま、シームレスに起動することが出来るのがすごい。

注意点

  • AndroidのOSが4.4以上
  • Bluetooth Low Energyに対応している端末
  • Nexus5, Nexus6, GalaxyS4, XperiaZ3, XperiaZ4

BLEは比較的新しい機能なのでOSが4.4でもBLEに未対応の機種があります。KKPを購入される際は、端末側のスペックなどをチェックして購入すること。必要であれば、メーカーに一度、確認することをオススメします。

KKP対応のアプリを作るための準備

会員登録が必要

デンソーが運営する、DENSO Connectivity Portalにて、
https://developer.navicon.com/regist/prov

ドキュメントやライブラリをダウンロード

下記リンクにアクセスして、ページの一番下にある、「Android版ライブラリ」をクリックしてダウンロードする。
https://developer.navicon.com/service/viper

ダウンロードした中身

android-adk__1_.png

デモアプリを動かす

SampleLauncher-src_denso.tbz(デモアプリ)を解凍して、Android StudioのFile→Open->SampleLauncherを選択してインポート、ビルドする。KKPとアプリが接続出来ない場合は、BluetoothがONになっているか確認すること。それでも接続できない場合は、OS側のBluetoothを一度、OFFにしてからONにすると直ります。

SampleLauncher

Screenshot_2016-02-02-17-31-37.jpeg

UIE MultiAccess

先ほどビルドしたSampleLauncherのプロジェクトファイルには、KKPと連携するのに必要なUIE MultiAccessが含まれている。SampleLauncher -> libs -> UIEMultiAccess.aar。aarは、Android Studioのライブラリ拡張子。

android-adk__1_ 2.png

Android Studioの新規プロジェクトにUIE MultiAccessを組み込む。

適当にEmpty projectを作成

注意点としてUIE MultiAccessはAndroid4.4から対応しているので、Minimum SDK4.4に合わせる。

Create_New_Project.png

UIEMultiAccess.aarをコピ

先ほど動かしたサンプルアプリSampleLauncherlibsフォルダにあるUIEMultiAccess.aarをコピーする。

android-adk__1_ 2.png

新規で作ったプロジェクトのlibsフォルダに貼り付ける。

libsを選択して、右クリックしてペースト。libsがない人はProjectになっていることを確認する。

MainActivity_java_-_KKPSample_-____Documents_デンソー_android-adk__1__KKPSample_.png

build.gradle編集

compile(name:'UIEMultiAccess', ext:'aar')を追加

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile(name:'UIEMultiAccess', ext:'aar') <-----(*´ω*)
}

libsにあるファイルを読み込む設定

repositories {
    jcenter()
    flatDir {
        dirs 'libs'
    }
}

AndroidManifest.xmlにBluetoothの権限を追加

  • uses-permission android:name="android.permission.READ_PHONE_STATE"
  • uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="araiyusuke.kkpsample">
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

MainActivity

UIE MultiAccessテスト

package araiyusuke.kkpsample;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import uie.multiaccess.app.UMAApplication;

public class MainActivity extends AppCompatActivity {
    private UMAApplication umaApplication = UMAApplication.INSTANCE;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        umaApplication.create(this);
    }
}

cannot resolve symbolエラーが。。。

Android Studio -> File -> invalidate Caches / Restartを実行すると動くよ!

スクリーンショット_2016-02-03_13_06_45.png

MainActivityにイベントリスナーを追加


import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

import uie.multiaccess.app.UMAApplication;
import uie.multiaccess.input.UMAHIDConstants;
import uie.multiaccess.input.UMAHIDInputEventListener;
import uie.multiaccess.input.UMASensorEvent;

public class MainActivity extends AppCompatActivity  implements UMAHIDInputEventListener {
    private UMAApplication umaApplication = UMAApplication.INSTANCE;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        umaApplication.create(this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        umaApplication.start(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        umaApplication.resume(this);
    }

    @Override
    protected void onPause() {
        super.onPause();
        umaApplication.pause(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        umaApplication.stop(this);
    }

    @Override
    protected void onDestroy() {
        umaApplication.destroy(this);
        super.onDestroy();
    }

    //ダイヤルが回転
    @Override
    public boolean onRotate(int distance, int direction) {
        Log.d("ダイヤル回転", "onRotate" + "distance" + ":" + distance + ":" + "direction" + ":" + direction);
        switch (direction) {
            case UMAHIDConstants.ROTATE_CLOCKWISE:
                Log.d("ダイヤル回転","時計回り");
                break;
            case UMAHIDConstants.ROTATE_ANTI_CLOCKWISE:
                Log.d("ダイヤル回転","反時計回り");
                break;
            default:
        }
        return false;
    }

    //方向キーが押された
    @Override
    public boolean onTranslate(int distanceX, int distanceY) {

        Log.d("方向キー", "onTranslate" + ":" + distanceX + ":" + distanceY);

        if (distanceX == 0 && distanceY == -1) {
            Log.d("方向キー","↑");
        } else if (distanceX == 0 && distanceY == 1) {
            Log.d("方向キー","↓");
        } else if (distanceX == -1 && distanceY == 0) {
            Log.d("方向キー","←");
        } else if (distanceX == 1 && distanceY == 0) {
            Log.d("方向キー", "→");
        }
        return false;
    }

    //ボタンを離した(ボタンを押して ~ 離すまで時間が開くと呼ばれない)
    @Override
    public boolean onPressUpButton(int button) {
        Log.d("ボタン離された", ":" + button);
        switch (button) {
            case UMAHIDConstants.BUTTON_MAIN:
                Log.d("ボタン離された","エンターボタン");
                break;
            case UMAHIDConstants.BUTTON_VR:
                Log.d("ボタン離された","マルチキー");
                break;
            case UMAHIDConstants.BUTTON_BACK:
                Log.d("ボタン離された","戻るボタン");
                break;
            case UMAHIDConstants.BUTTON_HOME:
                Log.d("ボタン離された","ホームボタン");
                break;
            default:
        }
        return false;
    }

    //ボタンが押された(方向キーは除く)
    @Override
    public boolean onPressDownButton(int button) {

        Log.d("ボタン押す", "opPressDownButton" + button);
        switch (button) {
            case UMAHIDConstants.BUTTON_MAIN:
                Log.d("ボタン押す","エンターボタン");
                break;
            case UMAHIDConstants.BUTTON_VR:
                Log.d("ボタン押す","マルチキー");
                break;
            case UMAHIDConstants.BUTTON_BACK:
                Log.d("ボタン押す","戻るボタン");
                break;
            case UMAHIDConstants.BUTTON_HOME:
                Log.d("ボタン押す","ホームボタン");
                break;
            default:
        }
        return false;
    }

    //ボタンのダブルクリック(方向キーは除く)
    @Override
    public void onDoubleClickButton(int button) {
        Log.d("ダブルクリック", ":" + button);
        switch (button) {
            case UMAHIDConstants.BUTTON_MAIN:
                Log.d("ダブルクリック","エンターボタン");
                break;
            case UMAHIDConstants.BUTTON_VR:
                Log.d("ダブルクリック","マルチキー");
                break;
            case UMAHIDConstants.BUTTON_BACK:
                Log.d("ダブルクリック","戻るボタン");
                break;
            case UMAHIDConstants.BUTTON_HOME:
                Log.d("ダブルクリック","ホームボタン");
                break;
            default:
        }
    }

    @Override
    public void onLongPressButton(int button) {

    }

    //ボタン長押し
    @Override
    public void onLongPressButton(int button, int state) {
        if (state == UMAHIDConstants.GESTURE_STATE_ENDED ) {
            Log.d("長押し", "長押し終了");
        } else if (state == UMAHIDConstants.GESTURE_STATE_BEGAN) {
            Log.d("長押し", "長押し開始");
        }

        switch (button) {
            case UMAHIDConstants.BUTTON_MAIN:
                Log.d("長押し", "エンターボタン");
                break;
            case UMAHIDConstants.BUTTON_VR:
                Log.d("長押し","マルチキー");
                break;
            case UMAHIDConstants.BUTTON_BACK:
                Log.d("長押し","戻るボタン");
                break;
            case UMAHIDConstants.BUTTON_HOME:
                Log.d("長押し","ホームボタン");
                break;
            case UMAHIDConstants.BUTTON_LEFT:
                Log.d("長押し","←");
                break;
            case UMAHIDConstants.BUTTON_UP:
                Log.d("長押し","↑");
                break;
            case UMAHIDConstants.BUTTON_DOWN:
                Log.d("長押し","↓");
                break;
            case UMAHIDConstants.BUTTON_RIGHT:
                Log.d("長押し","→");
                break;

        }
    }

    //動かない
    @Override
    public void onAccelerometerUpdate(UMASensorEvent sensor) {
        Log.d("加速度センサー", sensor.x + ":" + sensor.y + ":" + sensor.z);
    }
}


KKPのボタン確認

スクリーンショット_2016_02_02_18_44.png

イベントリスナーの詳細

KKPのボタン画像を見ながら、イベントリスナーの処理を解説していく

onRotate(int distance, int direction)

ロータリースイッチをくるくる回すことで呼び出されるイベントリスナー。引数のdirectionには、どちらの方向に回したのか向き情報が代入されている。

  • UMAHIDConstants.ROTATE_CLOCKWISE(時計回り)
  • UMAHIDConstants.ROTATE_ANTI_CLOCKWISE(反時計回り)

    //ダイヤルが回転
    @Override
    public boolean onRotate(int distance, int direction) {
        Log.d("ダイヤル回転", "onRotate" + "distance" + ":" + distance + ":" + "direction" + ":" + direction);
        switch (direction) {
            case UMAHIDConstants.ROTATE_CLOCKWISE:
                Log.d("ダイヤル回転","時計回り");
                break;
            case UMAHIDConstants.ROTATE_ANTI_CLOCKWISE:
                Log.d("ダイヤル回転","反時計回り");
                break;
            default:
        }
        return false;
    }

onPressDownButton(int button)

ボタン(方向キー除く)が押された時に呼び出されるイベントリスナ。


 //ボタンが押された(方向キーは除く)
    @Override
    public boolean onPressDownButton(int button) {

        Log.d("ボタン押す", "opPressDownButton" + button);
        switch (button) {
            case UMAHIDConstants.BUTTON_MAIN:
                Log.d("ボタン押す","エンターボタン");
                break;
            case UMAHIDConstants.BUTTON_VR:
                Log.d("ボタン押す","マルチキー");
                break;
            case UMAHIDConstants.BUTTON_BACK:
                Log.d("ボタン押す","戻るボタン");
                break;
            case UMAHIDConstants.BUTTON_HOME:
                Log.d("ボタン押す","ホームボタン");
                break;
            default:
        }
        return false;
    }

onPressUpButton(int button)

エンターボタン、戻るボタン、ホームボタン、マルチキーボタンがプッシュダウン、プッシュアップされると呼び出されるイベントリスナー。方向キーは対応していない。

  • UMAHIDConstants.BUTTON_MAIN(エンター)
  • UMAHIDConstants.BUTTON_VR(マルチキー)
  • UMAHIDConstants.BUTTON_BACK(戻る)
  • UMAHIDConstants.BUTTON_HOME(ホーム)

    //ボタンが押された(方向キーは除く)
    @Override
    public boolean onPressDownButton(int button) {

        Log.d("ボタン押す", "opPressDownButton" + button);
        switch (button) {
            case UMAHIDConstants.BUTTON_MAIN:
                Log.d("ボタン押す","エンターボタン");
                break;
            case UMAHIDConstants.BUTTON_VR:
                Log.d("ボタン押す","マルチキー");
                break;
            case UMAHIDConstants.BUTTON_BACK:
                Log.d("ボタン押す","戻るボタン");
                break;
            case UMAHIDConstants.BUTTON_HOME:
                Log.d("ボタン押す","ホームボタン");
                break;
            default:
        }
        return false;
    }

onTranslate(int distanceX, int distanceY)

方向キーが押された時に呼び出されるイベントリスナー。


    //方向キーが押された
    @Override
    public boolean onTranslate(int distanceX, int distanceY) {

        Log.d("方向キー", "onTranslate" + ":" + distanceX + ":" + distanceY);

        if (distanceX == 0 && distanceY == -1) {
            Log.d("方向キー","↑");
        } else if (distanceX == 0 && distanceY == 1) {
            Log.d("方向キー","↓");
        } else if (distanceX == -1 && distanceY == 0) {
            Log.d("方向キー","←");
        } else if (distanceX == 1 && distanceY == 0) {
            Log.d("方向キー", "→");
        }
        return false;
    }

onDoubleClickButton(int button)

ボタン(方向キーを除く)をダブルクリックした時に呼び出されるイベントリスナー。


 //ボタンのダブルクリック(方向キーは除く)
    @Override
    public void onDoubleClickButton(int button) {
        Log.d("ダブルクリック", ":" + button);
        switch (button) {
            case UMAHIDConstants.BUTTON_MAIN:
                Log.d("ダブルクリック","エンターボタン");
                break;
            case UMAHIDConstants.BUTTON_VR:
                Log.d("ダブルクリック","マルチキー");
                break;
            case UMAHIDConstants.BUTTON_BACK:
                Log.d("ダブルクリック","戻るボタン");
                break;
            case UMAHIDConstants.BUTTON_HOME:
                Log.d("ダブルクリック","ホームボタン");
                break;
            default:
        }
    }

onLongPressButton(int button, int state)

ボタンが長押しされた時に呼び出されるイベントリスナー。すべてのボタン(方向キー、エンターボタン、戻るボタン、ホームボタン、マルチキー)に対応している。引数のbuttonには、どの場所が押されたのか判別するための値が代入されている。stateには、押した時(0)、離した時(1)の情報が代入されている。


  //ボタン長押し
    @Override
    public void onLongPressButton(int button, int state) {
        if (state == UMAHIDConstants.GESTURE_STATE_ENDED ) {
            Log.d("長押し", "長押し終了");
        } else if (state == UMAHIDConstants.GESTURE_STATE_BEGAN) {
            Log.d("長押し", "長押し開始");
        }

        switch (button) {
            case UMAHIDConstants.BUTTON_MAIN:
                Log.d("長押し", "エンターボタン");
                break;
            case UMAHIDConstants.BUTTON_VR:
                Log.d("長押し","マルチキー");
                break;
            case UMAHIDConstants.BUTTON_BACK:
                Log.d("長押し","戻るボタン");
                break;
            case UMAHIDConstants.BUTTON_HOME:
                Log.d("長押し","ホームボタン");
                break;
            case UMAHIDConstants.BUTTON_LEFT:
                Log.d("長押し","←");
                break;
            case UMAHIDConstants.BUTTON_UP:
                Log.d("長押し","↑");
                break;
            case UMAHIDConstants.BUTTON_DOWN:
                Log.d("長押し","↓");
                break;
            case UMAHIDConstants.BUTTON_RIGHT:
                Log.d("長押し","→");
                break;

        }
    }

onAccelerometerUpdate(UMASensorEvent sensor)

動かない。

 //動かない
    @Override
    public void onAccelerometerUpdate(UMASensorEvent sensor) {
        Log.d("加速度センサー", sensor.x + ":" + sensor.y + ":" + sensor.z);
    }

BLE通信処理

HIDManager.isBLESupported()

AndroidのOSの設定をBluetooth Offの状態だとfalseが帰ってきた。BLE未対応の端末がないのでわかりませんが、Bluetooth4.0に対応していない場合もfalseが帰ってくるのかも。

if (HIDManager.isBLESupported()) {
   Log.d("デバッグ","BLEサポート");      

   BluetoothをONにしてくださいアラートを表示させたり
   BLEに対応していないのでアプリが使えないなどメッセージを表示する

} else {
   Log.d("デバッグ","BLE NOT サポート");
}

UMAHIDManager

接続処理は自動と手動をサポートしている。

Automatic connection モード

デバイスを発見すると自動で接続される。処理もシンプルです。

Manual connection モード

接続デバイスは、KKPだけとは限らない。他社からUIE MultiAccessに対応したデバイスが発売する可能性もあるのかもしれない。対応していないデバイスに自動で接続されるのは問題があるので、
接続先のデバイスを指定できるモードも用意されている。


 private UMAApplication umaApplication = UMAApplication.INSTANCE;
 private UMAHIDManager mHIDManager;
 private static final int CONNECTION_TIMEOUT_MS = 5000; /* 5秒 */
 private static final int DISCOVERY_TIMEOUT_MS = 10000; /* 10秒 */

 @Override
 protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    umaApplication.create(this);
    mHIDManager = (UMAHIDManager)umaApplication.getUMAService(UMAApplication.UMA_HID_SERVICE);
    mHIDManager.addHIDManagerCallback(new SampleHIDManagerCallback(new Handler(getMainLooper())));
    //手動で接続
    mHIDManager.enableManualConnection(DISCOVERY_TIMEOUT_MS);
    mHIDManager.startDiscoverDevice();
    }
    private class SampleHIDManagerCallback extends UMAHIDManagerCallback {
        private Handler mHandler;
        SampleHIDManagerCallback(Handler handler) {
            mHandler = handler;
        }
        public void onInputDeviceDiscovered(UMAInputDevice device){
            Log.d("デバッグ","onInputDeviceDiscovered" + ":" + device.getDevice().getName());
            mHIDManager.connectDevice(device, CONNECTION_TIMEOUT_MS);
        }
        public void onInputDeviceDiscoveryStarted() {
            Log.d("デバッグ","onInputDeviceDiscoveryStarted");
        }
        public void onInputDeviceDiscoveryStopped(final int stopReason) {
            Log.d("デバッグ","onInputDeviceDiscoveryStopped");
        }
        public void onInputDeviceConnected(final UMAInputDevice device) {
            Log.d("デバッグ","onInputDeviceConnected");
        }
        public void onInputDeviceFailToConnect(int error) {
            Log.d("デバッグ","onInputDeviceFailToConnect");
        }
        public void onInputDeviceDisconnected(UMAInputDevice device) {
            Log.d("デバッグ","onInputDeviceDisconnected");
        }
    }

再接続

UMAHIDManagerには、一度切断されたデバイスに再接続する機能も用意されている。前回接続したデバイスを記録しているので、discoveryせずに再接続が可能。


 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        umaApplication.create(this);
        UMAHIDManager HIDManager = (UMAHIDManager) UMAApplication.INSTANCE.getUMAService(UMAApplication.UMA_HID_SERVICE);
        if (HIDManager.isBLESupported()) {
            HIDManager.enableAutoConnection(5000, 5000, 5000);
            if (!HIDManager.connectLastConnectedDevice(5000)) {
                HIDManager.startDiscoverDevice();
            }
        }
    }

まとめ

KKP(くるくるピ)の簡単な説明、開発環境、接続について、まとめてみました。

次回

次回はデモアプリを作りながら、UIE MultiAccessのフォーカス、音声読み上げ機能を紹介します。

Screenshot_2016-02-04-18-30-05.png

37
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
araiyusuke
iOSエンジニアを目指しています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
37
Help us understand the problem. What is going on with this article?