Android

BLE 통신 개념 + 통신 과정 + Notification 설정

그란. 2018. 6. 21. 10:58

  

1. BLE 통신 개념

1) 개념 정리

  • BLE 란? :  
2010년, 새로운 Bluetooth 표준으로 Bluetooth 4.0 이 채택이 된다. 기존의 Bluetooth Classic과의 가장 큰 차이는 훨씩 적은 전력을 사용하여 Classic과 비슷한 수준의 무선 통신을 할 수 있다는 점이었다. 이는 당시 Bluetooth의 최대 단점이었던 과도한 베터리를 소모 문제를 해결하는 기술이었기 때문에, Bluetooth 관련 업계에 큰 반향을 일으켰다. 이렇게 저전력을 이용하여 무선통신을 하는 특징을 Bluetooth Low Energy (이하 BLE) 라고 부르는데, Bluetooth 4.0 이후의 버전들은 이 용어로 대체되서 불리기도 한다. 최근 출시되고 있는 스마트 밴드, 워치, 글래스 등의 웨어러블 무선통신 기기들의 대부분은 이 BLE 방식을 이용하여 무선 통신을 한다.

  • Master, Client :  앱 // Slave, Server : 블루투스 기기
  • 통신과정 : 스캔 -> 연결 -> 통신
  • 연산 종류 : Read, Write, Notification, Indication
  • Notification : 옵저버 패턴으로 작동, 기기에 옵저버를 심어놓은 후 상태가 변화했을 때 앱의 메소드로 콜백함
  • BluetoothGatt 구조 : 여러개의 BluetoothService 를 가짐 -> 각 BluetoothService는 여러개의 BluetoothCharacteristic을 가짐 -> 각 BluetoothCharacteristic은 여러개의 BluetoothDescriptor 를 가짐
(*이하 BluetoothCharacteristic = 캐릭터)
  • UUID개념 :각 BluetoothService, BluetoothCharacterstic, BluetoothDescriptor는 고유한 아이디인 uuid를 가진다

  • 좀 더 구체적으로 UUID를 알기 위해서 예시를 들면, 
블루투스 기기에 Connect한 후 , BluetoothGatt.getService() 를 하면 기본적으로 5개의 서비스가 검색된다

서비스1. 00001800-0000-1000-8000-00805f9b34fb  : Generic Access Service
서비스2. 00001801-0000-1000-8000-00805f9b34fb : Generic Attrubute Service

서비스3. 0000ff00-0000-1000-8000-00805f9b34fb   : UNDEFINED 
서비스4. 0000180a-0000-1000-8000-00805f9b34fb  : 디바이스 정보
서비스5. f000ffc0-0451-4000-b000-000000000000   : OAD 서비스 ( 다운로드) 공기를 매개체로 한 다운로드


이중에서 정해져 있는 고유한 값인 1,2,4,5를 제외하고 3번을 이용해야한다 
3번의 서비스는 또 3가지의 캐릭터를 가지고 있다

     캐릭터1 : 0000ff01-0000-1000-8000-00805f9b34fb    READ WRITE (디바이스 정보)

            

     캐릭터2 : 0000ff02-0000-1000-8000-00805f9b34fb    디바이스 이름

           

     캐릭터3 : 0000ff03-0000-1000-8000-00805f9b34fb    Notification


요기서 중요한것은 서비스3 & 캐릭터1 & 캐릭터3 이다

  • 기본 통신개념 : 통신은 바이트배열을 보내고 받는다 
- 바이트배열 -> 스트링
String str = new String (characteristic.getValue(), "UTF-8");

- 스트링 -> 바이트배열
byte[] by = str.getByte();


2. SCAN -> CONNECT -> READ/WRITE 과정


1) Scan하기 위해 블루투스 ON 상태확인과 위치권한이 필요 

* 스캔하는 창을 따로 띄우고 그 값을 받기 위해 Dialog테마의 액티비티를 이용 
( AlertDialog에 layout를 setView 로 했는데 문제가 있었음) 
(스캔하기 위해 postDelay를 사용하는데 터치가 잘 안되는 문제등)

->액티비티로 이동하게 한 후 선택한 값을 리턴하는 방식이 깔끔하다



메인액티비티에서 호출한 후 
Intent intent = new Intent(MainActivity.this, DeviceListBTActivity.class);
startActivityForResult(intent, DEVCIE_LIST_BT_ACTIVITY);


