캡스톤디자인/모바일 프론트엔드

Flutter 1. ESP32와 블루투스 통신하기

조찬국 2024. 3. 28. 12:44
728x90

1. VScode에서 Flutter 프로젝트 생성

 

1. `Ctrl`+`shift`+`P` 누른 후 `>flutter`라 입력을 한다. 그 후 `Appliaction` 선택

 

2. Flutter 프로젝트를 저장할 디렉토리에 이름을 설정해서 만든다.

 

3. 프로젝트명까지 입력하면 기본코드와 함께 Flutter 프로젝트가 생긴다.

 

<프로젝트 명 입력>

 

<기본 코드>

 

run 클릭 후, 원하는 애뮬레이터 선택.

 

 

실행 후, 오른쪽 +버튼 클릭 후에 값이 증가 함을 보면된다. 이제 기본 설정이 끝났으니, 본격적인 개발을 들어가 보겠다.

 

 

2. Flutter BLE 기본 설정(안드로이드만)

참고용 블로그

https://m.blog.naver.com/chandong83/222850757364

 

 

<실행 환경>

flutter sdk: 3.16.4
dart 3.2.3
flutter_blue_plus: 1.3.0
android compile sdk: 34
android kotlin version: 1.7.10

 

view->terminal 후 `flutter doctor` 입력

 

`AndroidManifest.xml`

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!--블루투스 권한-->
    <uses-permission android:name="android.permission.BLUETOOTH" />  
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <!-- SDK 31 이상을 타겟팅 하는 앱 Bluetooth 권한 (Android 12+) -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />        <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
    <uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
    
    <application
        android:label="taba"

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

모바일 폰에서 직접 디버깅 하는법(애뮬레이터 사용x)

1. USB 연결

2. 설정->개발자 옵션

3. 

 

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:taba/device_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  final title = 'Flutter BLE Scan Demo';

  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: title,
      home: MyHomePage(title: title),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // // 장치명을 지정해 해당 장치만 표시되게함
  final String targetDeviceName = 'ESP32 Force Sensor';

  // FlutterBluePlus flutterBlue = FlutterBluePlus();
  // List<BluetoothDevice> connectedDevices = [];
  List<ScanResult> scanResultList = [];
  bool _isScanning = false;

  @override
  initState() {
    super.initState();
    // 블루투스 초기화
    // 블루투스 초기화
    initBle();
  }

  // BLE 스캔 상태 얻기 위한 리스너
  void initBle() {
    // BLE 스캔 상태 얻기 위한 리스너
    FlutterBluePlus.isScanning.listen((isScanning) {
      _isScanning = isScanning;
      setState(() {});
    });
  }

  /*
  스캔 시작/정지 함수
  */
  scan() async {
    if (!_isScanning) {
      // 스캔 중이 아니라면
      // 기존에 스캔된 리스트 삭제
      scanResultList.clear();
      // 스캔 시작, 제한 시간 60초
      FlutterBluePlus.startScan(timeout: const Duration(seconds: 60));

      // 스캔 결과 리스너
      FlutterBluePlus.scanResults.listen((results) {
        // List<ScanResult> 형태의 results 값을 scanResultList에 복사
        scanResultList = results;
        // UI 갱신
        setState(() {
          _isScanning = true;
        });
      });
    }
  }

  Future<void> stopScan() async {
    setState(() {
      _isScanning = false;
    });
    await FlutterBluePlus.stopScan();
  }

  /*
   여기서부터는 장치별 출력용 함수들
  */

  /*  장치의 신호값 위젯  */
  Widget deviceSignal(ScanResult r) {
    return Text(r.rssi.toString());
  }

  /* 장치의 MAC 주소 위젯  */
  Widget deviceMacAddress(ScanResult r) {
    return Text(r.device.remoteId.toString());
  }

  /* 장치의 명 위젯  */
  Widget deviceName(ScanResult r) {
    String name = '';

    if (r.device.advName.isNotEmpty) {
      // device.name에 값이 있다면
      name = r.device.advName;
    } else if (r.advertisementData.advName.isNotEmpty) {
      // advertisementData.localName에 값이 있다면
      name = r.advertisementData.advName;
    } else {
      // 둘다 없다면 이름 알 수 없음...
      name = 'N/A';
    }
    return Text(name);
  }

  /* BLE 아이콘 위젯 */
  Widget leading(ScanResult r) {
    return const CircleAvatar(
      backgroundColor: Colors.cyan,
      child: Icon(
        Icons.bluetooth,
        color: Colors.white,
      ),
    );
  }

  /* 장치 아이템을 탭 했을때 호출 되는 함수 */
  void onTap(ScanResult r) {
    // 단순히 이름만 출력
    print(r.device.advName);
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => DeviceScreen(device: r.device)),
    );
  }

  /* 장치 아이템 위젯 */
  Widget listItem(ScanResult r) {
    return ListTile(
      onTap: () => onTap(r),
      leading: leading(r),
      title: deviceName(r),
      subtitle: deviceMacAddress(r),
      trailing: deviceSignal(r),
    );
  }

  /* UI */
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        /* 장치 리스트 출력 */
        child: ListView.separated(
          itemCount: scanResultList.length,
          itemBuilder: (context, index) {
            return listItem(scanResultList[index]);
          },
          separatorBuilder: (BuildContext context, int index) {
            return const Divider();
          },
        ),
      ),
      /* 장치 검색 or 검색 중지  */
      floatingActionButton: FloatingActionButton(
        onPressed: _isScanning ? stopScan : scan,
        // 스캔 중이라면 stop 아이콘을, 정지상태라면 search 아이콘으로 표시
        child: Icon(_isScanning ? Icons.stop : Icons.search),
      ),
    );
  }
}

 

