📱Mobile/React Native

[React Native] Android BLE Advertising

뉴발자 2023. 9. 14.
728x90

 

 

 

 

 

 

 

 

 

 

 

 

 

 

[React Native] Android BLE Advertising
BLE Advertising

 

 

BLE 란?

Bluetooth Low Energy의 줄임말로, 저전력 블루투스 라고 한다.

 

Bluetooth는 크게 Bluetooth Classic과 BLE로 구분된다.

 

Bluetooth 3.0 까지는 Bluetooth Classic, Bluetooth 4.0 부터는 BLE로 불린다.

 

Bluetooth Classic은 다른 기기와 단거리 에서 무선으로 연결되어 통신되는 엄청난 편리함을 주었지만

 

배터리 소모로 인해사용하는데 불편함이 있었다.

 

그리고 쌍방향 통신만 가능했기 때문에 데이터를 수신하기 위해서는 항상 기기가 대기상태여야 했다.

 

2010년 새로운 Bluetooth 표준으로 Bluetooth 4.0이 채택되었다.

 

Bluetooth Classic과의 가장 큰 차이는 훨씬 적은 전력으로 Classic과 비슷한 수준의 무선 통신을 할 수 있다는 점이다.

 

그리고 단방향 통신이 가능해졌기 때문에 데이터를 수신하기 위해 대기상태일 필요도 없어졌다.

 

이러한 방법으로 '비콘(Beacon)'은 이전의 Bluetooth 장치와 같이 페어링을 필요로 하지 않게 되었고,

새로운 활용법들을 가지게 되었다.

 

참고 사이트

https://blog.naver.com/ycpiglet/222630970457

 

저전력 블루투스 BLE(Bluetooth Low Energy)란?

저전력 블루투스를 뜻하는 BLE는 블루투스에서 확장된 개념으로 이해할 수 있다. 따라서 먼저 블루투스......

blog.naver.com

 

 

코드 작성 계기

BLE를 Advertising하는 코드를 작성하던 도중 구글의 자료들은 UUID를 필수로 입력해야 하는 코드밖에 없었다.

 

하지만 UUID 없이 Manufaturer Data만을 담아서 Advertising 하는 코드가 필요했다.

 

그래서 Android 공식 홈페이지를 참고하여 직접 코드를 작성하게 되었다.

 

코드

1. BLE 권한 추가

android/app/src/main/AndroidManifest.xml 파일 중간에 아래의 코드를 추가한다.

  <manifest
    ...
  >
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
    <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_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
    
    <application>
      ...
    </application>
  </manifest>

 

ACCESS_FINE_LOCATION - BLE를 사용하기 위한 위치 권한

 

ACCESS_COARSE_LOCATION - BLE를 사용하기 위한 위치 권한 (Android sdk 28 버전 이하)

 

BLUETOOTH - 앱에서 블루투스 기능을 사용하기 위한 권한 (Android sdk 30 버전 이하)

 

BLUETOOTH_ADMIN - 앱이 기기 검색을 시작하거나 블루투스 설정을 조적할 수 있는 권한 (Android sdk 30 버전 이하)

 

BLUETOOTH_SCAN - 주변 블루투스 기기 검색을 위한 권한

 

BLUETOOTH_CONNECT - 이미 페어링된 기기와 통신하기 위한 권한

 

BLUETOOTH_ADVERTISING - 현재 기기를 다른 기기에서 검색할 수 있도록 하기위한 권한

 

bluetooth_le - 앱이 저전력 블루투스(BLE) 지원 기기에만 제공된다고 선언 (android:required="true")

 

 

2. 모듈 파일 생성 및 코드 작성

android/app/src/main/package명 아래에 BeaconModule.java 파일을 생성한 후 아래의 코드를 작성해준다.

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import android.util.Log;

public class BeaconModule extends ReactContextBaseJavaModule {
  private ReactApplicationContext reactContext;

  public BeaconModule(ReactApplicationContext reactContext) {
    super(reactContext);
    this.reactContext = reactContext;
  }