퍼미션 체크를 위해 TedPermission 라이브러리를 사용

2) Scan Activity

public class DeviceListBTActivity extends Activity {
private static final int REQUEST_ENABLE_BT = 99;
private BluetoothAdapter mBluetoothAdapter;

private TextView mEmptyList;
public static final String TAG = "DeviceListBTActivity";

ArrayList<BluetoothDevice> deviceList = new ArrayList<>();
ArrayList<Integer> rssiList = new ArrayList<>();
private BTAdapter deviceAdapter;
private static final long SCAN_PERIOD = 10000; //scanning for 10 seconds
private Handler mHandler;
private boolean mScanning;

ListView listView;
private BluetoothLeScanner scanner;

private Button cancelButton;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
checkPermissionBluetooth();

}

@Override
protected void onResume() {
super.onResume();
//등록됐을때 맥주소를 DB에 저장해놓고, 저장플래그도 설정하고
}


private void checkPermissionBluetooth() {
/**
* 로직 : 1. 블루투스 검사
* 1-1. 블루투스 연결 된 경우 : 2 이동
* 1-2. 블루투스연결 안된 경우 : 설정 창 띄움
* 1-2-1. 설정에서 허용한경우 : 2 이동
* 1-2-2. 설정에서 허용하지 않은 경우 : finish
* 2. 위치 권한 검사
* 2-1. 위치 권한 허용 된 경우 : 스캔 시작
* 2-2. 위치 권한 허용 안된 경우 : 허용창 띄움
* 2-2-1. 허용창에서 허용한 경우 : 스캔 시작
* 2-2-2. 허용창에서 허용하지 않은 경우 : 설정창 이동 창 띄움
* 2-2-2-1. 설정창에서 허용하지 않은 경우 : finish
* 2-2-2-2. 설정창에서 허용한 경우 : 스캔 시작
*
*/

final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();

if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, "이 기기는 블루투스를 지원하지 않습니다", Toast.LENGTH_SHORT).show();
finish(); //이런 예외상황이 있을 수 있나. 어차피 최소버전은 21인데
}

if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
} else {
TedPermission.with(this)
.setPermissionListener(new PermissionListener() {
@Override
public void onPermissionGranted() {
Log.d(TAG, "onPermissionGranted: 권한 설정 완료");
initView();
}

@Override
public void onPermissionDenied(ArrayList<String> deniedPermissions) {
Toast.makeText(DeviceListBTActivity.this, "[위치]권한을 허용하신 후 다시 이용해 주세요", Toast.LENGTH_SHORT).show();
finish();
}
}) // 퍼미션에 대한 콜백 작업 (아래)
.setRationaleMessage("블루투스 기기를 검색하기 위해 [위치]권한을 허용하셔야 합니다")
.setGotoSettingButton(true)
.setPermissions(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
.check();
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == RESULT_OK) {
checkPermissionBluetooth();
} else {
Toast.makeText(DeviceListBTActivity.this, "블루투스 기능을 활성화한 후 다시 실행해주세요", Toast.LENGTH_SHORT).show();
finish();
}
}
}

private void initView() {
/* Initialize device list container */

setContentView(R.layout.activity_device_list_bt);
android.view.WindowManager.LayoutParams layoutParams = this.getWindow().getAttributes();
layoutParams.gravity = Gravity.TOP;
layoutParams.y = 200;
mHandler = new Handler();

deviceAdapter = new BTAdapter(DeviceListBTActivity.this, deviceList);
listView = findViewById(R.id.new_devices);
listView.setAdapter(deviceAdapter);
listView.setOnItemClickListener(deviceClickListener);
cancelButton = findViewById(R.id.btn_cancel);
mEmptyList = (TextView) findViewById(R.id.empty);

cancelButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mScanning == false) scanLeDevice(true);
else finish();
}
});

scanLeDevice(true);
}

/**
* 킷캣 , 롤리팝 분기 처리
*
* @param enable
*/

@SuppressLint("NewApi")
private void scanLeDevice(final boolean enable) {

if (enable) {

BluetoothModule.getInstance().disconnect();

//롤리팝 기준으로 분기 처리 (킷캣 별도 처리)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
scanner.stopScan(scanCallback);
mScanning = false;
cancelButton.setText("스캔시작");
}
}, SCAN_PERIOD);

