LoginSignup
6
4

More than 3 years have passed since last update.

AndroidとSesameとNFCで開けゴマ BLE版

Last updated at Posted at 2020-05-10

修正(2020/05/12):CradIDの型を指定、カードIDの等価式を修正

目的

家の鍵を「かざ」すだけでして開けたい!
→改良しました、、、https://qiita.com/sakujira/items/73e7438b7fe62d4f7a13

・Sesame
https://jp.candyhouse.co
・NFCカード + 電波遮断シート
 玄関のドアは金属性なので、台紙にしないと反応しないので
 (100均に電波遮断シートが売ってることにびっくりした、、、
・Androidスマホ

考え方

色んな開け方を考える
・Felicaを機器かざしてにかざして開ける
  機器の電源が必要になる
・スマホが近くに寄ったら開ける
  1m単位で位置確認が必要
・スマホをノックして開ける
  ノックの定義を正確にするのは難しい

個人的にどれも理想的ではなくて、本当に「かざす」だけにしたい、、、

「解錠・施錠が高速化! おサイフケータイでスマートロックを操る」
https://k-tai.watch.impress.co.jp/docs/column/minna/1240577.html

という方法もありますが、セキュリティの部分で怖い。

「あ、でも、NFCを読んだら、SesameのAPP画面で開ける様にすればOKなのでは?」
「しかし、別アプリから画面を操作はOSがさせてくれないかも?」
「あ、BLEで操作している人がいる、、、」

スマートロックのSesami miniをnodeでBluetooth連携する
https://qiita.com/odetarou/items/9628d66d4d94290b5f2d
OpenSesame: Reverse Engineering BLE Lock
https://itsze.ro/blog/2016/12/18/opensesame-reverse-engineering-ble-lock.html

「であれば、家の玄関に貼ったNFCタグをスマホで読み込んで、BLEとかAPIから開けるようにすればいいじゃない?」

ということで、実際に作って開けてみた!

方法

1.AndroidでNFCを読み込み→ネット→公開APIで解錠(別記事
WEBAPI.png
2.AndroidでNFCを読み込み→BLEで解錠(本記事)
BLE.png

成果物

事前準備として、
NXP TagWriter
https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter&hl=ja
にて、書込み可能なタグに対して
 「Plain Text」で適当な文字列
を書き込んで置いてください。(A1に影響)

※バグ取りが出来てなくてアプリが固まることがあるのでご注意ください
AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.myapplication3">

    <!-- APP起動に必要な権限 -->
    <uses-permission android:name="android.permission.NFC" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        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>

        <activity-alias
            android:name=".MainActivityAlias"
            android:enabled="true"
            android:targetActivity=".MainActivity">

            <!-- A1.NFC card からのアプリ起動 -->
            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>
        </activity-alias>
    </application>
</manifest>

MainActivity.kt

package com.example.myapplication3

//UIのためのおまじない
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
//NFCのため
import android.nfc.NfcAdapter
//BLEのため
import android.bluetooth.*
import android.content.Context
import android.content.pm.PackageManager
//Byte変換のため
import kotlin.math.floor
import java.util.*
//暗号化のため
import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

class MainActivity : AppCompatActivity() {
    //ユーザー情報
    val CardID : String = -鍵にしたいCardID-
    val UserID : String  = -Sesameのメールアドレス-
    val Password : String  = -公式APPから抜き出したパスワード-
    val BLEaddress : String = -SesameのBLEアドレス-
    val manufactreDataMacDataString : String  = -SesameのBluetoothMacアドレス?結局最後までわからず-

    //ここから下はアプリケーション用変数
    //施錠・解錠判定用(APIのように状態を教えてくれないので角度から判断する必要があるので
    val LockMinAngle = 10;
    val LockMaxAngle = 270;

    //SesameがもつBLEのサービス検索詞
    val ServiceOperationUuid : UUID          = UUID.fromString("00001523-1212-efde-1523-785feabcd123")
    val CharacteristicCommandUuid : UUID     = UUID.fromString("00001524-1212-efde-1523-785feabcd123")
    val CharacteristicStatusUuid : UUID      = UUID.fromString("00001526-1212-efde-1523-785feabcd123")
    val CharacteristicAngleStatusUuid : UUID = UUID.fromString("00001525-1212-efde-1523-785feabcd123")

    //サービス検索結果:各サービスへの接続詞
    var CharStatus:BluetoothGattCharacteristic? = null
    var CharCmd:BluetoothGattCharacteristic? = null
    var CharAngle:BluetoothGattCharacteristic? = null

    //状態管理用の変数
    var RunState : Boolean = false      //監視ループを管理
    var ConnectState : Boolean = false      //BLEとの接続を管理
    var SesameState : Int = 0           //Sesame側のカウンターを管理
    var CommandState : Int = 0          //次のコマンドを何を投げるかを管理
    var LockState : Int = 0         //開けるか・閉めるか・状態を聞くかを管理


    //コールバッククラス?(と呼べばいいのか?):BLEを使ってのSesameからの返信受付
    @ExperimentalUnsignedTypes
    private val mGattcallback: BluetoothGattCallback = object : BluetoothGattCallback() {
        //B3.Sesameへの接続確認
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
            super.onConnectionStateChange(gatt, status, newState)
            //接続確立を確認して
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                //サービスの検索を開始
                println("Btest:GattSa-Search")
                gatt?.discoverServices()
            }
        }

        //B4.サービスの検索完了:結果を分析
        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
            super.onServicesDiscovered(gatt, status)

            //サービスの検索が成功を確認して
            if(status == BluetoothGatt.GATT_SUCCESS) {
                println("Btest:GattSa-OK!")

                //サービスの一覧表を取得
                val GattSList: List<BluetoothGattService> =
                    gatt?.services as List<BluetoothGattService>
                //サービスの一覧表を走査
                for (GaService: BluetoothGattService in GattSList) {
                    println("Btest:>" + GaService.uuid.toString())

                    //事前にserviceOperationUuidと一致したものがあったら、
                    if (GaService.uuid.equals(ServiceOperationUuid)) {
                        //サービスが持っている機能・情報の一覧を取得
                        val GattCList: List<BluetoothGattCharacteristic> = GaService.characteristics
                        //機能・情報の一覧の走査
                        for (GaCharacteristic: BluetoothGattCharacteristic in GattCList) {
                            println("Btest:>>" + GaCharacteristic.uuid.toString())
                            //Sesameの状態情報取得の接続詞を取得
                            if (GaCharacteristic.uuid.equals(CharacteristicStatusUuid)){
                                CharStatus = GaCharacteristic
                            }
                            //Sesameのコマンド情報取得の接続詞を取得
                            if (GaCharacteristic.uuid.equals(CharacteristicCommandUuid)){
                                CharCmd = GaCharacteristic
                            }
                            //Sesameの角度情報取得の接続詞を取得
                            if (GaCharacteristic.uuid.equals(CharacteristicAngleStatusUuid)){
                                CharAngle = GaCharacteristic
                            }
                        }
                    }
                }
                //走査した結果が全てあるかどうかをチェックし、メインスレッドを次の状態に移行
                ConnectState = !(CharStatus == null || CharCmd == null || CharAngle == null)
            }
        }

        //接続詞を使っての読み込み依頼した結果を分析
        override fun onCharacteristicRead(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
            super.onCharacteristicChanged(gatt, characteristic)

            if (characteristic != null) {
                println("Btest:" + characteristic.uuid.toString())
                when (characteristic.uuid) {
                    //B9.依頼内容が「AngleStatus」であれば
                    CharacteristicAngleStatusUuid -> {
                        val data: ByteArray = characteristic.value          //データを取得
                        println("Btest:" + ByteArrayToString(data))         //データをFF:FF形式で表示
                        val angleRaw = ByteArrayToInt(data.slice(2..3).toByteArray())   //データを切り出して、Byte→Intへ
                        val angle = floor(angleRaw * 360 / 1024.0)          //角度を計算

                        //LockStateは、操作コマンドと兼ねているため 1:解錠(操作:施錠) 2:施錠(操作:解錠)と逆になる
                        LockState = 1;
                        if (angle < LockMinAngle || angle > LockMaxAngle) {
                            LockState = 2;
                        }
                        println("Btest:Byte:" + ByteArrayToString(data) + ", Angle:" + angle + ", LockStatus:" + (LockState==2))
                        CommandState += 1                       //メインスレッドを次の状態に移行
                    }
                    //B6.B12.依頼内容が「Status」であれば
                    CharacteristicStatusUuid -> {
                        val data: ByteArray = characteristic.value              //データの取得
                        val Sn: Int = ByteArrayToInt(data.slice(6..9).toByteArray()) + 1    //Sesameカウンターを取得
                        val Err: Int = ByteArrayToInt(data.slice(14..14).toByteArray()) + 1 //エラーコードを取得
                        //エラーコードリスト
                        val errMsg = arrayOf(
                            "Timeout",
                            "Unsupported",
                            "Success",
                            "Operating",
                            "ErrorDeviceMac",
                            "ErrorUserId",
                            "ErrorNumber",
                            "ErrorSignature",
                            "ErrorLevel",
                            "ErrorPermission",
                            "ErrorLength",
                            "ErrorUnknownCmd",
                            "ErrorBusy",
                            "ErrorEncryption",
                            "ErrorFormat",
                            "ErrorBattery",
                            "ErrorNotSend"
                        )
                        println("Btest:Byte:" + ByteArrayToString(data) + ", Sn:" + Sn + ", Err:" + errMsg[Err])
                        SesameState = Sn    //Sesameカウンタを記録
                        CommandState += 1   //メインスレッドを次の状態に移行
                    }
                }
            }
        }
    }
    //ここまでSesameからの通信受付処理

    //ここからアプリを開いた際の動き
    @ExperimentalUnsignedTypes
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //このアプリを開く[起因]はNFC情報を読み取り
        //A1.[起因]は、AndroidManifest.xmlに規定した<intent-filter>に起因する
        if(NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) {
            //カードのID情報を取得
            val tagId : ByteArray = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID)
            val list = ArrayList<String>()
            for(byte in tagId) {
                list.add(String.format("%02X", byte.toInt() and 0xFF))
            }

            println("Btest" + list.joinToString(separator = ":"))
            //A2.読み込んだカードIDが一致すれば、BLE操作を開始
            if(!RunState && list.joinToString(separator = ":") == CardID)){
                //状態関係の変数を初期化
                RunState = true
                ConnectState = false
                SesameState = 0
                CommandState = 0
                LockState = 0

                //BLE機能をオン!
                BLE_Sesame()
            }
        }
    }

    //BLEでのSesameへのアクセスメソッド
    @ExperimentalUnsignedTypes
    fun BLE_Sesame(){
        //B1.BLEの接続に必要なクラスを宣言
        var adapter: BluetoothAdapter? = null
        var device: BluetoothDevice? = null
        var mGatt: BluetoothGatt? = null

        //BLE対応端末かどうかを調べる。対応していない場合は終了
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            finishAndRemoveTask()
        }

        //Bluetoothアダプターを初期化する
        val manager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        adapter = manager.adapter
        if(adapter == null){
            finishAndRemoveTask()
        }

        //BLE端末一覧を検索せずに事前に調べたSesameに直接接続するように設定
        println("Btest:Start-Connect")
        device = adapter.getRemoteDevice(BLEaddress)

        println("Btest:Start-GattScan")
        if(device == null){
            finishAndRemoveTask()
        }
        //B2.Sesameに接続を実行
        mGatt = device.connectGatt(this, false, mGattcallback)

        //Sesameからの通信結果を待ちながら処理を進めて行く
        while(RunState) {
            if(ConnectState){
                when (CommandState){
                    //B5.Sesameの状態取得:Sesameカウントを取得
                    0->{
                        println("Btest:GetStatus")
                        CommandState = 1
                        mGatt.readCharacteristic(CharStatus)
                    }
                    //B7.LockState:0を送信しAngleを検知させる
                    2->{
                        println("Btest:SendData-1:")
                        CommandState = 3
                        //B7-1.各パラメータから送信データを作成
                        val PayLoad = CreateSign(LockState,"", Password, manufactreDataMacDataString, UserID, SesameState)
                        //B7-2.データをSesameに送信する
                        CharCmd?.let { ControlSesame(mGatt, it,PayLoad) }
                    }
                    4->{
                        //B8.角度を取得し、施錠・解錠を取得
                        println("Btest:GetRange")
                        CommandState = 5
                        mGatt.readCharacteristic(CharAngle)
                    }
                    6->{
                        //B10.施錠・解錠コマンドを送信
                        println("Btest:SendData-2")
                        CommandState = 7
                        val PayLoad = CreateSign(LockState,"", Password, manufactreDataMacDataString, UserID, SesameState)
                        CharCmd?.let { ControlSesame(mGatt, it,PayLoad) }
                    }
                    8->{
                        //B11.Sesameの状態取得(なくてもいいのですが、一応)
                        println("Btest:GetStatus")
                        CommandState = 9
                        mGatt.readCharacteristic(CharStatus)
                    }
                    10->{
                        //B13.ループを終えて
                        println("Btest:EndConnect!")
                        RunState = false
                    }
                }
            }
            Thread.sleep(500)
        }
        //B14.アプリを終わらせます
        println("Btest-END?")
        finishAndRemoveTask()
    }

    //B7-2.B10-2.Sesameに対してデータを送信(BLEの20バイト制約のため、分割して送信)
    fun ControlSesame(pGatt:BluetoothGatt, pChar : BluetoothGattCharacteristic, pPayload : ByteArray){
        var wFlag : Boolean = true

        //送信は20バイトごとに送信
        //分割する際には、[先頭:01][中間:02][最後:04]と付ける必要がある
        //なので、一回の送信は19バイトごと
        for(i in 0..pPayload.size step 19){
            val wSz = Math.min(pPayload.size-i,19)      //送るデータが最後かどうか?
            var wCc : Int = 2                   //初期値は[中間:02]とする
            var wBuf : ByteArray  = ByteArray(wSz+1)        //送信用データ場所を作成

            //先頭・最後を判定
            if(wSz < 19){
                wCc = 4
            }else if(i == 0){
                wCc = 1
            }

            //バイト列に分割詞をつける
            wBuf[0] = wCc.toByte()
            //送信データからバイト列を切り出し
            wBuf = ByteArrayCopy(wBuf, 1, pPayload, i,wSz)
            println("Btest:SendData:" + ByteArrayToString(wBuf))

            //Cmdの接続詞にデータをセット
            if(pChar.setValue(wBuf)) {
                //Sesameに対してデータを送信
                if (pGatt.writeCharacteristic(pChar)) {
                    Thread.sleep(200)               //連続して送るとNGがでるので(高速化したい、、、
                } else{
                    println("Btest:SnedNG")
                    wFlag = false               //通信で一つでもかければ、この回でのアプリを終了
                }
            }
        }

        if(wFlag){
            println("Btest:ALLOK!" + CommandState)
            CommandState += 1                   //メインスレッドを次の状態に移行
        }else{
            RunState = false                    //アプリ終了処理
            println("Btest:NG")
        }
    }

    //B7-1.B10-1.認証用バイトデータを作成(普段Byteを使わないから、符号あり・なしに振り回された、、、)
    fun CreateSign(pCode:Int, pPayload: String, pPassword : String, pMacData : String, pUserid : String, pNonce: Int) : ByteArray{
        //バイト配列の場所を作成
        var wBufnonPw : ByteArray = ByteArray(59 - 32 + pPayload.toByteArray().size)

        //manufactreDataのデータをコピー
        val ByteMacData : ByteArray = HexStringToByteArray(pMacData)
        println("Btest:Mac:" + ByteArrayToString(ByteMacData.sliceArray(3..ByteMacData.size-1)))
        wBufnonPw = ByteArrayCopy(wBufnonPw, 0, ByteMacData.sliceArray(3..ByteMacData.size-1),0,6)

        //md5のデータをコピー
        val md5 = MessageDigest.getInstance("MD5").digest(pUserid.toByteArray())
        println("Btest:md5:" + ByteArrayToString(md5))
        wBufnonPw = ByteArrayCopy(wBufnonPw,6,md5,0,16)

        //Statsu(Nonce)をコピー
        println("Btest:Nonce:" + ByteArrayToString(InttoByteArrayUnsign(pNonce)))
        wBufnonPw = ByteArrayCopy(wBufnonPw,22,InttoByteArrayUnsign(pNonce),0,4)

        //Codeをコピー
        println("Btest:Code:" + ByteArrayToString(InttoByteArrayUnsign(pCode)))
        wBufnonPw = ByteArrayCopy(wBufnonPw,26,InttoByteArrayUnsign(pCode),0,1)

        //Payloadをコピー
        wBufnonPw = ByteArrayCopy(wBufnonPw, 27, pPayload.toByteArray(),0, pPayload.toByteArray().size)

        //パラメータの結果を確認
        println("Btest:PrameterOK!:" + ByteArrayToString(wBufnonPw))

        //「生成したパラメータ」を「パスワード」を使って暗号化
        //「パスワード」を使用する暗号機を作成
        val key = SecretKeySpec(HexStringToByteArray(pPassword), "HmacSHA256")
        val mac = Mac.getInstance("HmacSHA256")
        mac.init(key)
        val wBufKey = mac.doFinal(wBufnonPw)
        println("Btest:wBufkey:" + ByteArrayToString(wBufKey))

        //全部を連結
        var wBuf : ByteArray = ByteArray(pPayload.toByteArray().size + 59)
        wBuf = ByteArrayCopy(wBuf,0, wBufKey,0, 32)
        wBuf = ByteArrayCopy(wBuf,32, wBufnonPw,0, wBufnonPw.size)
        println("Btest:ALL:" + ByteArrayToString(wBuf))

        return  wBuf
    }

    //Intを符号なしのバイト列に変換
    fun InttoByteArrayUnsign(num : Int): ByteArray{
        val wHexString : String = num.toString(16).padStart(12,'0')//文字埋めを12桁にしているのはByteArrayCopyで参照値外がないようにするため
        val wResult = HexStringToByteArray(wHexString)
        return  wResult.reversedArray() //1101→03F3となるが、送信データ上ではF303と逆にする必要がある
    }

    //HEX文字列をバイト配列にキャスト
    fun HexStringToByteArray(pHexString: String): ByteArray {
        val wBArray = ByteArray(pHexString.length / 2)
        for (index in 0 until wBArray.count()) {
            val pointer = index * 2
            wBArray[index] = pHexString.substring(pointer, pointer + 2).toInt(16).toByte()
        }
        return wBArray
    }

    //Byte配列を指定位置にコピー
    fun ByteArrayCopy(pTarget: ByteArray, pPosition: Int, pCopy: ByteArray, pStart: Int, pLength : Int):ByteArray{
        for(i in 0 until pLength){
            pTarget[pPosition + i] = pCopy[pStart + i]
        }
        return pTarget
    }

    //Byte配列を文字列化
    fun ByteArrayToString(pBytes: ByteArray): String{
        var wRsult : String = ""
        for (b in pBytes) {
            wRsult += String.format("%02X", b) + ":"
        }
        return wRsult
    }

    //Byte配列を数値化
    @ExperimentalUnsignedTypes
    fun ByteArrayToInt(pBytes: ByteArray): Int {
        var wResult: Int = 0
        for (b in pBytes.reversed()){
            wResult = wResult shl 8
            wResult += b.toUByte().toInt()
        }
        return wResult
    }
}