device_screen.dart

 

 

 

 

결과

 

 

위의 코드들을 통해 모든 BLE 연결 가능한 디바이스를 검색한 후 잘 작동하는지 확인했다.

 

다음으로는 main.dart 를 수정해서, 아두이노 IDE를 통해 설정한 디바이스명 `ESP32 Forse Sensor`만 찾도록 해보겠다.

 

`main.dart`

import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:taba/device_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  final title = 'Flutter BLE Scan Demo';

  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: title,
      home: MyHomePage(title: title),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // // 장치명을 지정해 해당 장치만 표시되게함
  final String targetDeviceName = 'ESP32 Force Sensor';

  // FlutterBluePlus flutterBlue = FlutterBluePlus();
  // List<BluetoothDevice> connectedDevices = [];
  List<ScanResult> scanResultList = [];
  bool _isScanning = false;

  @override
  initState() {
    super.initState();
    // 블루투스 초기화
    // 블루투스 초기화
    initBle();
  }

  // BLE 스캔 상태 얻기 위한 리스너
  void initBle() {
    // BLE 스캔 상태 얻기 위한 리스너
    FlutterBluePlus.isScanning.listen((isScanning) {
      _isScanning = isScanning;
      setState(() {});
    });
  }

  /*
  스캔 시작/정지 함수
  */
  scan() async {
    if (!_isScanning) {
      // 스캔 중이 아니라면
      // 기존에 스캔된 리스트 삭제
      scanResultList.clear();
      // 스캔 시작, 제한 시간 60초
      FlutterBluePlus.startScan(timeout: const Duration(seconds: 60));

      // 스캔 결과 리스너
      FlutterBluePlus.scanResults.listen((results) {
        setState(() {
          scanResultList.clear();

          for (var result in results) {
            if (result.device.advName == targetDeviceName) {
              if (!scanResultList.any((element) =>
                  element.device.remoteId == result.device.remoteId)) {
                scanResultList.add(result);
              }
            }
          }
        });
      });
    } else {
      // 스캔 중이라면 스캔 정지
      await FlutterBluePlus.stopScan();
    }
  }

  /*
   여기서부터는 장치별 출력용 함수들
  */

  /*  장치의 신호값 위젯  */
  Widget deviceSignal(ScanResult r) {
    return Text(r.rssi.toString());
  }

  /* 장치의 MAC 주소 위젯  */
  Widget deviceMacAddress(ScanResult r) {
    return Text(r.device.remoteId.toString());
  }

  /* 장치의 명 위젯  */
  Widget deviceName(ScanResult r) {
    String name = '';

    if (r.device.advName.isNotEmpty) {
      // device.name에 값이 있다면
      name = r.device.advName;
    } else if (r.advertisementData.advName.isNotEmpty) {
      // advertisementData.localName에 값이 있다면
      name = r.advertisementData.advName;
    } else {
      // 둘다 없다면 이름 알 수 없음...
      name = 'N/A';
    }
    return Text(name);
  }

  /* BLE 아이콘 위젯 */
  Widget leading(ScanResult r) {
    return const CircleAvatar(
      backgroundColor: Colors.cyan,
      child: Icon(
        Icons.bluetooth,
        color: Colors.white,
      ),
    );
  }

  /* 장치 아이템을 탭 했을때 호출 되는 함수 */
  void onTap(ScanResult r) {
    // 단순히 이름만 출력
    print(r.device.advName);
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => DeviceScreen(device: r.device)),
    );
  }

  /* 장치 아이템 위젯 */
  Widget listItem(ScanResult r) {
    return ListTile(
      onTap: () => onTap(r),
      leading: leading(r),
      title: deviceName(r),
      subtitle: deviceMacAddress(r),
      trailing: deviceSignal(r),
    );
  }

  /* UI */
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        /* 장치 리스트 출력 */
        child: ListView.separated(
          itemCount: scanResultList.length,
          itemBuilder: (context, index) {
            return listItem(scanResultList[index]);
          },
          separatorBuilder: (BuildContext context, int index) {
            return const Divider();
          },
        ),
      ),
      /* 장치 검색 or 검색 중지  */
      floatingActionButton: FloatingActionButton(
        onPressed: scan,
        // 스캔 중이라면 stop 아이콘을, 정지상태라면 search 아이콘으로 표시
        child: Icon(_isScanning ? Icons.stop : Icons.search),
      ),
    );
  }
}

 

결과

 

이렇게 해서 블루투스와 esp32 간 블루 투스 통신을 2024년 3월 버전으로 성공적으로 수행했다. 다음시간에는 프로젝트 메인 화면 구성을 하겠다.

728x90