mScanning = true;
scanner = mBluetoothAdapter.getBluetoothLeScanner();
List<ScanFilter> scanFilters = new ArrayList<>();
ScanFilter scanFilter = new ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString())
.build();

scanFilters.add(scanFilter);
ScanSettings scanSettings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC).build();
scanner.startScan(scanFilters, scanSettings, scanCallback);
cancelButton.setText("취소");

} else {

mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothAdapter.stopLeScan(scanLeCallBack);
cancelButton.setText("스캔시작");
}
}, SCAN_PERIOD);

mScanning = true;
mBluetoothAdapter.startLeScan(scanLeCallBack);
cancelButton.setText("취소");

}
}
}

public BluetoothAdapter.LeScanCallback scanLeCallBack = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, final int rssi, byte[] scanRecord) {

runOnUiThread(new Runnable() {
@Override
public void run() {
addDevice(device, rssi);
}
});
}
}
};

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, final ScanResult result) {
super.onScanResult(callbackType, result);

if (deviceList.contains(result.getDevice())) //중복 검색 제외
return;

runOnUiThread(new Runnable() {
@Override
public void run() {
addDevice(result.getDevice(), result.getRssi());
}
});
}

@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
Log.d(TAG, "onBatchScanResults: " + results.toString());
}

@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
}
};


private void addDevice(BluetoothDevice device, int rssi) {
rssiList.add(rssi);
deviceList.add(device);
mEmptyList.setVisibility(View.GONE);
deviceAdapter.notifyDataSetChanged();
}

private AdapterView.OnItemClickListener deviceClickListener = new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent result = new Intent();
result.putExtra(BluetoothDevice.EXTRA_DEVICE, deviceList.get(position).getAddress());
setResult(Activity.RESULT_OK, result);
finish();
}
};

class BTAdapter extends BaseAdapter {
Context context;
List<BluetoothDevice> devices;
LayoutInflater inflater;

BTAdapter(Context context, List<BluetoothDevice> devices) {
this.context = context;
inflater = LayoutInflater.from(context);
this.devices = devices;
}

@Override
public int getCount() {
return devices.size();
}

@Override
public Object getItem(int position) {
return devices.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewGroup vg;

if (convertView != null) {
vg = (ViewGroup) convertView;
} else {
vg = (ViewGroup) inflater.inflate(R.layout.device_bt_element, null);
}

BluetoothDevice device = devices.get(position);
final TextView tvadd = vg.findViewById(R.id.address);
final TextView tvname = vg.findViewById(R.id.name);
final TextView tvpaired = vg.findViewById(R.id.paired);
final TextView tvrssi = vg.findViewById(R.id.rssi);

tvrssi.setVisibility(View.VISIBLE);
tvrssi.setText(rssiList.get(position).toString());

tvname.setText(device.getName());
tvadd.setText(device.getAddress());
tvname.setTextColor(Color.BLACK);
tvadd.setTextColor(Color.BLACK);
tvpaired.setTextColor(Color.GRAY);
tvrssi.setTextColor(Color.BLACK);
tvrssi.setVisibility(View.VISIBLE);

if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
Log.i(TAG, "device::" + device.getName());

tvpaired.setText("paired");
tvpaired.setVisibility(View.VISIBLE);

} else {
tvpaired.setVisibility(View.GONE);
}
return vg;
}
}

protected void onPause() {
super.onPause();
}

@Override
public void onStop() {
super.onStop();
}

@Override
protected void onDestroy() {
super.onDestroy();
}

}


포인트는 스캔필터링이다 startLescan을 하면 advertise를 하고 있는 주위 블루투스기기들이 모두 검색된다.

특정 제품군만 필터링 하기 위해 UUID 를 넣어 스캔한다

스캔을 할 때 무식하게 스캔하기 때문에 중복기기가 검색될 수 도 있다.


문제 : 킷캣이하버전에서 특정 기기는 스캔필터링을 못한다. (지원이 안됨) 그래서 제품이름으로 비교해야한다.

해당 리스트를 선택하면 맥주소를 메인액티비티로 리턴해준다


