GATT 서버에 연결

BLE 기기와 상호작용하는 첫 번째 단계는 기기에 연결하는 것입니다. 더 구체적으로 말하면, 기기의 GATT 서버에 연결하는 것입니다. 블루투스 기기에서 GATT 서버에 연결하려면 connectGatt() 메서드를 사용합니다. 이 메서드는 Context 객체, autoConnect (사용 가능한 상태가 되는 즉시 BLE 기기에 자동으로 연결할지 여부를 나타내는 불리언), BluetoothGattCallback 참조라는 세 가지 매개변수를 사용합니다.

Kotlin

var bluetoothGatt: BluetoothGatt? = null
...

bluetoothGatt = device.connectGatt(this, false, gattCallback)

Java

bluetoothGatt = device.connectGatt(this, false, gattCallback);

그러면 블루투스 기기에서 호스팅하는 GATT 서버에 연결되고 BluetoothGatt 인스턴스가 반환되며, 이 인스턴스는 GATT 클라이언트 작업을 실행하는 데 사용할 수 있습니다. 호출자 (Android 앱)는 GATT 클라이언트입니다. BluetoothGattCallback는 연결 상태와 같은 결과 및 추가적인 GATT 클라이언트 작업을 클라이언트에 전달하는 데 사용됩니다.

바인드된 서비스 설정

다음 예에서 BLE 앱은 블루투스 기기에 연결하고 기기 데이터를 표시하며 기기에서 지원하는 GATT 서비스 및 특성을 표시하는 활동(DeviceControlActivity)을 제공합니다. 사용자 입력에 따라 이 활동은 BLE API를 통해 BLE 기기와 상호작용하는 BluetoothLeService라는 Service와 통신합니다. 통신은 활동이 BluetoothLeService에 연결하고 함수를 호출하여 기기에 연결할 수 있도록 허용하는 바인드된 서비스를 사용하여 실행됩니다. BluetoothLeService에는 활동의 서비스 액세스를 제공하는 Binder 구현이 필요합니다.

Kotlin

class BluetoothLeService : Service() {

    private val binder = LocalBinder()

    override fun onBind(intent: Intent): IBinder? {
        return binder
    }

    inner class LocalBinder : Binder() {
        fun getService() : BluetoothLeService {
            return this@BluetoothLeService
        }
    }
}

Java

class BluetoothLeService extends Service {

    private Binder binder = new LocalBinder();

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    class LocalBinder extends Binder {
        public BluetoothLeService getService() {
            return BluetoothLeService.this;
        }
    }
}

활동은 bindService()를 사용하여 서비스를 시작하고 Intent를 전달하여 서비스를 시작하고 연결 및 연결 해제 이벤트를 수신 대기하기 위한 ServiceConnection 구현, 추가 연결 옵션을 지정하는 플래그를 전달할 수 있습니다.

Kotlin

class DeviceControlActivity : AppCompatActivity() {

    private var bluetoothService : BluetoothLeService? = null

    // Code to manage Service lifecycle.
    private val serviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(
            componentName: ComponentName,
            service: IBinder
        ) {
            bluetoothService = (service as LocalBinder).getService()
            bluetoothService?.let { bluetooth ->
                // call functions on service to check connection and connect to devices
            }
        }

        override fun onServiceDisconnected(componentName: ComponentName) {
            bluetoothService = null
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.gatt_services_characteristics)

        val gattServiceIntent = Intent(this, BluetoothLeService::class.java)
        bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE)
    }
}

Java

class DeviceControlActivity extends AppCompatActivity {

    private BluetoothLeService bluetoothService;

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            bluetoothService = ((LocalBinder) service).getService();
            if (bluetoothService != null) {
                // call functions on service to check connection and connect to devices
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bluetoothService = null;
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.gatt_services_characteristics);

        Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
        bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
}

BluetoothAdapter 설정

서비스가 바인드되면 BluetoothAdapter에 액세스해야 합니다. 기기에서 어댑터를 사용할 수 있는지 확인해야 합니다. BluetoothAdapter에 관한 자세한 내용은 블루투스 설정을 참고하세요. 다음 예에서는 성공을 나타내는 Boolean 값을 반환하는 initialize() 함수에 이 설정 코드를 래핑합니다.

Kotlin

private const val TAG = "BluetoothLeService"

class BluetoothLeService : Service() {

