졸업 프로젝트

[React Native - Expo] BLE 블루투스 기능 _ (2) 권한 설정, 디바이스 스캔

yyyujinnn 2024. 11. 5. 02:46

제가 원하는 기능은

스마트폰 디바이스끼리 블루투스 연결해서 정수를 전달하는 기능

입니다.

 

자세히는

발신자 - 정수 ID값 전달

수신자 - 받은 ID값으로 API 요청하기


 

- 권한 설정 전 패키지 다운로드

import * as Device from 'expo-device';

 

- App.json - 안드로이드에 블루투스위치 권한 추가 -> 재빌드 해야함

{
  "expo": {
    "android": {
      "package": "com.teambenu.ssop",
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "permissions": [
        "READ_CONTACTS",
        "WRITE_CONTACTS", 
        "BLUETOOTH",
        "BLUETOOTH_ADMIN",
        "ACCESS_FINE_LOCATION",
        "ACCESS_COARSE_LOCATION"
      ]
    },
    "plugins": [
      [
        "react-native-ble-plx",
        {
          "isBackgroundEnabled": true,
          "modes": [
            "peripheral",
            "central"
          ],
          "bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to bluetooth devices"
        }
      ]
    ]
  }
}

 

  • Managed Workflow (Expo Go): Expo에서 제공하는 API와 라이브러리만 사용할 수 있으며, 네이티브 모듈을 사용할 수 없습니다.
  • Bare Workflow (npx expo run): 필요한 모든 네이티브 모듈을 사용할 수 있으며, react-native-ble-manager와 같은 외부 라이브러리를 자유롭게 사용할 수 있다.

 

계속 이런 오류가 발생해서

React Native와 관련된 네이티브 모듈을 사용하기 위해

expo를 나갔다!

expo eject
npx expo run:android

 

expo eject를 하면서 ios/android 관련 파일이 추가되는데 배포할 때 필요할까? 그렇다

  • 네이티브 코드 유지: 이젝트 과정에서 생성된 모든 파일과 변경 사항은 앱이 정상적으로 작동하는 데 필요하다.
                                     따라서 배포 전에 반드시 포함해야 합니다.
  • 테스트: 변경된 코드와 추가된 파일을 포함하여 앱을 충분히 테스트하여 배포 전 문제가 없도록 해야 한다.

결론적으로, 배포할 때는 이젝트 후 추가된 파일과 변경된 코드가 필수적입니다. 앱이 정상적으로 작동하도록 보장하기 위해 모든 변경 사항을 포함해야 합니다.


- 권한 설정 -> BluetoothRequestPermission.js ( 파일명이 길지만 수많은 파일 사이에서 알아보려면.. )

import { PermissionsAndroid, Platform } from 'react-native';
import * as ExpoDevice from 'expo-device';

const requestAndroid31Permissions = async () => {
    const bluetoothScanPermission = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
        {
            title: "블루투스 스캔 권한 요청",
            message: "블루투스 기능을 사용하려면 위치 권한이 필요합니다.",
            buttonPositive: "OK",
        }
    );
    const bluetoothConnectPermission = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
        {
            title: "블루투스 연결 권한 요청",
            message: "블루투스 기능을 사용하려면 위치 권한이 필요합니다.",
            buttonPositive: "OK",
        }
    );
    const fineLocationPermission = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
        {
            title: "위치 권한 요청",
            message: "블루투스 기능을 사용하려면 위치 권한이 필요합니다.",
            buttonPositive: "OK",
        }
    );

    return (
        bluetoothScanPermission === "granted" &&
        bluetoothConnectPermission === "granted" &&
        fineLocationPermission === "granted"
    );
};

const BluetoothRequestPermissions = async () => {
    if (Platform.OS === "android") {
        if ((ExpoDevice.platformApiLevel ?? -1) < 31) {
            const granted = await PermissionsAndroid.request(
                PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
                {
                    title: "위치 권한 요청",
                    message: "블루투스 기능을 사용하려면 위치 권한이 필요합니다.",
                    buttonPositive: "OK",
                }
            );
            return granted === PermissionsAndroid.RESULTS.GRANTED;
        } else {
            const isAndroid31PermissionsGranted = await requestAndroid31Permissions();
            return isAndroid31PermissionsGranted;
        }
    } else {
        return true; // iOS에서는 모든 권한이 허용된 것으로 가정
    }
};

export default BluetoothRequestPermissions;

 

- Bluetooth.js


import BluetoothRequestPermissions from "../../ble/BluetoothRequestPermissions.js";
import useBLE from '../../ble/useBLE.js'
import Toast from "react-native-toast-message";