3) Connect

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DEVICE_LIST_BT_ACTIVITY) {
if (resultCode == RESULT_OK) {
//블루스트 리스트를 선택했을때
String deviceAddress = data.getStringExtra(BluetoothDevice.EXTRA_DEVICE);
LoadingDialog.showLoading(this);
bluetoothModule.gattConnect(deviceAddress, new BluetoothModule.BluetoothConnectImpl() {
@Override
public void onSuccessConnect(BluetoothDevice device) {
Log.i(TAG, "onSuccessConnect: 연결완료");
Toast.makeText(SettingActivity.this, "연결완료", Toast.LENGTH_SHORT).show();
LoadingDialog.hideLoading();
binding.txtBt.setText(device.getName());
}

@Override
public void onFailed() {
Log.i(TAG, "onFailed: 연결실패, 다시 연결중....");
Toast.makeText(SettingActivity.this, "연결실패, 다시 연결중", Toast.LENGTH_SHORT).show();
}
});
}
}
}
받은 맥주소를 이용하여 connectGatt()을 한다
이때 true 로 콘넥트 하는게 좋은 것 같다 
한번에 연결이 안되는 경우가 종종 있음, 끊겨도 다시 연결하기 위해,

연결이 된 후에 BluetoothGatt 콜백에서 받을 수 있다

private BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
final Handler handler = new Handler(Looper.getMainLooper());

/**
* 연결상태가 변화 할때 마다 (연결, 끊김) 호출
*/
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {

Log.d(TAG, "onConnectionStateChange: " + status + " " + newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
bluetoothGatt.discoverServices(); // onServicesDiscovered() 호출 (서비스 연결 위해 꼭 필요)

} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Toast.makeText(context, "연결이 끊어졌습니다\n다시 연결중입니다기다려주세요", Toast.LENGTH_SHORT).show();
}
}

/**
* 서비스 연결 후 ( Notification 설정 ) cf) setCharacteristicNotification 까지만 해도 Notification이 되지만 이 메소드의 콜백을 받지 못한다
* (setCharacteristicNotification이 비동기로 완료되기 전에 통신을 한다면 에러가 난다) -> writeDescriptor가 완료 된 순간부터 통신이 가능하다
*/
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {

BluetoothGattCharacteristic ch = gatt.getService(SampleApplication.SERIVCE).getCharacteristic(SampleApplication.CHARACTERISTIC_NOTY);
gatt.setCharacteristicNotification(ch, true);

BluetoothGattDescriptor descriptor = ch.getDescriptor(SampleApplication.CCCD);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
BluetoothGattService service = bluetoothGatt.getService(SampleApplication.SERIVCE);
writeGattCharacteristic = service.getCharacteristic(SampleApplication.CHARACTERISTIC_READ_WRITE);

SharedPreferences pref = Properties.getSharedPreferences(context);
pref.edit().putString(Properties.CONNECTED_BT_ADDRESS, gatt.getDevice().getAddress()).apply();
pref.edit().putString(Properties.CONNECTED_BT_NAME, gatt.getDevice().getName()).apply();

}
}

@Override
public void onCharacteristicRead(final BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
handler.post(new Runnable() {
@Override
public void run() {
btConnectCallback.onSuccessConnect(gatt.getDevice()); // 통신 준비 완료
}
});
}

@Override
public void onDescriptorWrite(final BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
handler.post(new Runnable() {
@Override
public void run() {
btConnectCallback.onSuccessConnect(gatt.getDevice()); // 통신 준비 완료
}
});
}

/**
* 가장 중요한 메소드, ble 기기의 값을 받아온다
*/
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
handler.post(new Runnable() {
@Override
public void run() {
String value = characteristic.getStringValue(1);
value = value.replaceAll(" ", "");
value = value.substring(0, value.length() - 1);
try {
Log.d(TAG, "run: " + value);
btWriteCallback.onSuccessWrite(0, value);
} catch (IOException e) {
e.printStackTrace();
btWriteCallback.onFailed(e);
}
}
});
}
};


콜백의 메소드 구조를 잘 알아야 한다 (이게 포인트)
connect를 호출 하면  onConnectionStateChange 메소드를 타게 되고 정상적으로 연결이 되면

bluetoothGatt.discoverServices();

를 꼭 해줘야 한다.

그럼 아래의 onServicesDiscovered로 메소드를 타게된다

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {

}

이후 통신 가능


4) Read,Write

Read 를 하기 위해서 

BluetoothGattService service = bluetoothGatt.getService(SERVICE_UUID);
BluetoothGattCharacteristic ch = service.getCharacteristic(READ_WRITE_UUID);
ch.setValue("<AG>");

