SesameSDKをFlutterから触ってみて 〜後編〜

[前回](https://qiita.com/ume-bar/items/fc18d205c0f3bc4fa77f)の続きになります。`java`をアンインストールした後`Kotlin`をインストールして、`MainActivity`も`.kt`の拡張子になりました(プロジェクト作成時にLanguage切替)。前回は`MainActivity.java`の状態でしたが、途中からでもVScodeのプラグインから変えられますのでお調べ下さい。
![スクリーンショット 2022-03-10 15.38.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1134006/2ea47546-ee96-2f95-38cd-9621d1fb1e67.png)
`SDK`は`Flutter`から直接さわることが出来ないので`Flutter`に用意されている`MethodChannel`という通信メソッドを利用してプラットフォーム接続してネイティブコードを呼び出し対応します。
![スクリーンショット 2022-03-10 16.49.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1134006/e367afd4-5fc5-44ea-3e7d-dd7e6877c838.png)
[SESAMEのDocument](https://doc.candyhouse.co/ja?v=a515e892d876c64520162d1acea70664a4697b7c)がいつの間にかリニューアルしていて、`Sesame3`だけでなく`Sesame4`まで対応するようになりました。ものすごくわかりやすくなっていたのでご覧ください。

# MainActivity.kt
アプリをビルドして実行すると実装したActivityのインスタンスが起動してレイアウトが読み込まれます。この中で`MethodChannel`を書いていきます。
## MethodCall型
`Flutter`側で呼び出すときに渡される情報で`arguments`と`method`プロパティを持つ。`.method`の値に応じて処理を切り替えたりできます。
## MethodChannel.Result型
`Flutter`側へのレスポンスに使用。処理が成功したら`success`失敗したら`error`を返す設定ができる。

```app/src/main/kotlin/com/example/../MainActivity.kt

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.hoge/sesame"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        flutterEngine
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
// メソッドチャンネルはconfigureFlutterEngineメソッドをoverrideして設定していく。
// setMethodCallHandlerメソッドはチャネルでメソッド呼び出しを受信するためのコールバックを設定。
            call, result ->
            println("abc")
            if (call.method == "getBatteryLevel") {
                val batteryLevel = getBatteryLevel()

                if (batteryLevel != -1) {
                    result.success(batteryLevel)
// result.success()の引数がFlutter側で取得できる(batteryLevel)
// result.error()の引数がレスポンスされる
                } else {
                    result.error("UNAVAILABLE", "Battery level not available.", null)
                }
            } else if (call.method == "connectSSM") {
                connectSSM(result)
// 接続
            } else if (call.method == "lock") {
                        _lock(botTemp)
// 施錠
            } else if (call.method == "unlock") {
                        _unlock(botTemp)
// 解錠
　　　　　　　　　　　　　　　　　　　　　　　　} else if (call.method == "bleEnable") {
                val res = bleEnable()
                if (res == "success") {
                    result.success(res)
                } else {
                    result.error("UNAVAILABLE", res, null)
                }
　　　　　　　　　　　　　　　　　　　　　　　　} else if (call.method == "bleDisable") {
                val res = bleDisable()
                if (res == "success") {
                    result.success(res)
                } else {
                    result.error("UNAVAILABLE", res, null)
                }
            } else {
                result.notImplemented()  
// Dart側に未実装であることを伝える
            }
        }
    }

    private fun registeredCheckDivergence(result: MethodChannel.Result){
        CHDeviceManager.getCandyDevices{
// 登録済のセサミオブジェクトを取り出す
            it.onSuccess{
                System.out.println("get already register devices: " + it.data)
                if(it.data.size > 0){
                    it.data.forEach{
                        stateDivergence(it, true)
                    }
                } else{
                    register(result)
// 登録
                }
            }
            it.onFailure{
                System.out.println("get device faile")
                result.error("UNAVAILABLE", it.toString(), null)
            }
        }
    }

    var unRegisterDeviceList = ArrayList<CHDevices>()
    var alreadyfindDeviceId: String? = null
    private fun register(result: MethodChannel.Result) {
        CHBleManager.delegate = object : 
// 未登録デバイスリストのdelegateイベントを受け取る
CHBleManagerDelegate {
            override fun didDiscoverUnRegisteredCHDevices(devices: List<CHDevices>) {
                unRegisterDeviceList.clear()
                unRegisterDeviceList.addAll(devices)
                System.out.println("unregister devices: " + devices)
                if(unRegisterDeviceList.size > 0 && alreadyfindDeviceId != unRegisterDeviceList.firstOrNull()?.deviceId.toString()){
                    System.out.println("deviceId: " + alreadyfindDeviceId + " -> only first time stateDivergence()")
                    stateDivergence(unRegisterDeviceList.first(), false)
                    alreadyfindDeviceId = unRegisterDeviceList.firstOrNull()?.deviceId.toString()
                }else{
                    System.out.println("already register")
                }
            }
        }
    }

    // 新しい登録と登録済みデバイスの接続
    private fun connectSSM(result: MethodChannel.Result){
        var message: String = ""
        CHBleManager(applicationContext)
        registeredCheckDivergence(result)
    }

//BLEスキャンのスイッチをオンする
    private fun bleEnable(): String {
        var message: String = ""
        CHBleManager(applicationContext)
        CHBleManager.enableScan {
            it.onSuccess{
                message = "success"
            }
            it.onFailure{
                message = it.toString()
            }
        }
        return message
    }
//BLEスキャンのスイッチをオフする
    private fun bleDisable(): String {
        var message: String = ""
        CHBleManager(applicationContext)
        CHBleManager.disableScan {
            it.onSuccess{
                message = "success"
            }
            it.onFailure{
                message = it.toString()
            }
        }
        return message
    }

    var lockPosition: Short?  = 0;
    var unLockPosition: Short?  = 0;

    private fun setPosition(bot: CHSesame2, isLock: Boolean): String {
        var message: String = ""
        if(bot.mechStatus?.position == null)return message
        if(isLock){
            lockPosition = bot.mechStatus?.position
        }else{
            unLockPosition = bot.mechStatus?.position
        }
        bot.configureLockPosition(lockPosition!!, unLockPosition!!) {
            it.onSuccess {
                System.out.println("success configure")
            }
            it.onFailure{
                System.out.println("fail configure")
            }
        }
        return message
    }

   
    private fun getBatteryLevel(): Int {
       

        CHBleManager(applicationContext)
// 登録済の セサミオブジェクト を取り出す
        CHDeviceManager.getCandyDevices{
            it.onSuccess{
                System.out.println("get already register devices: " + it.data)
                System.out.println(it.data)
                stateDivergence(it.data.first(), true)
                System.out.println("############")

            }
            it.onFailure{
                System.out.println("get device faile")
            }
        }

// 未登録の新規デバイスがあるかを探索(unRegisterDeviceListへの追加) 
        CHBleManager.delegate = object : CHBleManagerDelegate {
            override fun didDiscoverUnRegisteredCHDevices(devices: List<CHDevices>) {
                System.out.println("find unregister device")
                unRegisterDeviceList.clear()
                unRegisterDeviceList.addAll(devices)
                System.out.println("unregister devices: " + devices)
            }
        }

        System.out.println(unRegisterDeviceList)
// 一度statedivergenceを行って入ればそれ以降は呼ばなくてよいため、UUIDで選別（デバッグ時のデバイスは一つのためリストではなくID一つを保持）
// stateが変わった時に一度呼び出される、state毎の関数の設定（デバイスごとに設定）
// 最初の一回目のボタンタップでは空振りdidDiscoverUnRegisteredCHDevicesがタイミング的にreturn 22のあとに実行されているためalreadyfindDeviceIdがnullの状態のままになっている
// 2回目のボタンタップで新規登録をする。（ボタンが一個のため同じボタンを再利用）
        if(unRegisterDeviceList.size > 0 && alreadyfindDeviceId != unRegisterDeviceList.firstOrNull()?.deviceId.toString()){
            System.out.println("deviceId: " + alreadyfindDeviceId + " -> only first time stateDivergence()")
            stateDivergence(unRegisterDeviceList.first(), false)
            alreadyfindDeviceId = unRegisterDeviceList.firstOrNull()?.deviceId.toString()
        }else{
            System.out.println("already register")
        }
        ////

        return 22
    }

    private fun _connect(bot: CHSesame2){
        bot.connect{
            it.onSuccess{
                System.out.println("connect")
            }
            it.onFailure{
                System.out.println("faile connect: " + it)
            }
        }
    }

    private fun _lock(bot: CHSesame2){
        bot.lock{
            it.onSuccess{
                System.out.println("success lock")
                System.out.println(bot.deviceStatus)
            }
            it.onFailure{
                System.out.println("faile lock: " + it)
                
            }
        }
    }
    private fun _unlock(bot: CHSesame2){
        bot.unlock{ //ここ以外_lockと同じ
            it.onSuccess{
                System.out.println("success unlock")
                System.out.println(bot.deviceStatus)
            }
            it.onFailure{
                System.out.println("faile unlock" + it)
            }
        }
    }
　　　　　　　　private fun _register(bot: CHSesame2){
        System.out.println("register prev status" + bot.deviceStatus)
        bot.register {
            it.onSuccess {
                System.out.println("success register")
            }
            it.onFailure{
                System.out.println("faile register")
            }
        }
    }

    private fun _doRegister(bot: CHSesame2){
        bot.configureLockPosition(256, 0) {
            it.onSuccess {
                System.out.println("success configure")
            }
            it.onFailure{
                System.out.println("fail configure")
            }
        }
    }

    lateinit var botTemp: CHSesame2

    private fun stateDivergence(devices: CHDevices, alreadyRegister: Boolean){
        System.out.println(devices)
        var bot = (devices as CHSesame2)
        _connect(bot)
        botTemp = bot
        CHBleManager.delegate = null 
// デバッグのため未登録のデバイスを探すのを止める
        bot.delegate = object : CHSesame2Delegate {
            override fun onBleDeviceStatusChanged(
                device: CHSesameLocker,
                status: CHSesame2Status,
                shadowStatus: CHSesame2ShadowStatus?
            ) {
                System.out.println("change state: " + bot.deviceStatus)
                if (bot.deviceStatus == CHSesame2Status.ReadyToRegister) {
                    System.out.println("state redyToRegister")
                    _register(bot)
                }
                if (bot.deviceStatus == CHSesame2Status.NoSettings) {
                    System.out.println("state noSettings")
                    _doRegister(bot)
                }
                if(bot.deviceStatus == CHSesame2Status.NoBleSignal){
                    System.out.println("state noBleSignal")
                }
            }
        }
    }
}
```
## build.gradle
作成したプログラムをビルドし実行するためのファイル
```android/app/build.gradle
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.gms.google-services'

android {
    compileSdkVersion 31
// 更新時、チェックポイント１
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    defaultConfig {
// 独自のアプリケーションIDを指定(https://developer.android.com/studio/build/application-id.html).
        applicationId "com.hoge"
        minSdkVersion 21
// 更新時、チェックポイント２
        targetSdkVersion 30
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        multiDexEnabled true
    }
// buildTypeまたはproductFlavor毎に異なるsigningConfigを設定する
    buildTypes {
        release {
// リリースビルド用に独自の署名設定を追加
// デバッグキーで署名しているので、flutterrun --releaseが機能
            signingConfig signingConfigs.debug
        }
    }
}
```
## Flutter側で操作するボタンのコード
こちらを使用すれば`SESAME`に接続して解錠・施錠ができます。メソッドチャンネル作成後に`invokeMethod`を非同期でコールすることで、`Kotlin`を呼び出すことが出来きます。
```../hoges/sesame.dart
ElevatedButton(
    child: Text('接続'),　onPressed: () async {
     　　const platform =　MethodChannel('com.hoge/sesame');
      String message;
      try {
        await platform.invokeMethod('bleEnable');
        message = 'BLE Enable proc: success';
        try {
          final String result =　await platform.invokeMethod('connectSSM');
          message = 'Connect proc: ' + result;
          } on PlatformException catch (e) {
            message = "Failed to Connect: '${e.message}'.";
          }
            } on PlatformException catch (e) {
              message = "Failed to BLE Enable: '${e.message}'.";
           }
           print(message);
     },
),
          
ElevatedButton(
    child: Text('解錠'), onPressed: () async {
      const platform = MethodChannel('com.hoge/sesame');
      try {
        await platform.invokeMethod('unlock');
          } on PlatformException catch (e) {
            "Failed to get battery level: '${e.message}'.";
          }
     },
),
         
ElevatedButton(
    child: Text('施錠'), onPressed: () async {
      const platform = MethodChannel('com.hoge/sesame');
      try {
        await platform.invokeMethod('lock');
          } on PlatformException catch (e) {
          print(e);
          }
    },
),
```
こちらは解錠・施錠位置に使用するMethodCallです。
![スクリーンショット 2022-03-11 14.07.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1134006/b9b2c5d1-9a15-2cf9-a192-c1d823452c15.png)

```MainActivity.kt
} else if (call.method == "setPosition"){
                 val res = setPosition(botTemp, call.arguments())
                if (res == "success") {
                    result.success(res)
                } else {
                    result.error("UNAVAILABLE", res, null)
                } 
```
SESAME3を使って記事を作成し始めた時に、SESAME4が出てしまって最初はSDKが対応してなかったのでどう頑張ってもできませんでしたが、ようやく`Flutter`で解錠と施錠ができるようになったので記事が完成できました。