  // React-Native 로 내보내는 명칭
  @Override
  public String getName() {
    return "BeaconModule";
  }

  // 광고 데이터 송신 시작
  @ReactMethod
  public void startAdvertising(String manufaturerData) {
    Log.d("BeaconModule", "StartAdvertising");
    MainApplication.getInstance().startAdvertising(manufaturerData);
  }

  // 광고 데이터 송신 중지
  @ReactMethod
  public void stopAdvertising() {
    Log.d("BLE", "StopAdvertising");
    MainApplication.getInstance().stopAdvertising();
  }
}

 

 

3. Package 파일 생성 및 코드 작성

모듈 파일과 동일한 경로(android/app/src/main/package명) 아래에 MyAppPackage.java파일을 생성한 후 아래의 코드를 작성해준다.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;

public class MyAppPackage implements ReactPackage {

  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }

  @Override
  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();

    // 생성한 커스텀 패키지 추가 (BeaconModule)
    modules.add(new BeaconModule(reactContext));

    return modules;
  }
}

 

 

4. MainApplication 코드 추가 작성

패키지 폴더 안의 MainApplication.java 파일의 코드를 아래와 같이 작성해준다.

import android.Manifest;
import android.app.Application;
import android.content.pm.PackageManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.util.Log;

import androidx.core.app.ActivityCompat;

import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.soloader.SoLoader;
import java.util.List;

public class MainApplication extends Application implements ReactApplication {
  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    public boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

