기존 글에 이어서 작성합니다.
3) 아두이노 셋팅
아두이노 셋팅을 해줍니다. 소프트웨어 시리얼 라이브러리를 임포트 해옵니다.
#include <SoftwareSerial.h>
SoftwareSerial BT_Serial(12, 11); // TX 송신, RX 수신 void setup() { Serial.begin(9600); BT_Serial.begin(9600); Serial.println("ATcommand"); //ATcommand Start } void loop() { while(BT_Serial.available()){ byte data=BT_Serial.read(); Serial.write(data); } while(Serial.available()){ byte data=Serial.read(); BT_Serial.write(data); } } |
우선 시리얼 모니터를 통해 컴퓨터와 BLE간의 통신을 테스트 할 겁니다.
TX, RX 를 아두이노 핀 넘버에 알맞게 껴주고 시리얼 모니터를 통해 AT 커멘드 테스를 해봅시다.
(6) AT 커멘드 테스트
AT를 쳤을 때 OK 라고 뜨면 입출력이 제대로 되고 있다는 뜻 입니다.
AT라고 입력해 보니 OK 라고 잘 뜹니다.
(혹 HC 시리즈나 BT-06, CC2541 등 다른 제품들이랑 통신할 때 안 뜬다면 아래 선택부분에서 Both NL & CR, Line ending 없음 등 설정을 바꿔보세요)
아까 만든 앱을 통해 CC41-A 와 연결 해 봅니다.. 연결이 되면 깜빡이던 LED가 불이 켜지며 멈추게 됩니다.
4) 연결 및 데이터 송수신
(1) 송수신 구조
대략적인 구조는 이렇습니다.
BLE 와 통신할 수 있는 BLE Service를 구현하고, BluetoothGattCallback 을 등록합니다.
ServiceConnection 을 통해 BLEActivity 하고 BLEService하고 연결합니다.
이 Callback 에서 BLEActivity 의 BroadcastReceiver 을 호출해서 UI를 업데이트 합니다.
(2) Service 구현
저는 서비스 구현 시 메뉴를 이용해 생성해 manifests 에 따로 등록하지 않았습니다.
바운드 서비스에 대해서 저도 잘 몰라 읽어보면서 헀습니다.
를 참조 하시면 많은 도움이 됩니다
위 링크에 있는 설명 중 일부 내용입니다.
클라이언트가 서비스에 바인드하려면 bindService()를 호출하면 됩니다. 이 때, 반드시 서비스와의 연결을 모니터링하는 ServiceConnection의 구현을 제공해야 합니다. bindService() 메서드는 값 없이 즉시 반환됩니다. 그러나 Android 시스템이 클라이언트와 서비스 사이의 연결을 생성하는 경우, 시스템은 onServiceConnected()를 ServiceConnection에서 호출하여 클라이언트가 서비스와 통신하는 데 사용할 수 있도록 IBinder를 전달합니다.
좌우지간 보자면 클라이언트가 서비스내의 공개 메서드를 호출하기 위해서 IBinder가 필요한 것 같네요.
서비스 클래스 내에서 IBinder 인터페이스 정의 - 바인더 클래스 상속 합니다
Binder의 인스턴스를 생성 -> mBinder(Localbinder)
private IBinder mBinder = new LocalBinder();
public class LocalBinder extends Binder { BLEService getService() { return BLEService.this; } } |
클라이언트에서 서비스를 바인드하면 호출 되는 onBind 콜백 메서드를 구현 합니다 (이 때 프로그래밍 인터페이스를 정의하는 IBinder 객체 반환) 여기서 리턴하는 mBinder는 방금 위에서 Binder를 상속하는 LocalBinder 클래스를 만들어줬죠.
@Override
public IBinder onBind(Intent intent) { return mBinder; } |
Localbinder 의 getService() 를 통해 클라이언트가 서비스내의 공개 메서드를 호출가능합니다.
BLEService getService() {
return BLEService.this; } |
(2) BindService
다시 액티비티로 돌아와서 서비스와 클라이언트를 연결하는 ServiceConnection 를 구현합니다.
서비스를 바인드 설정한 후 ServiceConection에서 서비스내의 LocalBinder 의 getService를 통해 호출합니다.
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { mBLEService = ((BLEService.LocalBinder) iBinder).getService(); // 클라이언트가 서비스내의 공개 메서드를 호출하기 위해 if (mBLEService.initialize()) { Log.d(TAG, "BLE Service Connected"); } } |
서비스를 바인드합니다.
private void settingUpBLE() {
... Intent gattServiceIntent = new Intent(getApplicationContext(), BLEService.class); bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE); } |
(3) 서비스 내부 메소드 설정
서비스 내부에서 GattCallback 구현합니다. 디바이스와 클라이언트가 connect 될 때 해당 콜백 메소드를 연결 할 것입니다.
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String intentAction; if (newState == BluetoothProfile.STATE_CONNECTED) { intentAction = ACTION_GATT_CONNECTED; Log.i(TAG, "GATT 서버에 접속"); broadcastUpdate(intentAction); mBluetoothGatt.discoverServices(); Log.i(TAG, "Services Discover 함"); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { intentAction = ACTION_GATT_DISCONNECTED; Log.i(TAG, "Disconnected from GATT server."); broadcastUpdate(intentAction); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.i(TAG, "GATT 서비스 발견 : " + status); broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); } } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicWrite(gatt, characteristic, status); Log.d(TAG, "onCharacteristicWrite 보냈음"); } }; |
콜백에서 호출하는 broadcastUpdate, sendBroadcast 도 함께 구현해 줍니다.
private void broadcastUpdate(final String action) {
final Intent intent = new Intent(action); sendBroadcast(intent); } private void broadcastUpdate(final String action, final BluetoothGattCharacteristic characteristic) { final Intent intent = new Intent(action); final byte[] data = characteristic.getValue(); if (data != null && data.length > 0) { final StringBuilder stringBuilder = new StringBuilder(data.length); for (byte byteChar : data) stringBuilder.append(String.format("%02X ", byteChar)); // String.format %02X 는 2자리 16진수 대문자로 출력, 만약 1자리면 0으로 채움 intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString()); } sendBroadcast(intent); } |
엑티비티의 ServiceConnection 에서 호출할 initialize 메소드 구현합니다.
블루투스매니저와 아답터를 가져옵니다.
public boolean initialize() {
if (mBluetoothManager == null) { mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (mBluetoothManager == null) { Log.e(TAG, "Unable to initialize BluetoothManager."); return false; } } mBluetoothAdapter = mBluetoothManager.getAdapter(); if (mBluetoothAdapter == null) { Log.e(TAG, "Unable to obtain a BluetoothAdapter."); return false; } return true; } |
엑티비티에서 호출 할 connectDevice 메소드를 만들고, connectGatt 에서 GattCallback 을 등록합니다
public boolean connectDevice(String address) {
if (mBluetoothAdapter != null && address != null) { final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); Log.d(TAG, "연결 시도 중"); mBluetoothGatt = device.connectGatt(this, false, mGattCallback); return true; } else { Log.d(TAG, "연결 실패 아답터 없거나 주소 null"); return false; } } |
(4) GattBroadCastReceiver 구현
GattCallback에서 sendbroadcast 를 리시브 하는 GattBroadCastReciever 구현합니다.
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BLEService.ACTION_GATT_CONNECTED.equals(action)) { deviceStates.set(curDevicePosition, "연결 완료"); bleAdapter.notifyDataSetChanged(); } else if (BLEService.ACTION_GATT_DISCONNECTED.equals(action)) { deviceStates.set(curDevicePosition, "연결 실패"); bleAdapter.notifyDataSetChanged(); } else if (BLEService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { displayGattServices(mBLEService.getSupportedGattServices()); } else if (BLEService.ACTION_DATA_AVAILABLE.equals(action)) { displayData(intent.getStringExtra(BLEService.EXTRA_DATA)); } } }; |
(5) Read, Write 구현
이제 어느정도 준비는 마쳤고, 앱파트에서는 거의 마지막 부분입니다.
GATT 콜백 부분을 다시 주목하세요
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { ...
mBluetoothGatt.discoverServices();
...
} @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { ... } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { ... } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { ... } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { ... } }; |
서비스내부의 connectDevice 메소드를 통해 연결이 되면
Gatt콜백 내부의 onConnectionStateChange 가 호출이 됩니다.
그리고 이 후에 onServicesDiscovered 가 호출 됩니다.
이때 많이 빼먹는 부분이 있는데, mBluetoothGatt.discoverServices() 을 해주셔야 합니다.
discoverServices()가 onServiceDiscoverd 의 트리거이기 때문에 이걸 안 해주시면 디바이스와 연결은 되어도 onServicesDiscovered 가 호출이 안되며 그 이후 작업을 할 수가 없습니다.
공식 가이드에는 discoverServices()가 로그로 되어 있어서 무시하고 지나갈 수 있는데, 로그로라도 실행을 시켜주지 않으면 이후 onCharacteristicRead, onCharacteristicChanged 가 호출 되지 않아 데이터 주고 받기가 불가능 해집니다.
이제 onServiceDiscovered가 되면 브로드 캐스트 리시버에서 다시 displayGattServices() 를 통해 mBluetoothGatt.setCharacteristicNotification 과 mBluetoothGatt.readCharacteristic, writeCharacteristic 을 설정 해 줍니다.
private void displayGattServices(List<BluetoothGattService> gattServices) {
for (BluetoothGattService gattService : gattServices) { mGattServices.add(gattService); // 나중에 write 할때 gattService 를 구하기 위해 저장 List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics(); for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { // BluetoothGattCharacteristic 에는 읽기 가능한 곳과 쓰기 가능한 곳, 알림이 가능한 곳이 있음 // 이걸 chr.BluetoothGattCharacteristic.getProperties() 를 통해 알아낼 수 있음 // 그래서 Writable, Readable, Notification 용 BluetoothGattCharacteristic 을 구해서 BluetoothGatt 에 사용함 boolean readable = mBLEService.isReadableCharacteristic(gattCharacteristic); if (readable) { mBLEService.setReadCharacteristics(gattCharacteristic); } boolean writable = mBLEService.isWritableCharacteristic(gattCharacteristic); if (writable) { writableGattCharacteristics.add(gattCharacteristic); // 지금 쓸께 아니고 나중에 쓸꺼니까 저장해두기 } boolean isNotificationCharacteristic = mBLEService.isNotificationCharacteristic(gattCharacteristic); if (isNotificationCharacteristic) { mBLEService.setCharacteristicNotification(gattCharacteristic, true); } } } } |
mBluetoothGatt.readCharacteristic 는 BLE 디바이스의 메모리에 있는 정보들을 읽어오는 것이고 setCharacteristicNotification 를 통해 변경되는 정보를 얻을 수 있습니다.
즉 아두이노의 시리얼 모니터에서 입력해서 BLE 내부의 메모리 값이 변경되면 거기서 변경된 값을 읽어오는 것입니다.
그리고 BLE 내부에서 정보를 읽고 쓰고 노티피케이션을 담당하는 부분이 따로 있는 것 같습니다.
getProperties() 를 통해서 해당 영역이 읽기, 쓰기가 가능한지 노티를 담당하는 곳 인지 확인하여 setCharacteristicNotification, readCharacteristic, 에 등록합니다. (앞에 쓰기를 담당하는 부분을 위해서는 writeCharacteristic 같은 경우는 지금 사용 할 것이 아니니까, 전역변수에 저장해 두었다. 나중에 데이터를 전송할 일이 있을 때 사용합니다.)
public boolean isReadableCharacteristic(BluetoothGattCharacteristic bleChar) {
if(bleChar == null) return false; final int charaProp = bleChar.getProperties(); if((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { return true; } else { return false; } } |
public void setReadCharacteristics(BluetoothGattCharacteristic characteristic) {
if (mBluetoothGatt != null && mBluetoothAdapter != null){ mBluetoothGatt.readCharacteristic(characteristic); } } public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) { if (mBluetoothGatt != null && mBluetoothAdapter != null){ mBluetoothGatt.setCharacteristicNotification(characteristic, enabled); } } |
이제 GattCallback 에서 노티가 호출되었을때 호출되는 메소드 -> 브로드 캐스트 보내기 -> 리시버에서 받기 -> UI 업데이트를 진행해주면 됩니다.
BLE로 보내는 부분은 아까 저장해둔 writeCharacteristic 을 이용해 보내면 됩니다.
private void sendCharacteristic() {
BluetoothGattCharacteristic gattCharacteristic = null; for (BluetoothGattCharacteristic characteristic : writableGattCharacteristics) { if (mBLEService.isWritableCharacteristic(characteristic)) { gattCharacteristic = characteristic; } } String sampleMsg = "hi"; if (gattCharacteristic != null) { gattCharacteristic.setValue(sampleMsg.getBytes()); gattCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); mBLEService.setWriteCharacteristics(gattCharacteristic); } else { Log.d(TAG, "쓰기가 불가능한 BluetoothGattCharacteristic"); } } |
public void setWriteCharacteristics(BluetoothGattCharacteristic characteristic) {
if (mBluetoothGatt != null && mBluetoothAdapter != null){ mBluetoothGatt.writeCharacteristic(characteristic); } } |
이제 읽기와 쓰기는 완료했습니다~ 사실 이후 블루투스 멀티 연결도 시도해보려고 했는데, 시간이 부족하네요. 현재 코드에 connectDevice 할 때 BLE Address 를 여러개 넣어서 연결하면 되긴 됩니다. 그런데 서비스, GattBLE 와 연결 할 때 언바인드나 여러가지를 제대로 컨트롤 하는 부분은 조금 더 연구를 해봐야 할 것 같네요.
5) BLE Wifi 연결
이제 아두이노 파트로 다시 와서 클라이언트에서 보내준 와이파이와 비밀번호 정보를 받아서 이제 접속하는 일만 남았습니다.
Wifi를 접속 할 NodeMCU를 준비합니다. NodeMCU에 BLE를 연결합니다.
IDE에서 보드셋팅을 NodeMCU 1.0 으로 맞춰주세요. 혹 USB 드라이버가 깔려있지 않다면 깔아주셔야 합니다.
NodeMCU 핀맵입니다. 저는 아두이노부터 마구잡이로 시작하였는데, 아직 데이터시트나 핀맵을 잘 볼 줄 모릅니다. 대략 GPIO3, GPIO1 이 RXD0, TXD0 이고 D9, D10에 해당하며 하드웨어 시리얼로 사용되는 것 같습니다.
저희는 BLE와 SoftwareSerial로 통신할 것이기 때문에 해당핀 말고 다른 핀을 이용합니다. 저는 D5, D6을 이용했습니다. 코드를 입력합니다.
#include <SoftwareSerial.h>
SoftwareSerial swSer(D5, D6); void setup() { Serial.begin(9600); swSer.begin(9600); Serial.println("\nSoftware serial test started"); } void loop() { while(swSer.available()){ byte data=swSer.read(); Serial.write(data); } while(Serial.available()){ byte data=Serial.read(); swSer.write(data); } } |
AT 커멘드를 입력해보니 잘 동작합니다. 어플리케이션을 켜서 NodeMCU + BLE에 데이터를 송신 후 시리얼 모니터에서 출력해보니 잘 뜨네요.
BLE와 통신 테스트를 했으니, 클라이언트에서 정보를 쏘면 받아서 와이파이에 접속하게 하는 코드를 만듭니다. 클라이언트에서 데이터를 보내면 readBytes를 써서 receiveChar에 저장하고 String으로 변환해 recieveStr에 저장합니다.
저는 클라이언트에서 데이터를 보내줄때 “/” 로 시작하면 와이파이 SSID 이게 설정하였습니다. 또한 BT_Serial.write() 을 이용해 클라이언트에게 디바이스 상황을 알려주며 통신합니다.
char receiveChar[20];
String receiveStr;
String wifiNameStr;
String wifiPassStr;
boolean wifiExist;
boolean passExist;
while(BT_Serial.available()) {
BT_Serial.readBytes(receiveChar, 20); // readBytes() 를 쓰면 버퍼를 마지막에 한번만 } receiveStr = String(receiveChar); // 받은 char 값을 String 으로 변경 if (receiveStr.startsWith("/")) { // 받은 문자가 / 로 시작할때는 와이파이 Name 임 wifiNameStr = receiveStr; wifiNameStr.remove(0,1); Serial.print("Wifi Name : "); Serial.println(wifiNameStr); for (int i = 0 ; i < 20 ; i++) { // 빈 배열로 초기화 해줌 receiveChar[i] = '\0'; } BT_Serial.write("ok"); wifiExist = true; } else if (receiveStr.startsWith("?")) { wifiPassStr = receiveStr; wifiPassStr.remove(0,1); Serial.print("Wifi Pass : "); Serial.println(wifiPassStr); for (int i = 0 ; i < 20 ; i++) { // 빈 배열로 초기화 해줌 receiveChar[i] = '\0'; } BT_Serial.write("ok PW"); passExist = true; } if (wifiExist && passExist) { Serial.println("Try Connecting"); wifiExist = false; passExist = false; connectWifi(); } |
라이브러리를 임포트 해오고 Wifi에 접속합니다.
#include <ESP8266WiFi.h>
void connectWifi() {
WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(500); } Serial.println(); Serial.print("connected: "); Serial.println(WiFi.localIP()); BT_Serial.write("Connect Wifi"); } |
아래는 결과 화면 입니다.
자 이렇게 모든 과정을 다 했습니다.
코딩을 하면서 글을 쓰다 보니 시간이 너무 오래걸려서, 구현을 다하고 글을 썼는데, 설명이 너무 조잡하고, 상세하지 못하네요..
코딩 글은 쓰지 말아야겠습니다..
댓글 없음:
댓글 쓰기