100均防犯ブザー+Atom lite+androidで着信通知デバイスを作る

# ご注意
本記事にはハードウェアの改造が含まれています。またソフトウェアの実装においてセキュリティーへの考慮はほとんどしていません。なので真似して作る際のリスクは各人の判断でお願いします。
ソースコードのライセンスはMITとします。

![PXL_20210122_072239759.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58150/b319e0c8-999e-0266-e5d2-2ff6e707afaa.jpeg)


# 動機
実家の父親いわく、母の耳が弱くなってスマホの着信に気づかないことが多くなって困っているとのこと。着信がわかりやすくなる機械ってないの？
# 調べてみた
有線電話なら
https://www.yodobashi.com/product/100000001002096313/
みたいな「リンガー」というものが割と昔からある。
しかしスマホでは１つしか見つけられなかった。
https://www.youtube.com/watch?v=HyIdrvCXAiw
# ざっくり仕様
- 着信したら音と光でおしらせ
- ２箇所以上で使えること
- ペアリングさせたくない。切れたら復旧するのはめんどい。
- 名前は「exringer SSP-2」とする。(SSP = Sugoku Syoboi Product)

# exringerシステムダイアグラム的なにか
![スクリーンショット (42).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58150/2f8e95ac-7b5c-e148-74a8-64c6c761aa20.png)

# 実装
## Android側
#### MainActivity
Permissionの設定とSharedPreferencesにMajor, Minor(今は使っていません)をセットするだけです。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val lightid = getSharedPreferences("lightid", Context.MODE_PRIVATE)
        val major: Int = lightid.getInt("Major", 1)
        val minor: Int = lightid.getInt("Minor", 1)
        val etMajor: TextView = findViewById(R.id.etMajor)
        etMajor.text = major.toString()
        val etMinor: TextView = findViewById(R.id.etMinor)
        etMinor.text = minor.toString()
        val editor: SharedPreferences.Editor = lightid.edit()
        editor.putInt("calling", 0)
        editor.apply()

        // Android M Permission check 
        if (checkSelfPermission(ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            AlertDialog.Builder(this)
                .setTitle("BLE")
                .setMessage("許可？")
                .setPositiveButton(android.R.string.ok, null)
                .setOnDismissListener {
                    requestPermissions(
                        arrayOf(ACCESS_COARSE_LOCATION),
                        1
                    )
                }
                .show()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String?>, grantResults: IntArray
    ) {
        when (requestCode) {
            1 -> {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//                    Log.d(FragmentActivity.TAG, "coarse location permission granted")
                } else {
                    AlertDialog.Builder(this)
                        .setTitle("Functionality limited")
                        .setMessage("Since location access has not been granted, this app will not be able to discover beacons when in the background.")
                        .setPositiveButton(android.R.string.ok, null)
                        .setOnDismissListener { }
                        .show()
                }
                return
            }
        }
    }

    override fun onStop() {
        super.onStop()
        val lightid = getSharedPreferences("lightid", Context.MODE_PRIVATE)
        val editor: Editor = lightid.edit()
        val etMajor: TextView = findViewById(R.id.etMajor)
        val major: Int = etMajor.text.toString().toInt()
        editor.putInt("Major", major)
        editor.apply()
        val etMinor: TextView = findViewById(R.id.etMinor)
        val minor: Int = etMinor.text.toString().toInt()
        editor.putInt("Minor", minor)
        editor.apply()
        editor.putInt("calling", 0)
        editor.apply()
    }
```

#### ビーコン発信
ポイントは常時待受をするためにNotificationを使うところです。

```kotlin:BeaconIntentService.kt
class BeaconIntentService(name: String = "BeaconIntentService") : Service() {
    override fun onBind(intent: Intent): IBinder? {
        throw UnsupportedOperationException("Not yet implemented")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val name = "Title notif"
        val id = "beacon_foreground"
        val notifyDescription = "この通知の詳細情報を設定します"

        if (manager.getNotificationChannel(id) == null) {
            val mChannel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH)
            mChannel.apply {
                description = notifyDescription
            }
            manager.createNotificationChannel(mChannel)
        }

        var notification = NotificationCompat.Builder(this, id)
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setContentTitle("Title")
            .setContentText("desc")
            .build()

        transmitBeacon();

        startForeground(1, notification)