    @Override
    protected List<ReactPackage> getPackages() {
      @SuppressWarnings("UnnecessaryLocalVariable")
      List<ReactPackage> packages = new PackageList(this).getPackages();
      // Packages that cannot be autolinked yet can be added manually here, for
      // example:
      packages.add(new MyAppPackage());
      return packages;
    }

    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }
  
  private static MainApplication instance;
  private BluetoothLeAdvertiser advertiser;

  // BLE Advertising 성공, 실패 시 Callback 함수
  private AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
    @Override
    public void onStartSuccess(AdvertiseSettings settingsInEffect) {
      super.onStartSuccess(settingsInEffect);
      Log.d("MainApplication", "Advertising Start Success : " + settingsInEffect);
    }

    @Override
    public void onStartFailure(int errorCode) {
      super.onStartFailure(errorCode);
      Log.d("MainApplication", "Advertising Failed : " + errorCode);
    }
  };

  public static MainApplication getInstance() {
    return instance;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    instance = this;

    SoLoader.init(this, /* native exopackage */ false);
    if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
      // If you opted-in for the New Architecture, we load the native entry point for this app.
      DefaultNewArchitectureEntryPoint.load();
    }
    ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
  }

  // 받아온 manufaturer string data -> Byte 변환 함수
  public byte[] hexToBytes(String manufacturerData) {
    byte[] result = null;

    if (manufacturerData != null) {
      result = new byte[manufacturerData.length() / 2];

      for (int i = 0; i < result.length; i++) {
        result[i] = (byte) Integer.parseInt(manufacturerData.substring(2 * i, 2 * i + 2), 16);
      }
    }

    return result;
  }

  // 광고 데이터 송신 시작
  public void startAdvertising(String manufacturerData) {
    Log.i("MainApplication", "BLE ADVERTISING START");
    try {
      if (ActivityCompat.checkSelfPermission(this,
          Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
        return;
      }
      
      if (ActivityCompat.checkSelfPermission(this,
          Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
        return;
      }

      advertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();

      byte[] byteData = hexToBytes(manufacturerData);

      AdvertiseData ad_data = new AdvertiseData.Builder()
        .setIncludeDeviceName(true)
        .addManufacturerData(0xFFFF, byteData) // Use custom manufacturer data
        .build();

      AdvertiseSettings ad_settings = new AdvertiseSettings.Builder()
        .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
        .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
        .setConnectable(false)
        .build();

      advertiser.startAdvertising(ad_settings, ad_data, advertiseCallback);
    } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
    }
  }

  // 광고 데이터 송신 중지
  public void stopAdvertising() {
    Log.i("MainApplication", "BLE ADVERTISING STOP");
    if (ActivityCompat.checkSelfPermission(this,
        Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
      return;
    }
    
    if (ActivityCompat.checkSelfPermission(this,
        Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
      return;
    }

    if (advertiser != null){
      advertiser.stopAdvertising(advertiseCallback);
      advertiser = null;
    }
  }
}

 

 

5. React Native에서 Android 코드 사용하기

src/module/nativeModule.ts

import { NativeModules } from "react-native";

export interface AndroidBeaconModule {
  startAdvertising: (hexString: string) => void;
  stopAdvertising: () => void;
};

export default {
  ...NativeModules.BeaconModule as AndroidBeaconModule, 
};

 

src/hooks/useBluetooth.ts

import { useState, useEffect, useCallback } from "react";
import nativeModules from "../module/nativeModules";
import BackgroundTimer from "react-native-background-timer";

const {
  startAdvertising,
  stopAdvertising,
} = nativeModules;

const useBluetooth = () => {
  const [isTransmitter, setIsTransmitter] = useState(false);
  const transmitterMs = 5000;

  // 광고 데이터 송신 시작 함수
  const onStartAdvertising = useCallback((hexString: string) => {
    setIsTransmitter(true);

    try {
      startAdvertising(hexString);
      
    } catch (error) {
      console.log(error);
    }
    
  }, []);

  // 지정한 시간 후에 BLE 광고를 종료한다.
  useEffect(() => {
    if (isTransmitter) {
      const timer = BackgroundTimer.setTimeout(() => {
        setIsTransmitter(false);

        // 광고 데이터 송신 종료 함수
        stopAdvertising();
      }, transmitterMs);

      return () => BackgroundTimer.clearTimeout(timer);
    }
  }, [isTransmitter, transmitterMs]);

  return {
    onStartAdvertising,
  };
};

export default useBluetooth;

 

App.tsx

import { TouchableOpacity, View, Text } from "react-native";
import useBluetooth from "./src/hooks/useBluetooth";

const ViewStyle = {
  flex: 1,
  justifyContent: "center",
  alignItems: "center",
};

const ButtonStyle = {
  backgroundColor: "red",
  padding: 10,
  border: "1px solid black",
  borderRadius: 5
};

const TextStyle = {
  color: "white",
};

const App = () => {
  const { onStartAdvertising } = useBluetooth();
  
  const bleAdvertising = () => {
    const manufacturerData = "1111111111";

    onStartAdvertising(manufacturerData);
  };

  return (
    <View
      style={ViewStyle}
    >
      <TouchableOpacity
        onPress={bleAdvertising}
        style={ButtonStyle}
      >
        <Text style={TextStyle}>BLE 광고</Text>
      </TouchableOpacity>
    </View>
  );
};

export default App;

 

 

6. 테스트 및 데이터 확인

[React Native] Android BLE Advertising - 코드 - 6. 테스트 및 데이터 확인
그림 6-1. App 메인 화면
[React Native] Android BLE Advertising - 코드 - 6. 테스트 및 데이터 확인
그림 6-2. nRFConnect BLE Advertising 데이터

 

! 혹시라도 동작하지 않는다면 어플의 Bluetooth와 위치 서비스가 켜져있는지 확인해 보도록 하자

 

 

Android에서 Advertising하는 코드는 구현했지만, iOS에서 UUID없이 Manufacturer Data를 담아서 Advertising하는 코드는 구현하지 못했다.

 

현재 iOS의 CoreBluetooth는 Manufacturer Data를 가공할 수 없게 막아놓은 상태이다.

 

https://stackoverflow.com/questions/15780267/the-advertisement-key-manufacturer-data-is-not-allowed-in-corebluetooth

 

The advertisement key 'Manufacturer Data' is not allowed in CoreBluetooth

I am working with the core bluetooth framework . I am trying to create the peripheral using this framework . My peripheral advertise the data using : manager=[[CBPeripheralManager alloc]initWithDe......

stackoverflow.com

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90

댓글