function Step1Screen() {
  const [permissionGranted, setPermissionGranted] = useState(null); // 권한 상태
 
  // 블루투스 권한 요청
  useEffect(() => {
    const checkPermissions = async () => {
      console.log('권한 요청 시작');
      const permissionsGranted = await BluetoothRequestPermissions();
      console.log('권한 요청 결과:', permissionsGranted);
      setPermissionGranted(permissionsGranted); // 권한 상태 업데이트

    checkPermissions();
    fetchCardData();
  }, []);
  
  // 카드 선택하여 cardId를 파라미터로 같이 전송
  const handleNext = (cardId) => {
    if (permissionGranted) {  // 상태로 설정된 permissionGranted를 직접 참조
      // 권한이 허용된 경우 Step2Screen으로 이동
      navigation.navigate('Step2', { permissionGranted, cardId });
    } else {
      Alert.alert('권한 요청', '권한이 거부되었습니다.');
      console.log('권한이 거부되었습니다.');
    }
  };
}

function Step2Screen({ route }) {
 const { permissionGranted = false, cardId } = route.params || {};

  const [recipients, setRecipients] = useState([]);
  const [recipientStatuses, setRecipientStatuses] = useState({});
  const { scanForPeripherals, allDevices } = useBLE();
  const [isScanning, setIsScanning] = useState(false);

  useEffect(() => {
    const startScanning = async () => {
      if (permissionGranted && !isScanning) {
        setIsScanning(true);
        await scanForPeripherals(); // 스캔 메서드
      }
    };
    startScanning();
  }, [permissionGranted, isScanning]);

  useEffect(() => {
    setRecipients(allDevices || []); // 스캔한 모든 디바이스 저장
  }, [allDevices]);
}

 


 

- 블루투스 디바이스 스캔 -> useBLE.js

import React, { useEffect, useState, useRef } from 'react';
import { BleManager } from 'react-native-ble-plx';
import BluetoothRequestPermissions from './BluetoothRequestPermissions';

const useBLE = () => {
  const bleManager = useRef(new BleManager()).current;
  const discoveredDevices = useRef(new Map()); // 장치를 저장할 Map
  const [allDevices, setAllDevices] = useState([]); // 연결 가능한 주변 Bluetooth 장치 목록
  const [isScanning, setIsScanning] = useState(false); // 스캔 상태

  // 디바이스 스캔
  const scanForPeripherals = async () => {
    if (bleManager.isScanning) {
      console.log("현재 스캔 중입니다.");
      return;
    }

    discoveredDevices.current.clear(); // 새로운 스캔 시작 시 기존 장치 목록 초기화
    setIsScanning(true); // 스캔 시작 표시

    bleManager.startDeviceScan(null, null, (error, device) => {
      if (error) {
        // console.error("Scan error:", error.reason); -> 사실 오류납니다... 추후 수정
        return;
      }

      if (device && device.name) {
        // 장치 이름이 있을 경우에만 처리
        if (!discoveredDevices.current.has(device.id)) {
          discoveredDevices.current.set(device.id, device);

          // 상태 업데이트 (중복 없이)
          setAllDevices((prevDevices) => {
            // 장치가 기존 목록에 없으면 추가
            if (!prevDevices.some(prevDevice => prevDevice.id === device.id)) {
              console.log(`새로운 장치 추가: ${device.name}`);
              return [...prevDevices, device]; // 새로운 배열 반환
            }
            return prevDevices; // 중복된 경우 기존 배열 반환
          });
        } else {
          // 이미 발견된 장치
        }
      } else if (device) {
        // 장치 이름 없음
      }
    });
    // 2초 후 스캔 중지
    const timeoutId = setTimeout(() => {
      bleManager.stopDeviceScan();
      setIsScanning(false); // 스캔 종료 표시
      console.log("스캔 종료 (타임아웃)");
    }, 2000);

    return () => {
      clearTimeout(timeoutId); // 클린업 함수에서 타임아웃 제거
      bleManager.stopDeviceScan(); // 스캔 중지
      setIsScanning(false); // 스캔 종료 표시
    };
  };

  useEffect(() => {
    const initializeBLE = async () => {
      const permissionGranted = await BluetoothRequestPermissions();
      console.log("권한 요청 결과:", permissionGranted);

      if (permissionGranted) {
        console.log("Bluetooth 권한이 부여되었습니다.");
        scanForPeripherals(); // 스캔 시작
      } else {
        console.log("Bluetooth 권한이 거부되었습니다.");
      }
    };

    initializeBLE();

    return () => {
      if (isScanning) {
        bleManager.stopDeviceScan();
        setIsScanning(false); // 스캔 종료 표시
        console.log("스캔 종료 (클린업)");
      }
    };
  }, []);

  return {
    allDevices,
    scanForPeripherals,
  };
};

export default useBLE;