動作フロー

・スマホにNFCタグをかざす
A1.Androidのタグディスパッチシステムにより、アプリが起動
A2.onCreate内でそのままIDを読み込み
・NFCからBLEへ操作が移り変わる
B1.BLEの初期化
(BLEのスキャン処理はこのアプリではしない)
B2.機器への接続
B3.機器への接続確立
B4.サービスへの接続詞の確保
B5.Sesameの状態を取得依頼
B6.Sesameの状態を取得:Sesameの実行カウントを取得
B7.データを送信
  これをしないと直ぐに切断されるらしい(先人ページにて)
  また角度情報は送信データが一度でもないと貰えないみたい
B8.角度情報の取得依頼
B9.角度情報の取得:鍵の施錠・解錠を取得
B10.Sesameへの施錠・解錠を依頼
B11.Sesameの状態を取得依頼
B12.Sesameの状態を取得:コマンドの実行完了を確認
B13.BLEの処理終了
B14.アプリを閉じて、またNFCでアプリを起動できる状態へ

導入したい方へ(いるか分かりませんが、、、

・必要なモノ
 ・PC
  ・[HD]Bluethooth環境
  ・[SW]NODE環境
   @odetarou様のNode版を実行して
   [BLEaddress]と[manufactreDataMacDataString]を取得するため
   (Androidだけで[manufactreDataMacDataString]に当たるものが見つけられなかったので
  ・[SW]JAVAの実行環境
  ・[SW]apktool2.3系列
  ・[SW]テキストエディタ
  ・[SW]AndroidStduio(SDK:keytoolとjarsigner)
   @offmon様の記事どおりに、[Password]を抜くために
 ・スマホ(当たり前ですねw
  ・[設定]開発者向けオプション:USBデバックをON
  ・[SW]NXP TagWriter
   NDEFレコードに書き込みしておく必要があるので、、、
   (使わなくなったnanacoとかでするならタグ編集出来ないので、プログラムを修正する必要があるかと?下手すればそのスマホで”ICカードの残額確認”が出来ないかも?

 ・書き込み可能なNFCカード/タグ
  自分はFelicaタイプのカードにしました
  (実験のためにシールタイプを買ったら剥がしたら、アンテナ線が切れたので硬派に、、、

 ・スマホとのUSBケーブル
  Logcatでパスワードを見る必要があるので

 ・Sesame
 ・SesameAPP
  これがないとこの記事は成り立ちませんw

・手順
1.公式アプリからパスワードを取得
  @offmon様の記事どおりに、[Password]を抜くために 
2.NODE版でBLEのアドレス値を取得
  @odetarou様のNode版を実行して[BLEaddress]と[manufactreDataMacDataString]を取得
3.鍵にしたNFCタグにNDEFレコードを書き込み
4.当プログラムをコンパイル
  ついでに、タグのIDを調査!
5.実行したいスマホで公式APPで施錠・解錠
  どうもBLEスキャンをすっ飛ばしたせいで、スマホ自体に情報がなくて通信できないみたい?
6.実行したいスマホにアプリをインストール

参考文献

開祖様

OpenSesame: Reverse Engineering BLE Lock
https://itsze.ro/blog/2016/12/18/opensesame-reverse-engineering-ble-lock.html

スマートロックのSesami miniをnodeでBluetooth連携する
https://qiita.com/odetarou/items/9628d66d4d94290b5f2d

スマートロックのSesami miniをBluetooth連携する(ESP32編)
https://qiita.com/odetarou/items/51424e1963c5d5099f1a

この方々がいなければ始めもしなかったですね。
@odetarou様のNode版でByteデータを確認しながら作業できたのが、かなり助かりました。感謝!感謝です!

BLE版の原動力

非rootなAndroidでSesame miniの鍵を取得した時のメモ
https://qiita.com/offmon/items/6f24c70ae692a938602e

@offmon様のこの記事を見つけたので、BLE版を考えていたけどしなかったという状態から、始めよう!と思ったキッカケになりました。
ただ、なぜか実験機のAndroid ver.7 Nougatでは、logcatのデバックが表示されなくて、悩みました、、、
なので、デバック用の関数がprintlnだったりします。調べる気力がここまで避けなかったので、、、、

NFCのプログラムについて

Android向けNFCアプリ作成の基本
http://www.atelier-nodoka.net/2012/05/android-nfctag-intent/

[Kotlin] AndroidでNFCタグを使ってみた① – ID情報取得 –
https://www.dcom-web.co.jp/lab/mobile/android/nfc_tutorial1

NFCタグをタッチしたときにIntentが起こるようにします。反応するタグの種類を設定できます。 · GitHub
https://gist.github.com/meco300/4561607

書き方とか、書き方の違いによってどのタイミングで動くかを理解するために、色んなサイトを参考したのですが、記録していたのがこの方々の記事ですね。下記の影響もあって、onCreateに書いて開くと同時に動作させることにしました。

NFCをかざした時のAndroidのアプリの選択について(A1の範囲)

kanetaiの二次記憶装置 - Android NFC
http://kanetai.hatenablog.com/entry/20131122/1385119482

Enjoy RFID - AARメッセージをゲットする
http://enjoy-rfid.blogspot.com/2013/07/aar.html

NFCカードをかざした時の動作について調べたサイト。知識については前者のサイト、書き方・動かし方については後者のサイトで実験して納得しました。
[Android Application Record]も実際にNXP TagWriterで作れたので実際に作って、アプリ名で開いてみましたが、日常で使われなさすぎでは?っと思ったり。便利な機能な気がするのに、、、、

BLEの書き方について

hiramine.com -BLE通信ソフトを作る ( Android Studio 2.3.3 + RN4020 )
https://www.hiramine.com/programming/blecommunicator/index.html

Android BLE 実装についての概要
https://qiita.com/zzt-osamuhanzawa/items/a2b538bf5f564173ec88

AndroidとiOS間のデータのやりとりをBluetooth LEで行う
http://blog.techfirm.co.jp/2014/05/08/androidとios間のデータのやりとりをbluetooth-leで行う/

野生のプログラマZ フィード-AndroidのBluetoothでスキャンしてみる
http://harumi.sakura.ne.jp/wordpress/2019/09/03/androidのbluetoothでスキャンしてみる/

ここについても色んなページを参考にしたので、記録していたものを、、、。正直どう書くのが一番安定なのかが、未だによくわからない。

アプリからUIを除こうと考えた時に、、、

Activityなしのアプリを作ってIntentを受け取れるか
https://qiita.com/litmon/items/7ac3c49280f166757f74

@litmon様のという記事を見て、「あ、これしないほうがいいかも?」と思って、UIありのまま実装することに、、、、

連続してNFCが反応しないようにする準備として、、、

Manifestに静的に記述するIntentFilterを動的にon/offする方法
https://qiita.com/kino2718/items/412f12e08260b94dd3b0

Androidのアプリアイコンとタイトルを動的に変更する
https://qiita.com/temoki/items/3fa4acc0a897bbbbbc8f

@kino2718様の方法を眺めつつ、実装にまではせず。
実際、クールタイムみたいなのがスマホにはあるので、、、、
ただ、実験はしてみようと思ったので、書き方については@temoki様の記事も含めて参考にしてAndroidManifest.xmlを書いてます。

Kotlinについて

まくまくKotlinノート - 変数を定義する (val, var)
https://maku77.github.io/kotlin/basic/var.html

KotlinのNull Safety攻略ガイド
https://qiita.com/mitchy321/items/ccc584fb3bd7fc3db8da

【Kotlin】whenでif-else
https://qiita.com/AAkira/items/3d5b694d488fe029d7b9

コジオニルク-Boolean 比較に対するより良い書き方
https://kojion.com/posts/602

実際Androidのアプリの開発はコレが始めてだったので、AndroidStudioの入れ方から調べたという。インストールをして、AndroidStudioの使い方の理解と、Android開発のためのファイル構造の理解、Androidの専門用語を把握するのに放置しながら2ヶ月ほどかかることに、、、、

Kotlinでのバイト計算について

クマは森で用を足しますか? - 16 進文字列とバイト値を変換する関数を Kotlin で書く
https://cheerio-the-bear.hatenablog.com/entry/2020/03/07/233028

KotlinでByteArrayをLong, Intに変換する
https://qiita.com/k-yamada-github/items/499b774fcf1f2d6ec611

普段Byte処理なんてしないので、「この数字をこうしたいのに、どうしてならない!」と嘆きまくることに、、、。符号なしの32bit整数のバイト列にしたものとか、それの16進法文字列とか表現方法に対して、検索する為の「検索ワード」が出てこないことにも四苦八苦。
あ、Javaさんは符号ありがデフォルトなんですね、、、(遠い目

Androidアプリのリバースエンジニアリングについて

Android apk の解析
https://qiita.com/kasaharu/items/900fc937be80d87090ce

Android アプリ解析基礎 その2 -PC編-
https://qiita.com/totem/items/48f25abd5769315afa18

Androidアプリのapkファイルを解析する方法
https://kaworu.jpn.org/java/Androidアプリのapkファイルを解析する方法

当初は公式APPにBluetoothの書き方があるから、読めばなんとかなる!と甘く見積もっていたのですが、難読化されてて読めないことに絶望しました。なので、リバース・リバースエンジニアリングみたいなことをすることに、、、、

編集後記

一旦作り終えて思いますが、「これ、公式で作ってくれたらいいのに」とか「iOSのショートカットで実現できればいいのに」なんて思いながら、バイト列に嘆きながら書いてました。
実際動きはするのですが、BLE部分はもう少し理解を深めて、整理しないと運用は厳しいかなと感じてます。(なんか幽霊みたいにかざしてないのに動いたし、、、

実装作業は、後で投稿するWEBAPI版が10時間、このBLE版は20時間位ですかね。丸二日考えて、唸ってました。調査時間は、どれくらいだろう?ここが一番かかってるかもしれません。

やはり自分が出来ることは、何かをつくることではなくて、物事をつなげることしかできないなと改めて実感した次第です。

ドルチェグストを壊すのもあるので、まだまだ遊び道具には困らなさそうです。

それでは~!

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4