bluetoothGatt.readCharacteristic(ch);


READ,WRITE 할 수 있는 캐릭터UUID를 가져와야한다

( DiscoveryService 할 때 기본적으로 저장되어있음 ) (찾을 수 있다)

*기본적으로 서비스 안에 READWRITE 캐릭터가 내제돼 있다


이렇게 Read 를 하면 onCharacteristicRead 콜백을 타게되고 

public void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, int status) {
Log.d(TAG, "onCharacteristicRead: .리드 된거야 " + characteristic.getStringValue(0));

}

로 값을 받을 수 있다.



3. Notification ★★★ 

사실 포스팅을 한 이유가 Notification때문, 이걸로 일주일 삽질한것 같다

Read, Write를 할 때 <AG> 를 보낼 때 ( 사실은 3C 41 47 3E의 배열로 보냄 ) 블루투스 기기가 만약 여러개의 값을 저장하고 있다면 <AS1111> <AS2222> <AS3333> 의 순서대로 값을 변화하면서 하나씩 보낸다

그럼 Read를 한번만 하게 되면 마지막 값인 <AS3333>의 값만 리턴해준다.

그래서 Notification 사용은 필수적이다 무조건이다   (옵저버 패턴으로 작용하는듯 하다)


-설정-


Connect 과정에서 bluetoothGatt.discoverServices();를 호출하면 아래의 메소드로 들어간다

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {

System.out.println("서비스 발견, status : " + status + "서비스 검색 시작");
if (status == BluetoothGatt.GATT_SUCCESS) {

BluetoothGattCharacteristic ch = gatt.getService(SERIVCE_UUID).getCharacteristic(SERIVCE_NOTY_UUID);
gatt.setCharacteristicNotification(ch, true);

} else {

}
}

NOTIFICATION UUID 값을 설정하면 된다 (NOTY_UUID 찾는 법도 위에서 설명)


이렇게 설정 후 Write통신을 하게 되면 
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {

runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "onCharacteristicChanged: 변화 감지 했습니다");
Log.d(TAG, "onCharacteristicChanged: " + characteristic.getStringValue(0));

}
});
}

이렇게 받을 수 있다. 블루투스 기기가 연속적으로 데이터를 보내면 이 메소드에서 모두 받을 수 있다 



모듈화 -싱글톤 패턴

블루투스 모듈

public class BluetoothModule {
//BluetoothGatt 객체로 Connect 해주고, writeCharacter 해주는 클래스
public static final String TAG = "BluetoothModule";

private BluetoothGatt bluetoothGatt;

private BluetoothConnectImpl btConnectCallback;
private BluetoothWriteImpl btWriteCallback;
private BluetoothGattCharacteristic writeGattCharacteristic;
private Context context;


BluetoothModule() {
}


private static class BluetoothModuleHolder {
private static final BluetoothModule instance = new BluetoothModule();
}

public static BluetoothModule getInstance() {
return BluetoothModuleHolder.instance;
}

public boolean isConnected() {

return bluetoothGatt != null && bluetoothGatt.connect();
}

public BluetoothGatt getGatt() {
return bluetoothGatt;
}

public void disconnect() {
if (isConnected()) {
bluetoothGatt.disconnect();

}
}

/**
* 맥주소를 받아서 connect
*/
public void gattConnect(String macAddress, BluetoothConnectImpl btConnectCallback) {

this.btConnectCallback = btConnectCallback;
context = SampleApplication.instance;

final BluetoothManager bm = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bm.getAdapter();

BluetoothDevice device = bluetoothAdapter.getRemoteDevice(macAddress);
bluetoothGatt = device.connectGatt(context, true, gattCallback);

}

/**
* 프로토콜 보내기 write 를 하고 ble장치로부터 값을 받으면 onCharacteristicChanged() 메소드가 호출 된다
*/
public void sendProtocol(String protocol, BluetoothWriteImpl btWriteCallback) {
if (isConnected()) {
this.btWriteCallback = btWriteCallback;
protocol = "<" + protocol.toUpperCase() + ">";
writeGattCharacteristic.setValue(protocol);
bluetoothGatt.writeCharacteristic(writeGattCharacteristic);
}
}

/**
* 블루투스 콜백
*/
private BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
final Handler handler = new Handler(Looper.getMainLooper());

/**
* 연결상태가 변화 할때 마다 (연결, 끊김) 호출
*/
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {

Log.d(TAG, "onConnectionStateChange: " + status + " " + newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
bluetoothGatt.discoverServices(); // onServicesDiscovered() 호출 (서비스 연결 위해 꼭 필요)

} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Toast.makeText(context, "연결이 끊어졌습니다\n다시 연결중입니다기다려주세요", Toast.LENGTH_SHORT).show();
}
}

