안드로이드 BLE & Beacon

이영준·2023년 4월 27일
0

📌 BLE(Bluetooth Low Energy)

블루투스 4.0에 적용된 데이터 통신만을 위해 탄생된 새로운 기술, 음성지원이 되지 않으므로 주로 데이터 통신으로 사용된다.

🔑 BLE 주요개념

  • ATT(Attribute Protocol)
    서버와 클라이언트 사이의 데이터 교환에 대한 규칙을 정의
    • 애플리케이션에서의 데이터 교환은 ATT를 기반으로 이루어짐
    • 프로토콜 동작 명령어로는 Request, Response, Command, Notification, Indication, Confirmation 등이 있음
  • GAP(Generic Access Profile)

    • 서로 다른 제조사가 만든 BLE 디바이스들끼리 호환되도로 통신 규격을 제공
    • Advertisiting, Connection 제어 기능을 수행
  • GATT
    BLE 디바이스간의 데이터를 교환할 때 데이터의 구조를 정의해 놓은 profile

  • Profile

    • service들(ex) 조도, 습도 등등)로 이루어져있으며, 서비스는 데이터를 논리적 단위로 나눠 놓은 그룹으로, 특성이라는 더 작은 데이터 단위를 하나 이상 포함하고 있다. 이 서비스는 UUID라는 고유 식별자를 갖고 있다.
    • ex) 온도 서비스 UUID -> f000a00-0451-4000-b000-00000000000

🔑 BLE 통신의 역할

Central (GATT client, master)

  • 전원과 메모리 등 리소스를 가진 장치
  • GATT 서버로 데이터 요청
  • 폰, 태블릿

Peripheral (GATT Server, slave)

  • 저전력, 제한된 리소스를 가진 장치로 요청한 센서 데이터
  • 일정한 주기로 신호를 주변에 broadcast 한다.
    -Service, Characteristic에 대한 정의
  • 웨어러블 디바이스, 센서

📌 BLE 스캔 구현

  1. Manifest 권한 선언
    <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

    <uses-feature android:name="android.hardware.bluetooth" android:required="true" />
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

runtime 퍼미션 구현

private val runtimePermissions = arrayOf(
 Manifest.permission.BLUETOOTH_SCAN,
        Manifest.permission.BLUETOOTH_ADVERTISE,
  Manifest.permission.BLUETOOTH_CONNECT
    )
    .. 후략(테드퍼미션 쓰자)

bluetoothManager를 통해 adapter 불러서 bluetooth adapter 에 연결

startScan 함수를 만들어 이를 호출 시 스캔을 시작하도록 하였다.
bluetoothscanner 객체를 통하여 주위의 블루투스 장비들을 스캔할 수 있다.

    private fun startScan() {
        decideListAdapter.clear()
        decideListAdapter.notifyDataSetChanged()
        Log.d(TAG, "startScan")
        // BLE Sensor Scan
        //전체 scan.
        scanner.startScan(scanCallback)

        //10초 후 scan 중지
        handler.postDelayed({
            stopScan()
        }, 5_000)

    }

이 스캔을 필터를 통해 조건을 줄 수도 있다.

전체코드

class MainActivity : AppCompatActivity() {
    companion object {
        private const val PERMISSION_REQUEST_CODE = 8
    }
    private lateinit var bluetoothManager: BluetoothManager
    private lateinit var scanner: BluetoothLeScanner
    private lateinit var decideListAdapter: LeDeviceListAdapter
    private lateinit var checkPermission: CheckPermission

    private var blueAdapter: BluetoothAdapter? = null