    private var bluetoothAdapter: BluetoothAdapter? = null

    fun initialize(): Boolean {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        if (bluetoothAdapter == null) {
            Log.e(TAG, "Unable to obtain a BluetoothAdapter.")
            return false
        }
        return true
    }

    ...
}

Java

class BluetoothLeService extends Service {

    public static final String TAG = "BluetoothLeService";

    private BluetoothAdapter bluetoothAdapter;

    public boolean initialize() {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter == null) {
            Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
            return false;
        }
        return true;
    }

    ...
}

활동은 ServiceConnection 구현 내에서 이 함수를 호출합니다. initialize() 함수에서 false 반환 값을 처리하는 방법은 애플리케이션에 따라 다릅니다. 현재 기기가 블루투스 작업을 지원하지 않음을 나타내는 오류 메시지를 사용자에게 표시하거나 블루투스 작동을 필요로 하는 모든 기능을 사용 중지할 수 있습니다. 다음 예에서는 활동에서 finish()가 호출되어 사용자를 이전 화면으로 다시 보냅니다.

Kotlin

class DeviceControlActivity : AppCompatActivity() {

    // Code to manage Service lifecycle.
    private val serviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(
            componentName: ComponentName,
            service: IBinder
        ) {
            bluetoothService = (service as LocalBinder).getService()
            bluetoothService?.let { bluetooth ->
                if (!bluetooth.initialize()) {
                    Log.e(TAG, "Unable to initialize Bluetooth")
                    finish()
                }
                // perform device connection
            }
        }

        override fun onServiceDisconnected(componentName: ComponentName) {
            bluetoothService = null
        }
    }

    ...
}

Java

class DeviceControlsActivity extends AppCompatActivity {

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            bluetoothService = ((LocalBinder) service).getService();
            if (bluetoothService != null) {
                if (!bluetoothService.initialize()) {
                    Log.e(TAG, "Unable to initialize Bluetooth");
                    finish();
                }
                // perform device connection
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bluetoothService = null;
        }
    };

    ...
}

기기에 연결

BluetoothLeService 인스턴스가 초기화되면 BLE 기기에 연결할 수 있습니다. 활동은 연결을 시작할 수 있도록 기기 주소를 서비스에 전송해야 합니다. 서비스는 먼저 BluetoothAdapter에서 getRemoteDevice()를 호출하여 기기에 액세스합니다. 어댑터가 이 주소가 있는 기기를 찾을 수 없으면 getRemoteDevice()에서 IllegalArgumentException이 발생합니다.

Kotlin

fun connect(address: String): Boolean {
    bluetoothAdapter?.let { adapter ->
        try {
            val device = adapter.getRemoteDevice(address)
        } catch (exception: IllegalArgumentException) {
            Log.w(TAG, "Device not found with provided address.")
            return false
        }
    // connect to the GATT server on the device
    } ?: run {
        Log.w(TAG, "BluetoothAdapter not initialized")
        return false
    }
}

Java

public boolean connect(final String address) {
    if (bluetoothAdapter == null || address == null) {
        Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
        return false;
    }

    try {
        final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
    } catch (IllegalArgumentException exception) {
        Log.w(TAG, "Device not found with provided address.");
        return false;
    }
    // connect to the GATT server on the device
}

서비스가 초기화되면 DeviceControlActivity가 이 connect() 함수를 호출합니다. 활동은 BLE 기기의 주소를 전달해야 합니다. 다음 예에서는 기기 주소가 활동에 인텐트 추가 항목으로 전달됩니다.

Kotlin

// Code to manage Service lifecycle.
private val serviceConnection: ServiceConnection = object : ServiceConnection {
    override fun onServiceConnected(
    componentName: ComponentName,
    service: IBinder
    ) {
        bluetoothService = (service as LocalBinder).getService()
        bluetoothService?.let { bluetooth ->
            if (!bluetooth.initialize()) {
                Log.e(TAG, "Unable to initialize Bluetooth")
                finish()
            }
            // perform device connection
            bluetooth.connect(deviceAddress)
        }
    }

    override fun onServiceDisconnected(componentName: ComponentName) {
        bluetoothService = null
    }
}

Java

private ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        bluetoothService = ((LocalBinder) service).getService();
        if (bluetoothService != null) {
            if (!bluetoothService.initialize()) {
                Log.e(TAG, "Unable to initialize Bluetooth");
                finish();
            }
            // perform device connection
            bluetoothService.connect(deviceAddress);
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        bluetoothService = null;
    }
};