/**
* 서비스 연결 후 ( Notification 설정 ) cf) setCharacteristicNotification 까지만 해도 Notification이 되지만 이 메소드의 콜백을 받지 못한다
* (setCharacteristicNotification이 비동기로 완료되기 전에 통신을 한다면 에러가 난다) -> writeDescriptor가 완료 된 순간부터 통신이 가능하다
*/
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {

BluetoothGattCharacteristic ch = gatt.getService(SampleApplication.SERIVCE).getCharacteristic(SampleApplication.CHARACTERISTIC_NOTY);
gatt.setCharacteristicNotification(ch, true);

BluetoothGattDescriptor descriptor = ch.getDescriptor(SampleApplication.CCCD);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
BluetoothGattService service = bluetoothGatt.getService(SampleApplication.SERIVCE);
writeGattCharacteristic = service.getCharacteristic(SampleApplication.CHARACTERISTIC_READ_WRITE);

SharedPreferences pref = Properties.getSharedPreferences(context);
pref.edit().putString(Properties.CONNECTED_BT_ADDRESS, gatt.getDevice().getAddress()).apply();
pref.edit().putString(Properties.CONNECTED_BT_NAME, gatt.getDevice().getName()).apply();

}
}

@Override
public void onCharacteristicRead(final BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
handler.post(new Runnable() {
@Override
public void run() {
btConnectCallback.onSuccessConnect(gatt.getDevice()); // 통신 준비 완료
}
});
}

@Override
public void onDescriptorWrite(final BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
handler.post(new Runnable() {
@Override
public void run() {
btConnectCallback.onSuccessConnect(gatt.getDevice()); // 통신 준비 완료
}
});
}

/**
* 가장 중요한 메소드, ble 기기의 값을 받아온다
*/
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
handler.post(new Runnable() {
@Override
public void run() {
String value = characteristic.getStringValue(1);
value = value.replaceAll(" ", "");
value = value.substring(0, value.length() - 1);
try {
Log.d(TAG, "run: " + value);
btWriteCallback.onSuccessWrite(0, value);
} catch (IOException e) {
e.printStackTrace();
btWriteCallback.onFailed(e);
}
}
});
}
};

public interface BluetoothConnectImpl {
void onSuccessConnect(BluetoothDevice device);

void onFailed();
}

public interface BluetoothWriteImpl {
void onSuccessWrite(int status, String data) throws IOException;

void onFailed(Exception e);
}

}


어려웠던 점 : 블루투스는 연결할때 context 를 필요로 한다, 하지만 싱글톤으로 작동하려면 context에 종속되지

않아야하는데 context를 필요하니까 모순같은 상황이였다.


그래서 BaseAppliaction(Application 을 상속받은 클래스)의 context를  static 으로 선언후 그 context 를 사용


public static BaseApplication instance = null;
@Override
public void onCreate() {
super.onCreate();
instance = this;
}

context = BaseApplication.instance;


3. 통신

BluetoothModule bluetoothModule = BluetoothModule.getInstance();


bluetoothModule.sendProtocol("AG", new BluetoothModule.BluetoothWriteImpl() {
@Override
public void onSuccessWrite(int status, String data) throws IOException {
//TODO
}

@Override
public void onFailed(Exception e) {
System.out.println("알람데이터 못가져옴");
LoadingDialog.hideLoading();
}
});



참고자료 : https://blog-kr.zoyi.co/bluetooth-low-energy-ble/

   https://medium.com/@avigezerit/bluetooth-low-energy-on-android-22bc7310387a

   https://developer.android.com/guide/topics/connectivity/bluetooth-le

'Android' 카테고리의 다른 글

Rxjava + MVVM + databinding  (0) 2018.08.21
리사이클러뷰 리스트 바인딩 문제  (0) 2018.07.27
GridLayoutManager + Spacing  (0) 2017.11.20
두번 누르면 종료  (0) 2017.11.20
CustomTitlebar 재활용  (0) 2017.11.20