        return START_STICKY
    }

    private fun transmitBeacon() {
        val beaconParser = BeaconParser()
            .setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24")
        val beaconTransmitter = BeaconTransmitter(applicationContext, beaconParser)
        beaconTransmitter.advertiseMode = ADVERTISE_MODE_LOW_LATENCY

        val lightid = getSharedPreferences("lightid", Context.MODE_PRIVATE)
        val major: String = lightid.getInt("Major", 1).toString()
        val minor: String = lightid.getInt("Minor", 1).toString()

        val uuid = "";
        val rssi = "0"
        val second = 3;

        val beacon = Beacon.Builder()
            .setId1(uuid)
            .setId2(major)
            .setId3(minor)
            .setRssi(rssi.toInt())
            .setTxPower(rssi.toInt())
            .setManufacturer(0x004C)
            .build()

        if( beaconTransmitter.isStarted ) {
            Log.d("BeaconActivity", "isStarted")
            return
        }

        val thread = Thread(Runnable {
            val lightid = getSharedPreferences("lightid", Context.MODE_PRIVATE)
            val editor: SharedPreferences.Editor = lightid.edit()
            try {
                while (lightid.getInt("calling", 0) == 1){
                    Log.d("BeaconIntentService", "calling = "+lightid.getInt("calling", 0).toString())

                    beaconTransmitter.startAdvertising(beacon, object : AdvertiseCallback() {
                        override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
                            super.onStartSuccess(settingsInEffect)
                            //成功
                            Log.d("BeaconIntentService", "isStarted")
                        }

                        override fun onStartFailure(errorCode: Int) {
                            //失敗
                            Log.d("BeaconIntentService", "failed")
                            editor.putInt("calling", 0)
                            editor.apply()
                        }
                    })
                    Thread.sleep((second.toInt() * 1000).toLong())
                    if( beaconTransmitter.isStarted ) {
                        beaconTransmitter.stopAdvertising()
                        Log.d("BeaconIntentService", "stoped")
                    }
                }
            } catch (e: InterruptedException) {
                e.printStackTrace()
                if( beaconTransmitter.isStarted ) {
                    beaconTransmitter.stopAdvertising()
                    Log.d("BeaconIntentService", "failed")
                    editor.putInt("calling", 0)
                    editor.apply()
                }
            }
        })
        thread.start()
    }
}
```

#### 着信
ポイントはパーミッションをコードでセットするだけでなく、端末の設定変更が必要なところです。
(参考) https://qiita.com/kabayan/items/190936f4b71cf048c1e4
電話の着信以外の通知もコードを書けば受けとれるはずです。

```kotlin:Incoming.kt
class IncomingCall : BroadcastReceiver() {
    private var ctx: Context? = null
    override fun onReceive(context: Context, intent: Intent?) {
        Log.d("IncomingCall", "Intent: $intent")
        ctx = context
        try {
            //TelephonyManagerの生成
            val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
            //リスナーの登録
            val phoneListener = MyPhoneStateListener()
            tm.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE)
        } catch (e: Exception) {
        }
    }

    /**
     * カスタムリスナーの登録
     * 着信〜終了 CALL_STATE_RINGING > CALL_STATE_OFFHOOK > CALL_STATE_IDLE
     * 不在着信 CALL_STATE_RINGING > CALL_STATE_IDLE
     */
    private inner class MyPhoneStateListener : PhoneStateListener() {
        override fun onCallStateChanged(state: Int, callNumber: String) {
            val con = ctx
            if (con != null) {
                val lightid = con.getSharedPreferences("lightid", Context.MODE_PRIVATE)
                val editor: SharedPreferences.Editor = lightid.edit()
                when (state) {
                    TelephonyManager.CALL_STATE_IDLE -> {
                        Log.d("IncomingCalling", "idle")
                        editor.putInt("calling", 0)
                        editor.apply()
                    }
                    TelephonyManager.CALL_STATE_RINGING
                    -> {
                        Log.d("IncomingCalling", "ring")
                            editor.putInt("calling", 1)
                            editor.apply()
                            val intent = Intent(con, BeaconIntentService::class.java)
                            con.startForegroundService(intent)
                    }
                    TelephonyManager.CALL_STATE_OFFHOOK -> {
                        Log.d("IncomingCalling", "offhook")
                        editor.putInt("calling", 0)
                        editor.apply()
                    }
                }
            }
        }
    }
}
```

## Atom lite側

AndroidからのBeaconを受信しますが、Android側で設定したMajorと同じビーコンに反応します。
Atom liteのポートはG21、G25を使っています。
Beacon受信に関しては
https://qiita.com/KazuyukiEguchi/items/159e628ab9f7fcc74541
を参考にさせていただきました。
複数のAtom liteを同じコードで動かせば何台でも同時に反応します。

```cpp:Incoming.ino
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

int scanTime = 1; // スキャン完了までの秒数
int acceptMajor = 1; // 受信するMajor。これが一致するビーコンに反応します
int flag = 0;

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      char uuid[60];
      BLEAddress addr = advertisedDevice.getAddress();
      int rssi = advertisedDevice.getRSSI();
      std::string data = advertisedDevice.getManufacturerData();

      if (data.length() == 25)
      {
        if ((data[0] == 0x4c) && (data[1] == 0x00) && (data[2] == 0x02) && (data[3] == 0x15)) {
          sprintf(uuid, "%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X"
                  , data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13]
                  , data[14], data[15], data[16], data[17], data[18], data[19]);
          int major = (int)(((data[20] & 0xff) << 8) + (data[21] & 0xff));
          int minor = (int)(((data[22] & 0xff) << 8) + (data[23] & 0xff));
          signed char power = (signed char)(data[24] & 0xff);

          if (major == acceptMajor) {
            Serial.printf("addr=%s rssi=%d uuid=%s,major=%d,minor=%d,power=%d\n", addr.toString().c_str(), rssi, uuid, major, minor, power);
            flag = 1;
          }
        }
      }
    }
};

void setup() {
  Serial.begin(115200);
  pinMode(21, OUTPUT);
  pinMode(25, OUTPUT);
}

void loop() {
  flag = 0;
  BLEDevice::init("");
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), true);  // ignore dup、set false
  pBLEScan->setActiveScan(true);
  BLEScanResults foundDevices = pBLEScan->start(scanTime);
  Serial.println("Scan done!");
  if (flag == 1) {
    digitalWrite(21, HIGH);
    digitalWrite(25, HIGH);
    delay(500);
    digitalWrite(21, LOW);
    digitalWrite(25, LOW);
  } else {
    digitalWrite(21, LOW);   
    digitalWrite(25, LOW);   
  }
}
```


## ブザー側
今回購入したブザーには２つスイッチがあり、それぞれがLEDとブザーに割り当てられています。そのスイッチ部にリレーからの配線を接続しておきます。

# 実際の動作
着信すると0.5秒おきに光とブザーでお知らせします。

https://youtu.be/nTF5ThzwrPI

# アイデアとか
- Major値をAndoridから書き換えられるといいかなぁ。
- Minor値を使ってLEDの色変えるとかもできそう。