GATT 콜백 선언

활동이 서비스에 연결할 기기를 알리고 서비스가 기기에 연결되면 서비스는 블루투스 기기의 GATT 서버에 연결해야 합니다. 이 연결에서는 연결 상태, 서비스 검색, 특성 읽기, 특성 알림에 대한 알림을 수신하려면 BluetoothGattCallback가 필요합니다.

이 주제에서는 연결 상태 알림에 중점을 둡니다. 서비스 검색, 특성 읽기, 요청 특성 알림을 수행하는 방법은 BLE 데이터 전송을 참고하세요.

onConnectionStateChange() 함수는 기기의 GATT 서버 연결이 변경될 때 트리거됩니다. 다음 예에서는 서비스가 연결되면 BluetoothDevice와 함께 사용할 수 있도록 Service 클래스에 콜백이 정의되어 있습니다.

Kotlin

private val bluetoothGattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            // successfully connected to the GATT Server
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            // disconnected from the GATT Server
        }
    }
}

Java

private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            // successfully connected to the GATT Server
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            // disconnected from the GATT Server
        }
    }
};

GATT 서비스에 연결

BluetoothGattCallback가 선언되면 서비스는 connect() 함수의 BluetoothDevice 객체를 사용하여 기기의 GATT 서비스에 연결할 수 있습니다.

connectGatt() 함수가 사용됩니다. 이렇게 하려면 Context 객체, autoConnect 불리언 플래그, BluetoothGattCallback가 필요합니다. 이 예에서는 앱이 저전력 블루투스 기기에 직접 연결되므로 autoConnect에는 false이 전달됩니다.

BluetoothGatt 속성도 추가됩니다. 이렇게 하면 서비스가 더 이상 필요하지 않을 때 연결을 종료할 수 있습니다.

Kotlin

class BluetoothLeService : Service() {

...

    private var bluetoothGatt: BluetoothGatt? = null

    ...

    fun connect(address: String): Boolean {
        bluetoothAdapter?.let { adapter ->
            try {
                val device = adapter.getRemoteDevice(address)
                // connect to the GATT server on the device
                bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)
                return true
            } catch (exception: IllegalArgumentException) {
                Log.w(TAG, "Device not found with provided address.  Unable to connect.")
                return false
            }
        } ?: run {
            Log.w(TAG, "BluetoothAdapter not initialized")
            return false
        }
    }
}

Java

class BluetoothLeService extends Service {

...

    private BluetoothGatt bluetoothGatt;

    ...

    public boolean connect(final String address) {
        if (bluetoothAdapter == null || address == null) {
            Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
            return false;
        }
        try {
            final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
            // connect to the GATT server on the device
            bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);
            return true;
        } catch (IllegalArgumentException exception) {
            Log.w(TAG, "Device not found with provided address.  Unable to connect.");
            return false;
        }
    }
}

브로드캐스트 업데이트

서버는 GATT 서버에 연결하거나 연결 해제할 때 활동에 새 상태를 알려야 합니다. 여기에는 여러 가지 방법이 있습니다. 다음 예에서는 브로드캐스트를 사용하여 서비스에서 활동으로 정보를 전송합니다.

서비스는 새 상태를 브로드캐스트하는 함수를 선언합니다. 이 함수는 시스템에 브로드캐스트되기 전에 Intent 객체에 전달되는 작업 문자열을 사용합니다.

Kotlin

private fun broadcastUpdate(action: String) {
    val intent = Intent(action)
    sendBroadcast(intent)
}

Java

private void broadcastUpdate(final String action) {
    final Intent intent = new Intent(action);
    sendBroadcast(intent);
}

브로드캐스트 함수가 배치되면 이 함수는 BluetoothGattCallback 내에서 연결 상태에 관한 정보를 GATT 서버와 전송하는 데 사용됩니다. 상수와 서비스의 현재 연결 상태는 Intent 작업을 나타내는 서비스에 선언됩니다.

Kotlin

class BluetoothLeService : Service() {

    private var connectionState = STATE_DISCONNECTED