    private val runtimePermissions = arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.BLUETOOTH_SCAN,
        Manifest.permission.BLUETOOTH_ADVERTISE,
        Manifest.permission.BLUETOOTH_CONNECT
    )

    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        bluetoothManager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
        checkPermission = CheckPermission(this)
        blueAdapter = bluetoothManager.adapter

        if (blueAdapter == null || !blueAdapter!!.isEnabled) {
            Toast.makeText(this, "블루투스 기능을 확인해 주세요.", Toast.LENGTH_SHORT).show()
            val bleIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(bleIntent, 1)
        }
        scanner = blueAdapter!!.getBluetoothLeScanner()

        if (!checkPermission.runtimeCheckPermission(this, *runtimePermissions)) {
            ActivityCompat.requestPermissions(this, runtimePermissions, PERMISSION_REQUEST_CODE)
        } else { //이미 전체 권한이 있는 경우
            initView()
        }
    }

    override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray  ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            PERMISSION_REQUEST_CODE -> if (grantResults.size > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 권한을 모두 획득했다면.
                initView()
            } else {
                checkPermission.requestPermission()
            }
        }
    }

    private fun initView() {
        decideListAdapter = LeDeviceListAdapter(this)
        binding.devicelist.adapter = decideListAdapter
        binding.devicelist.onItemClickListener =
            OnItemClickListener { parent, view, position, id ->
                val device = decideListAdapter.getItem(position) as BluetoothDevice
                Log.d(TAG, "++++++++++++ Selected Device ++++++++++++++++++")
                Toast.makeText(this@MainActivity, "selected..$device", Toast.LENGTH_SHORT).show()
                Log.d(TAG, "onItemClick: $device") //62:A6:2D:32:69:F2
//				onScanResult: result:ScanResult{device=62:A6:2D:32:69:F2, scanRecord=ScanRecord [mAdvertiseFlags=4, mServiceUuids=[0000fd5a-0000-1000-8000-00805f9b34fb], mServiceSolicitationUuids=[], mManufacturerSpecificData={}, mServiceData={0000fd5a-0000-1000-8000-00805f9b34fb=[21, -22, 3, 1, 70, -20, -122, -22, 126, 44, 105, 77, -61, 0, 0, 0, -97, 25, -34, -7]}, mTxPowerLevel=-2147483648, mDeviceName=Smart Tag, mTransportBlocks=[]], rssi=-85, timestampNanos=2925531664814018, eventType=27, primaryPhy=1, secondaryPhy=0, advertisingSid=255, txPower=127, periodicAdvertisingInterval=0}
            }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.action_scan ->
                // Toast.makeText(this, "Start Scan", Toast.LENGTH_SHORT).show();
                startScan()
            R.id.action_stop ->
                // Toast.makeText(this, "Stop Scan", Toast.LENGTH_SHORT).show();
                stopScan()
        }
        return super.onOptionsItemSelected(item)
    }

    private fun startScan() {
        decideListAdapter.clear()
        decideListAdapter.notifyDataSetChanged()
        Log.d(TAG, "startScan")
        // BLE Sensor Scan
        //전체 scan.
//        scanner.startScan(scanCallback)

        val setting = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
            .build()

        val filter = ScanFilter.Builder()
            .setDeviceAddress("54:6C:0E:B7:D1:04")
            .build()

        //특정 device 만 scan
        scanner.startScan(mutableListOf(filter) , setting, scanCallback)

        //10초 후 scan 중지
        handler.postDelayed({
            stopScan()
        }, 10_000)

    }

    private fun stopScan() {
        Log.d(TAG, "stopSCan")
        // BLE Sensor Scan Stop
        scanner.stopScan(scanCallback)
    }

    private var scanCallback: ScanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            processResult(result)
        }

        override fun onScanFailed(errorCode: Int) {
            Log.d(TAG, "errorCode: errorCode:$errorCode")
        }

        private fun processResult(result: ScanResult) {
            Log.d(TAG, "processResult: ")
            Log.d(TAG, "++++++++++++scan Result++++++++++++++++++")
            Log.d(TAG, result.toString())
            //블루투스는 UI가 아닌 다른 스레드에서 실행되므로 ui 변경은 ui스레드에서 하도록
            runOnUiThread {
                decideListAdapter.addDevice(result.device)
                decideListAdapter.notifyDataSetChanged()
            }

        }
    }

    var handler = Handler(Looper.getMainLooper())

    override fun onPause() {
        super.onPause()
        stopScan()
    }

}

BLE 센서를 직접적으로 다루어 온도, 습도 등의 값을 가져오는 작업은 내용이 방대하여 소스코드로 대체한다.

📌 Beacon

BLE 통신규약에 근거하여 BLE 신호와 연동하여 다양한 정보를 송수신하는 무선통신기술

  • NFC에 비해 전송거리가 넓지만 보안성이 취약하다.
  • 일대다, 다대다 서비스가 가능하다.

🔑 Beacon 패킷 구조

  • Proximity UUID : 어떤 조직이나 어떤 타입의 장비를 나타내는 고유한 값
  • Major : 같은 UUID에서 대 분류에 주로 사용되면 2byte로 구성
  • Minor : 같은 UUID의 중 분류에서 주로 사용되며 2byte로 구성
  • TX : 신호강도를 기반으로 거리를 측정할 수 있는 값

beacon 역시 major, minor, 거리(tx)를 가져와 사용할 수 있는데, 거리를 변환해주는 라이브러리를 제공한다.

https://github.com/AltBeacon/android-beacon-library

implementation 'org.altbeacon:android-beacon-library:2:19'
    private fun startScan() {
        Log.d(TAG, "startScan")
        leDeviceListAdapter.clear()
        leDeviceListAdapter.notifyDataSetChanged()
        // IBeacon Sensor Scan
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .build()
        val filter = ScanFilter.Builder()
//            .setDeviceAddress("54:6C:0E:B7:40:05")
            .setDeviceAddress("24:71:89:0B:9C:2D")
            .build()
        bluetoothLeScanner.startScan(mutableListOf(filter), settings, mLeScanCallback)
    }

beacon의 major, minor, 및 uuid 가져오기

profile
컴퓨터와 교육 그사이 어딘가

0개의 댓글