Checker 11.20 BLE + Wifi 설정 2탄

기존 글에 이어서 작성합니다.

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");
}


아래는 결과 화면 입니다.


자 이렇게 모든 과정을 다 했습니다.


코딩을 하면서 글을 쓰다 보니 시간이 너무 오래걸려서, 구현을 다하고 글을 썼는데, 설명이 너무 조잡하고, 상세하지 못하네요..

코딩 글은 쓰지 말아야겠습니다..

댓글 없음:

댓글 쓰기

BatteCoins 프로젝트

BattleCoins 17.12.26 요 몇 일 전부터 코인 매니저인 송대표가 찾아왔습니다. 그리고 이런저런 이야기를 나누다가 지금 진행중인 Checker를 잠깐 일시정지하고 코인 관련 아이템을 짧게 진행해보기로 했습니다. 그...