    private val bluetoothGattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                // successfully connected to the GATT Server
                connectionState = STATE_CONNECTED
                broadcastUpdate(ACTION_GATT_CONNECTED)
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                // disconnected from the GATT Server
                connectionState = STATE_DISCONNECTED
                broadcastUpdate(ACTION_GATT_DISCONNECTED)
            }
        }
    }

    ...

    companion object {
        const val ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED"
        const val ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"

        private const val STATE_DISCONNECTED = 0
        private const val STATE_CONNECTED = 2

    }
}

Java


class BluetoothLeService extends Service {

    public final static String ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
    public final static String ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";

    private static final int STATE_DISCONNECTED = 0;
    private static final int STATE_CONNECTED = 2;

    private int connectionState;
    ...

    private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                // successfully connected to the GATT Server
                connectionState = STATE_CONNECTED;
                broadcastUpdate(ACTION_GATT_CONNECTED);
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                // disconnected from the GATT Server
                connectionState = STATE_DISCONNECTED;
                broadcastUpdate(ACTION_GATT_DISCONNECTED);
            }
        }
    };

    …
}

활동의 업데이트 수신 대기

서비스가 연결 업데이트를 브로드캐스트하면 활동은 BroadcastReceiver를 구현해야 합니다. 활동을 설정할 때 이 수신기를 등록하고 활동이 화면을 벗어날 때 등록을 취소합니다. 활동은 서비스의 이벤트를 수신 대기하여 BLE 기기와의 현재 연결 상태에 따라 사용자 인터페이스를 업데이트할 수 있습니다.

Kotlin

class DeviceControlActivity : AppCompatActivity() {

...

    private val gattUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            when (intent.action) {
                BluetoothLeService.ACTION_GATT_CONNECTED -> {
                    connected = true
                    updateConnectionState(R.string.connected)
                }
                BluetoothLeService.ACTION_GATT_DISCONNECTED -> {
                    connected = false
                    updateConnectionState(R.string.disconnected)
                }
            }
        }
    }

    override fun onResume() {
        super.onResume()
        registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter())
        if (bluetoothService != null) {
            val result = bluetoothService!!.connect(deviceAddress)
            Log.d(DeviceControlsActivity.TAG, "Connect request result=$result")
        }
    }

    override fun onPause() {
        super.onPause()
        unregisterReceiver(gattUpdateReceiver)
    }

    private fun makeGattUpdateIntentFilter(): IntentFilter? {
        return IntentFilter().apply {
            addAction(BluetoothLeService.ACTION_GATT_CONNECTED)
            addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED)
        }
    }
}

Java

class DeviceControlsActivity extends AppCompatActivity {

...

    private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
                connected = true;
                updateConnectionState(R.string.connected);
            } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
                connected = false;
                updateConnectionState(R.string.disconnected);
            }
        }
    };

    @Override
    protected void onResume() {
        super.onResume();

        registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter());
        if (bluetoothService != null) {
            final boolean result = bluetoothService.connect(deviceAddress);
            Log.d(TAG, "Connect request result=" + result);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver(gattUpdateReceiver);
    }

    private static IntentFilter makeGattUpdateIntentFilter() {
        final IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);
        intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);
        return intentFilter;
    }
}

BLE 데이터 전송에서 BroadcastReceiver는 기기의 특성 데이터뿐만 아니라 서비스 검색도 전달하는 데 사용됩니다.

GATT 연결 닫기

블루투스 연결을 처리할 때 중요한 단계 중 하나는 블루투스 연결이 끝나면 연결을 닫는 것입니다. 이렇게 하려면 BluetoothGatt 객체에서 close() 함수를 호출합니다. 다음 예에서 서비스는 BluetoothGatt 참조를 보유합니다. 활동이 서비스에서 바인딩 해제되면 기기 배터리가 소모되지 않도록 연결이 닫힙니다.

Kotlin

class BluetoothLeService : Service() {

...

    override fun onUnbind(intent: Intent?): Boolean {
        close()
        return super.onUnbind(intent)
    }

    private fun close() {
        bluetoothGatt?.let { gatt ->
            gatt.close()
            bluetoothGatt = null
        }
    }
}

Java

class BluetoothLeService extends Service {

...

      @Override
      public boolean onUnbind(Intent intent) {
          close();
          return super.onUnbind(intent);
      }

      private void close() {
          if (bluetoothGatt == null) {
              Return;
          }
          bluetoothGatt.close();
          bluetoothGatt = null;
      }
}