universal_bluetooth 0.0.1 copy "universal_bluetooth: ^0.0.1" to clipboard
universal_bluetooth: ^0.0.1 copied to clipboard

A universal Bluetooth plugin for Flutter supporting Classic Bluetooth, BLE, and iBeacon.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:universal_bluetooth/models/bluetooth_models.dart';
import 'package:universal_bluetooth/universal_bluetooth.dart';
import 'package:permission_handler/permission_handler.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Universal Bluetooth Demo',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const BluetoothDemo(),
    );
  }
}

class BluetoothDemo extends StatefulWidget {
  const BluetoothDemo({super.key});

  @override
  State<BluetoothDemo> createState() => _BluetoothDemoState();
}

class _BluetoothDemoState extends State<BluetoothDemo>
    with TickerProviderStateMixin {
  late TabController _tabController;

  // Classic Bluetooth
  bool _isBluetoothEnabled = false;
  bool _isScanning = false;
  bool _isDiscoverable = false;
  String _bluetoothAddress = 'Unknown';
  List<BluetoothScanResult> _bluetoothDevices = [];

  // 연결 상태 관리를 위한 추가 변수들
  Map<String, BluetoothConnectionState> _connectionStates = {};
  String? _connectedDeviceId;

  // BLE
  bool _isBleScanning = false;
  List<BleDevice> _bleDevices = [];

  // iBeacon
  bool _isBeaconScanning = false;
  bool _isBeaconAdvertising = false;
  List<BeaconDevice> _beacons = [];

  bool _hasBluetoothPermissions = false;
  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    _initializeBluetooth();
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  Future<void> _initializeBluetooth() async {
    // 🔧 앱 시작 시 바로 권한 요청
    print('앱 시작 - 블루투스 권한 확인 중...');
    final hasPermissions = await _requestBluetoothPermissions();

    setState(() {
      _hasBluetoothPermissions = hasPermissions;
    });

    if (!hasPermissions) {
      print('블루투스 권한이 거부되었습니다.');
      _showSnackBar('⚠️ 블루투스 권한이 필요합니다. 설정에서 권한을 허용해주세요.');
      // 권한이 없어도 기본 초기화는 진행
    }

    // Check Bluetooth status
    final isEnabled = await UniversalBluetooth.isBluetoothEnabled;
    String bluetoothAddress = 'Unknown';

    try {
      bluetoothAddress = await UniversalBluetooth.getBluetoothAddress();
      print('My Bluetooth Address: $bluetoothAddress');
    } catch (e) {
      print('블루투스 주소 가져오기 실패: $e');
      if (hasPermissions) {
        bluetoothAddress = 'Permission Required';
      } else {
        bluetoothAddress = 'Permission Denied';
      }
    }

    setState(() {
      _isBluetoothEnabled = isEnabled;
      _bluetoothAddress = bluetoothAddress;
    });

    // 권한이 있고 블루투스가 활성화되어 있다면 성공 메시지
    if (hasPermissions && isEnabled) {
      _showSnackBar('✅ 블루투스가 준비되었습니다!');
    } else if (hasPermissions && !isEnabled) {
      _showSnackBar('📱 블루투스를 활성화해주세요.');
    }

    // 🔧 개선된 글로벌 메서드 호출 핸들러 설정
    UniversalBluetoothExtensions.setMethodCallHandler((call) async {
      print('Flutter: 🔔 Global received method call: ${call.method}');
      print('Flutter: 🔔 Arguments: ${call.arguments}');

      try {
        switch (call.method) {
          case 'onBluetoothDataReceived':
            final arguments = call.arguments as Map<Object?, Object?>;
            final String deviceId = arguments['deviceId'] as String;
            final List<int> data = List<int>.from(arguments['data'] as List);

            print('Flutter: 📩 Processing received data from $deviceId: $data');
            _handleReceivedData(deviceId, data);
            break;

          case 'onBluetoothConnectionStateChanged':
            final arguments = call.arguments as Map<Object?, Object?>;
            final String deviceId = arguments['deviceId'] as String;
            final int state = arguments['state'] as int;

            print(
              'Flutter: 🔗 Processing connection state change for $deviceId: $state',
            );
            _handleConnectionStateChanged(deviceId, state);
            break;

          default:
            print('Flutter: ❓ Unknown method call: ${call.method}');
        }
      } catch (e, stackTrace) {
        print('Flutter: ❌ Error processing method call ${call.method}: $e');
        print('Flutter: ❌ Stack trace: $stackTrace');
      }
    });

    // 🔧 수정: 스캔 결과 리스너를 더 안정적으로 설정
    _setupBluetoothScanListeners();

    // Listen to BLE scan results
    UniversalBluetooth.bleScanResults.listen((device) {
      setState(() {
        final index = _bleDevices.indexWhere((d) => d.id == device.id);
        if (index >= 0) {
          _bleDevices[index] = device;
        } else {
          _bleDevices.add(device);
        }
      });
    });

    // Listen to beacon scan results
    UniversalBluetooth.beaconScanResults.listen((beacon) {
      setState(() {
        final index = _beacons.indexWhere(
          (b) =>
              b.uuid == beacon.uuid &&
              b.major == beacon.major &&
              b.minor == beacon.minor,
        );
        if (index >= 0) {
          _beacons[index] = beacon;
        } else {
          _beacons.add(beacon);
        }
      });
    });
  }

  void _setupBluetoothScanListeners() {
    // Listen to scan results with improved error handling
    UniversalBluetooth.bluetoothScanResults.listen(
      (result) {
        print(
          'Flutter: Received scan result - ${result.device.name} (${result.device.address})',
        );
        setState(() {
          final index = _bluetoothDevices.indexWhere(
            (device) => device.device.address == result.device.address,
          );
          if (index >= 0) {
            // 기존 기기 업데이트 (RSSI 등)
            _bluetoothDevices[index] = result;
            print('Flutter: Updated device at index $index');
          } else {
            // 새 기기 추가
            _bluetoothDevices.add(result);
            print(
              'Flutter: Added new device, total: ${_bluetoothDevices.length}',
            );
          }
          // 신호 강도 순으로 정렬 (강한 신호부터)
          _bluetoothDevices.sort(
            (a, b) => (b.device.rssi ?? -999).compareTo(a.device.rssi ?? -999),
          );
        });
      },
      onError: (error) {
        print('Flutter: Error in scan results stream: $error');
        _showSnackBar('스캔 오류: $error');
      },
    );

    // Listen to scan finished events
    UniversalBluetooth.bluetoothScanFinished.listen(
      (_) {
        print('Flutter: Scan finished event received');
        setState(() {
          _isScanning = false;
        });
        _showSnackBar('스캔 완료! ${_bluetoothDevices.length}개 기기 발견');
      },
      onError: (error) {
        print('Flutter: Error in scan finished stream: $error');
      },
    );
  }

  // 🔧 개선된 데이터 수신 처리 메서드
  void _handleReceivedData(String deviceId, List<int> data) {
    try {
      final message = String.fromCharCodes(data);
      print('Flutter: 📩 Received data from $deviceId:');
      print('Flutter: 📩   Raw bytes: $data');
      print('Flutter: 📩   Message: "$message"');
      print('Flutter: 📩   Length: ${data.length} bytes');

      // 수신된 메시지에 따라 다른 응답
      if (message.trim().toLowerCase() == 'hello') {
        print('Flutter: 🔄 Received "hello", will show notification');
        _showSnackBar('📩 받은 메시지: "$message" - 상대방이 인사했습니다!');
      } else if (message.trim().toLowerCase() == 'hi') {
        print('Flutter: 🔄 Received "hi", showing response notification');
        _showSnackBar('📩 받은 응답: "$message" - 상대방이 응답했습니다!');
      } else {
        print('Flutter: 🔄 Received other message');
        _showSnackBar('📩 받은 메시지: "$message"');
      }

      // 🔧 추가: UI 상태 업데이트
      setState(() {
        // 필요시 UI 상태 업데이트
      });
    } catch (e, stackTrace) {
      print('Flutter: ❌ Error handling received data: $e');
      print('Flutter: ❌ Stack trace: $stackTrace');
      _showSnackBar('❌ 데이터 처리 오류: $e');
    }
  }

  // 연결 상태 변경 처리 메서드
  void _handleConnectionStateChanged(String deviceId, int state) {
    print('🔔 Flutter: Connection state changed for $deviceId: $state');

    setState(() {
      switch (state) {
        case 0: // disconnected
          _connectionStates[deviceId] = BluetoothConnectionState.disconnected;
          if (_connectedDeviceId == deviceId) {
            _connectedDeviceId = null;
          }
          break;
        case 1: // connecting
          _connectionStates[deviceId] = BluetoothConnectionState.connecting;
          break;
        case 2: // connected
          _connectionStates[deviceId] = BluetoothConnectionState.connected;
          _connectedDeviceId = deviceId;
          break;
        case 4: // error
          _connectionStates[deviceId] = BluetoothConnectionState.error;
          break;
      }
    });

    // 사용자에게 알림
    final deviceResult = _bluetoothDevices.firstWhere(
      (result) => result.device.id == deviceId,
      orElse: () => BluetoothScanResult(
        device: BluetoothDevice(
          isConnected: true,
          id: deviceId,
          name: 'Unknown Device',
          address: deviceId,
          rssi: 0,
        ),
        timestamp: DateTime.now(),
        isFirstScan: true,
      ),
    );

    final deviceName = deviceResult.device.name.isNotEmpty
        ? deviceResult.device.name
        : 'Unknown Device';

    switch (state) {
      case 2: // connected
        _showSnackBar('🔗 $deviceName 연결됨');
        break;
      case 0: // disconnected
        _showSnackBar('🔌 $deviceName 연결 해제됨');
        break;
      case 4: // error
        _showSnackBar('❌ $deviceName 연결 오류');
        break;
    }
  }

  // 권한 상태 표시 위젯
  Widget _buildPermissionStatus() {
    return Container(
      margin: const EdgeInsets.all(16.0),
      padding: const EdgeInsets.all(12.0),
      decoration: BoxDecoration(
        color: _hasBluetoothPermissions
            ? Colors.green.withOpacity(0.1)
            : Colors.orange.withOpacity(0.1),
        borderRadius: BorderRadius.circular(8),
        border: Border.all(
          color: _hasBluetoothPermissions ? Colors.green : Colors.orange,
          width: 1,
        ),
      ),
      child: Row(
        children: [
          Icon(
            _hasBluetoothPermissions ? Icons.check_circle : Icons.warning,
            color: _hasBluetoothPermissions ? Colors.green : Colors.orange,
            size: 20,
          ),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              _hasBluetoothPermissions ? '블루투스 권한이 허용되었습니다' : '블루투스 권한이 필요합니다',
              style: TextStyle(
                color: _hasBluetoothPermissions ? Colors.green : Colors.orange,
                fontWeight: FontWeight.w500,
                fontSize: 13,
              ),
            ),
          ),
          if (!_hasBluetoothPermissions)
            TextButton(
              onPressed: () async {
                final granted = await _requestBluetoothPermissions();
                setState(() {
                  _hasBluetoothPermissions = granted;
                });
                if (granted) {
                  _showSnackBar('✅ 권한이 승인되었습니다!');
                  // 권한 승인 후 블루투스 주소 다시 가져오기
                  try {
                    final address =
                        await UniversalBluetooth.getBluetoothAddress();
                    setState(() {
                      _bluetoothAddress = address;
                    });
                  } catch (e) {
                    print('블루투스 주소 가져오기 실패: $e');
                  }
                }
              },
              style: TextButton.styleFrom(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              ),
              child: const Text('허용', style: TextStyle(fontSize: 12)),
            ),
        ],
      ),
    );
  }

  Future<bool> _requestBluetoothPermissions() async {
    print('블루투스 권한 요청 시작...');

    try {
      Map<Permission, PermissionStatus> permissions = await [
        Permission.bluetooth,
        Permission.bluetoothScan,
        Permission.bluetoothConnect,
        Permission.bluetoothAdvertise,
        Permission.location,
        Permission.locationWhenInUse,
      ].request();

      print('권한 요청 결과:');
      permissions.forEach((permission, status) {
        print('  ${permission.toString()}: ${status.toString()}');
      });

      // 필수 권한 체크 (좀 더 유연하게)
      bool hasBluetoothBasic =
          permissions[Permission.bluetooth] == PermissionStatus.granted ||
          permissions[Permission.bluetoothConnect] == PermissionStatus.granted;

      bool hasBluetoothScan =
          permissions[Permission.bluetoothScan] == PermissionStatus.granted ||
          permissions[Permission.bluetooth] == PermissionStatus.granted;

      if (!hasBluetoothBasic || !hasBluetoothScan) {
        print('⚠️ 필수 블루투스 권한이 거부되었습니다.');

        // 사용자에게 더 친화적인 메시지와 설정 이동 옵션 제공
        _showPermissionDialog();
        return false;
      }

      print('✅ 블루투스 권한이 승인되었습니다.');
      return true;
    } catch (e) {
      print('권한 요청 중 오류: $e');
      _showSnackBar('권한 요청 중 오류가 발생했습니다: $e');
      return false;
    }
  }

  void _showPermissionDialog() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Row(
            children: [
              Icon(Icons.bluetooth_disabled, color: Colors.orange),
              SizedBox(width: 8),
              Text('블루투스 권한 필요'),
            ],
          ),
          content: const Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('이 앱을 사용하려면 다음 권한이 필요합니다:'),
              SizedBox(height: 8),
              Text('• 블루투스 연결'),
              Text('• 블루투스 스캔'),
              Text('• 위치 정보 (BLE 스캔용)'),
              SizedBox(height: 12),
              Text(
                '설정에서 권한을 허용해주세요.',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('나중에'),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop();
                openAppSettings(); // 설정 앱으로 이동
              },
              child: const Text('설정 열기'),
            ),
          ],
        );
      },
    );
  }

  // Classic Bluetooth Methods
  Future<void> _toggleBluetoothScan() async {
    // 권한 확인 및 요청
    final hasPermissions = await _requestBluetoothPermissions();
    if (!hasPermissions) {
      return;
    }

    if (!_isBluetoothEnabled) {
      final enabled = await UniversalBluetooth.requestBluetoothEnable();
      if (!enabled) {
        _showSnackBar('Bluetooth is not enabled');
        return;
      }
      setState(() {
        _isBluetoothEnabled = true;
      });
    }

    try {
      if (_isScanning) {
        await UniversalBluetooth.stopBluetoothScan();
      } else {
        await UniversalBluetooth.startBluetoothScan();
        setState(() {
          _bluetoothDevices.clear();
        });
      }

      setState(() {
        _isScanning = !_isScanning;
      });
    } catch (e) {
      _showSnackBar('Bluetooth scan error: $e');
    }
  }

  Future<void> _toggleBluetoothDiscoverable() async {
    // 권한 확인 및 요청
    final hasPermissions = await _requestBluetoothPermissions();
    if (!hasPermissions) {
      return;
    }

    if (!_isBluetoothEnabled) {
      final enabled = await UniversalBluetooth.requestBluetoothEnable();
      if (!enabled) {
        _showSnackBar('Bluetooth is not enabled');
        return;
      }
      setState(() {
        _isBluetoothEnabled = true;
      });
    }

    try {
      if (_isDiscoverable) {
        await UniversalBluetooth.stopBluetoothDiscoverable();
        _showSnackBar('Stopped advertising');
      } else {
        final success = await UniversalBluetooth.startBluetoothDiscoverable(
          duration: 300,
        );
        if (success) {
          _showSnackBar('Device is now discoverable for 5 minutes');
        } else {
          _showSnackBar('Discoverable request was cancelled');
          return;
        }
      }

      setState(() {
        _isDiscoverable = !_isDiscoverable;
      });
    } catch (e) {
      _showSnackBar('Bluetooth discoverable error: $e');
    }
  }

  // 연결 메서드 수정
  Future<void> _connectBluetoothDevice(String deviceId) async {
    if (_isScanning) {
      await UniversalBluetooth.stopBluetoothScan();
      setState(() {
        _isScanning = false;
      });
    }

    final deviceResult = _bluetoothDevices.firstWhere(
      (result) => result.device.id == deviceId,
      orElse: () => _bluetoothDevices.first,
    );

    final deviceName = deviceResult.device.name.isNotEmpty
        ? deviceResult.device.name
        : 'Unknown Device';

    try {
      _showSnackBar('$deviceName 연결 중...');
      await UniversalBluetooth.connectToBluetoothDevice(deviceId);

      // 🔧 글로벌 리스너가 연결 상태를 처리하므로 여기서는 제거
      print('Flutter: Connection request sent for $deviceId');
    } catch (e) {
      print('Flutter: Connection failed for $deviceId: $e');
      _showSnackBar('❌ $deviceName 연결 실패: $e');
    }
  }

  // 디버깅을 위한 테스트 메서드들 추가
  Future<void> _testConnection() async {
    if (_connectedDeviceId == null) {
      _showSnackBar('❌ 연결된 기기가 없습니다.');
      return;
    }

    try {
      // 연결된 기기 목록 확인
      final connectedDevices =
          await UniversalBluetoothExtensions.getConnectedDevices();
      print('📱 Connected devices: $connectedDevices');
      _showSnackBar('📱 연결된 기기: ${connectedDevices.length}개');

      // 테스트 데이터 전송
      final result = await UniversalBluetoothExtensions.testDataSend(
        _connectedDeviceId!,
      );
      _showSnackBar('🧪 테스트 결과: $result');
    } catch (e) {
      _showSnackBar('❌ 테스트 실패: $e');
    }
  }

  // 데이터 수신 리스너 설정
  void _setupDataListener(String deviceId) {
    UniversalBluetooth.bluetoothDataReceived(deviceId).listen(
      (data) {
        final message = String.fromCharCodes(data);
        print('Flutter: Received data from $deviceId: $message');

        // 수신된 메시지에 따라 다른 응답
        if (message.trim().toLowerCase() == 'hello') {
          _showSnackBar('📩 받은 메시지: "$message" - "hi" 응답 전송됨');
        } else if (message.trim().toLowerCase() == 'hi') {
          _showSnackBar('📩 받은 응답: "$message"');
        } else {
          _showSnackBar('📩 받은 메시지: "$message"');
        }
      },
      onError: (error) {
        print('Flutter: Error in data stream: $error');
        _showSnackBar('데이터 수신 오류: $error');
      },
    );
  }

  // 🔧 개선된 Hello 메시지 전송 메서드
  Future<void> _sendHelloMessage() async {
    if (_connectedDeviceId == null) {
      _showSnackBar('❌ 연결된 기기가 없습니다.');
      return;
    }

    try {
      print('Flutter: 📤 Sending hello message to $_connectedDeviceId');

      final message = 'hello';
      final data = message.codeUnits;

      print('Flutter: 📤 Message: "$message"');
      print('Flutter: 📤 Data: $data');

      await UniversalBluetooth.sendBluetoothData(_connectedDeviceId!, data);

      print('Flutter: ✅ Hello message sent successfully');
      _showSnackBar('📤 "hello" 메시지 전송됨 - 응답을 기다려보세요!');
    } catch (e, stackTrace) {
      print('Flutter: ❌ Failed to send hello message: $e');
      print('Flutter: ❌ Stack trace: $stackTrace');
      _showSnackBar('❌ 메시지 전송 실패: $e');
    }
  }

  // 🔧 테스트용 다양한 메시지 전송 메서드
  Future<void> _sendTestMessage(String message) async {
    if (_connectedDeviceId == null) {
      _showSnackBar('❌ 연결된 기기가 없습니다.');
      return;
    }

    try {
      print('Flutter: 📤 Sending test message: "$message"');

      final data = message.codeUnits;
      await UniversalBluetooth.sendBluetoothData(_connectedDeviceId!, data);

      print('Flutter: ✅ Test message sent successfully');
      _showSnackBar('📤 메시지 전송됨: "$message"');
    } catch (e) {
      print('Flutter: ❌ Failed to send test message: $e');
      _showSnackBar('❌ 메시지 전송 실패: $e');
    }
  }

  // 연결 해제 메서드
  Future<void> _disconnectDevice(String deviceId) async {
    try {
      await UniversalBluetooth.disconnectBluetoothDevice(deviceId);
      setState(() {
        _connectionStates[deviceId] = BluetoothConnectionState.disconnected;
        if (_connectedDeviceId == deviceId) {
          _connectedDeviceId = null;
        }
      });
      _showSnackBar('🔌 연결 해제됨');
    } catch (e) {
      _showSnackBar('❌ 연결 해제 실패: $e');
    }
  }

  // BLE Methods
  Future<void> _toggleBleScan() async {
    // 권한 확인 및 요청
    final hasPermissions = await _requestBluetoothPermissions();
    if (!hasPermissions) {
      return;
    }

    if (!_isBluetoothEnabled) {
      final enabled = await UniversalBluetooth.requestBluetoothEnable();
      if (!enabled) {
        _showSnackBar('Bluetooth is not enabled');
        return;
      }
      setState(() {
        _isBluetoothEnabled = true;
      });
    }

    try {
      if (_isBleScanning) {
        await UniversalBluetooth.stopBleScan();
      } else {
        await UniversalBluetooth.startBleScan(
          timeout: const Duration(seconds: 30),
        );
        setState(() {
          _bleDevices.clear();
        });
      }

      setState(() {
        _isBleScanning = !_isBleScanning;
      });
    } catch (e) {
      _showSnackBar('BLE scan error: $e');
    }
  }

  Future<void> _connectBleDevice(String deviceId) async {
    try {
      await UniversalBluetooth.connectToBleDevice(deviceId);
      _showSnackBar('Connecting to BLE device...');

      // Listen for connection state changes
      UniversalBluetooth.bleConnectionStateChanged(deviceId).listen((state) {
        _showSnackBar('BLE Connection state: ${state.name}');
      });
    } catch (e) {
      _showSnackBar('Failed to connect to BLE device: $e');
    }
  }

  // iBeacon Methods
  Future<void> _toggleBeaconScanning() async {
    try {
      if (_isBeaconScanning) {
        await UniversalBluetooth.stopBeaconScanning();
      } else {
        final hasPermission =
            await UniversalBluetooth.requestLocationPermission();
        if (!hasPermission) {
          _showSnackBar('Location permission is required for beacon scanning');
          return;
        }

        await UniversalBluetooth.startBeaconScanning();
        setState(() {
          _beacons.clear();
        });
      }

      setState(() {
        _isBeaconScanning = !_isBeaconScanning;
      });
    } catch (e) {
      _showSnackBar('Beacon scanning error: $e');
    }
  }

  Future<void> _toggleBeaconAdvertising() async {
    try {
      if (_isBeaconAdvertising) {
        await UniversalBluetooth.stopBeaconAdvertising();
      } else {
        await UniversalBluetooth.startBeaconAdvertising(
          uuid: 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0',
          major: 1,
          minor: 100,
          identifier: 'UniversalBluetoothDemo',
        );
        _showSnackBar('Started advertising as iBeacon');
      }

      setState(() {
        _isBeaconAdvertising = !_isBeaconAdvertising;
      });
    } catch (e) {
      _showSnackBar('Failed to start beacon advertising: $e');
    }
  }

  void _showSnackBar(String message) {
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(message)));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Universal Bluetooth Demo'),
        actions: [
          // 🔧 디버깅 버튼 추가
          if (_connectedDeviceId != null)
            IconButton(
              icon: const Icon(Icons.bug_report),
              onPressed: _testConnection,
              tooltip: 'Test Connection',
            ),
        ],
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: 'Classic BT', icon: Icon(Icons.bluetooth)),
            Tab(text: 'BLE', icon: Icon(Icons.bluetooth_connected)),
            Tab(text: 'iBeacon', icon: Icon(Icons.location_on)),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _buildClassicBluetoothTab(),
          _buildBleTab(),
          _buildBeaconTab(),
        ],
      ),
      // Hello 메시지 전송을 위한 플로팅 액션 버튼 추가
      floatingActionButton: _connectedDeviceId != null
          ? Column(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                FloatingActionButton(
                  onPressed: () => _sendTestMessage('test'),
                  child: const Icon(Icons.science),
                  backgroundColor: Colors.purple,
                  heroTag: "test",
                  tooltip: 'Send Test',
                ),
                const SizedBox(height: 8),
                FloatingActionButton(
                  onPressed: () => _sendTestMessage('hi'),
                  child: const Icon(Icons.waving_hand),
                  backgroundColor: Colors.orange,
                  heroTag: "hi",
                  tooltip: 'Send Hi',
                ),
                const SizedBox(height: 8),
                FloatingActionButton.extended(
                  onPressed: _sendHelloMessage,
                  icon: const Icon(Icons.send),
                  label: const Text('Hello'),
                  backgroundColor: Colors.green,
                  heroTag: "hello",
                ),
              ],
            )
          : null,
    );
  }

  Widget _buildClassicBluetoothTab() {
    return Column(
      children: [
        _buildPermissionStatus(),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: Column(
            children: [
              Row(
                children: [
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'Bluetooth Status: ${_isBluetoothEnabled ? "Enabled" : "Disabled"}',
                          style: Theme.of(context).textTheme.titleMedium,
                        ),
                        Text(
                          'My Address: $_bluetoothAddress',
                          style: Theme.of(context).textTheme.bodySmall
                              ?.copyWith(
                                color: _bluetoothAddress.contains('Permission')
                                    ? Colors.orange
                                    : null,
                              ),
                        ),
                        if (_connectedDeviceId != null)
                          Text(
                            'Connected: $_connectedDeviceId',
                            style: Theme.of(context).textTheme.bodySmall
                                ?.copyWith(
                                  color: Colors.green,
                                  fontWeight: FontWeight.bold,
                                ),
                          ),
                      ],
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton.icon(
                      onPressed: _toggleBluetoothScan,
                      icon: Icon(_isScanning ? Icons.stop : Icons.search),
                      label: Text(_isScanning ? '스캔 중지' : '기기 검색'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: _isScanning ? Colors.red : Colors.blue,
                        foregroundColor: Colors.white,
                        padding: const EdgeInsets.symmetric(vertical: 12),
                      ),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: ElevatedButton.icon(
                      onPressed: _toggleBluetoothDiscoverable,
                      icon: Icon(
                        _isDiscoverable
                            ? Icons.visibility_off
                            : Icons.visibility,
                      ),
                      label: Text(_isDiscoverable ? '광고 중지' : '광고 시작'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: _isDiscoverable
                            ? Colors.green
                            : Colors.orange,
                        foregroundColor: Colors.white,
                        padding: const EdgeInsets.symmetric(vertical: 12),
                      ),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Text(
                '발견된 기기: ${_bluetoothDevices.length}개',
                style: Theme.of(context).textTheme.titleSmall?.copyWith(
                  color: _bluetoothDevices.isEmpty ? Colors.grey : Colors.blue,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
        ),
        if (_isScanning)
          const Padding(
            padding: EdgeInsets.all(16.0),
            child: LinearProgressIndicator(),
          ),
        Expanded(
          child: _bluetoothDevices.isEmpty
              ? Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(
                        Icons.bluetooth_searching,
                        size: 64,
                        color: Colors.grey,
                      ),
                      const SizedBox(height: 16),
                      Text(
                        _isScanning
                            ? '블루투스 기기를 검색 중...'
                            : '스캔 버튼을 눌러 기기를 검색하세요',
                        style: Theme.of(
                          context,
                        ).textTheme.bodyLarge?.copyWith(color: Colors.grey),
                      ),
                    ],
                  ),
                )
              : ListView.separated(
                  itemCount: _bluetoothDevices.length,
                  separatorBuilder: (context, index) =>
                      const Divider(height: 1),
                  itemBuilder: (context, index) {
                    final result = _bluetoothDevices[index];
                    final device = result.device;
                    final rssi = device.rssi ?? -999;
                    final isConnected =
                        _connectionStates[device.id] ==
                        BluetoothConnectionState.connected;

                    // 신호 강도에 따른 색상 결정
                    Color signalColor;
                    IconData signalIcon;
                    String signalText;
                    if (rssi > -50) {
                      signalColor = Colors.green;
                      signalIcon = Icons.bluetooth_connected;
                      signalText = '강함';
                    } else if (rssi > -70) {
                      signalColor = Colors.orange;
                      signalIcon = Icons.bluetooth;
                      signalText = '보통';
                    } else if (rssi > -90) {
                      signalColor = Colors.red;
                      signalIcon = Icons.bluetooth_disabled;
                      signalText = '약함';
                    } else {
                      signalColor = Colors.grey;
                      signalIcon = Icons.bluetooth_disabled;
                      signalText = '매우약함';
                    }

                    return ListTile(
                      contentPadding: const EdgeInsets.symmetric(
                        horizontal: 16,
                        vertical: 8,
                      ),
                      leading: CircleAvatar(
                        backgroundColor: isConnected
                            ? Colors.green.withOpacity(0.1)
                            : Colors.blue.withOpacity(0.1),
                        child: Icon(
                          isConnected
                              ? Icons.bluetooth_connected
                              : Icons.bluetooth,
                          color: isConnected ? Colors.green : Colors.blue,
                        ),
                      ),
                      title: Text(
                        device.name.isNotEmpty ? device.name : 'Unknown Device',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                          color: isConnected ? Colors.green : null,
                        ),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                      subtitle: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const SizedBox(height: 4),
                          Text(
                            device.address,
                            style: TextStyle(
                              fontFamily: 'monospace',
                              color: Colors.grey[600],
                              fontSize: 13,
                            ),
                          ),
                          const SizedBox(height: 2),
                          Row(
                            children: [
                              Icon(signalIcon, size: 16, color: signalColor),
                              const SizedBox(width: 4),
                              Text(
                                'RSSI: ${rssi}dBm ($signalText)',
                                style: TextStyle(
                                  color: signalColor,
                                  fontSize: 12,
                                  fontWeight: FontWeight.w500,
                                ),
                              ),
                              if (isConnected) ...[
                                const SizedBox(width: 8),
                                Container(
                                  padding: const EdgeInsets.symmetric(
                                    horizontal: 6,
                                    vertical: 2,
                                  ),
                                  decoration: BoxDecoration(
                                    color: Colors.green,
                                    borderRadius: BorderRadius.circular(8),
                                  ),
                                  child: const Text(
                                    '연결됨',
                                    style: TextStyle(
                                      color: Colors.white,
                                      fontSize: 10,
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                ),
                              ],
                            ],
                          ),
                        ],
                      ),
                      trailing: isConnected
                          ? ElevatedButton(
                              onPressed: () => _disconnectDevice(device.id),
                              style: ElevatedButton.styleFrom(
                                backgroundColor: Colors.red,
                                foregroundColor: Colors.white,
                                padding: const EdgeInsets.symmetric(
                                  horizontal: 16,
                                  vertical: 8,
                                ),
                                minimumSize: const Size(70, 36),
                              ),
                              child: const Text(
                                '해제',
                                style: TextStyle(fontSize: 12),
                              ),
                            )
                          : ElevatedButton(
                              onPressed: () =>
                                  _connectBluetoothDevice(device.id),
                              style: ElevatedButton.styleFrom(
                                backgroundColor: Colors.blue,
                                foregroundColor: Colors.white,
                                padding: const EdgeInsets.symmetric(
                                  horizontal: 16,
                                  vertical: 8,
                                ),
                                minimumSize: const Size(70, 36),
                              ),
                              child: const Text(
                                '연결',
                                style: TextStyle(fontSize: 12),
                              ),
                            ),
                      onTap: isConnected
                          ? null
                          : () => _connectBluetoothDevice(device.id),
                    );
                  },
                ),
        ),
      ],
    );
  }

  Widget _buildBleTab() {
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Row(
            children: [
              Expanded(
                child: Text(
                  'BLE Devices Found: ${_bleDevices.length}',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ),
              ElevatedButton(
                onPressed: _toggleBleScan,
                child: Text(_isBleScanning ? 'Stop Scan' : 'Start Scan'),
              ),
            ],
          ),
        ),
        if (_isBleScanning)
          const Padding(
            padding: EdgeInsets.all(16.0),
            child: LinearProgressIndicator(),
          ),
        Expanded(
          child: ListView.builder(
            itemCount: _bleDevices.length,
            itemBuilder: (context, index) {
              final device = _bleDevices[index];
              return ListTile(
                title: Text(
                  device.name.isNotEmpty ? device.name : 'Unknown Device',
                ),
                subtitle: Text('${device.address}\nRSSI: ${device.rssi}'),
                trailing: ElevatedButton(
                  onPressed: () => _connectBleDevice(device.id),
                  child: const Text('Connect'),
                ),
                leading: const Icon(Icons.bluetooth_connected),
              );
            },
          ),
        ),
      ],
    );
  }

  Widget _buildBeaconTab() {
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton(
                      onPressed: _toggleBeaconScanning,
                      child: Text(
                        _isBeaconScanning ? 'Stop Scanning' : 'Start Scanning',
                      ),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: ElevatedButton(
                      onPressed: _toggleBeaconAdvertising,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: _isBeaconAdvertising
                            ? Colors.red
                            : null,
                      ),
                      child: Text(
                        _isBeaconAdvertising
                            ? 'Stop Advertising'
                            : 'Start Advertising',
                      ),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Text(
                'Beacons Found: ${_beacons.length}',
                style: Theme.of(context).textTheme.titleMedium,
              ),
            ],
          ),
        ),
        if (_isBeaconScanning)
          const Padding(
            padding: EdgeInsets.all(16.0),
            child: LinearProgressIndicator(),
          ),
        Expanded(
          child: ListView.builder(
            itemCount: _beacons.length,
            itemBuilder: (context, index) {
              final beacon = _beacons[index];
              return ListTile(
                title: Text('UUID: ${beacon.uuid}'),
                subtitle: Text(
                  'Major: ${beacon.major}, Minor: ${beacon.minor}\n'
                  'RSSI: ${beacon.rssi}, Distance: ${beacon.distance?.toStringAsFixed(2)}m\n'
                  'Proximity: ${beacon.proximity.name}',
                ),
                leading: Icon(
                  Icons.location_on,
                  color: _getProximityColor(beacon.proximity),
                ),
              );
            },
          ),
        ),
      ],
    );
  }

  Color _getProximityColor(BeaconProximity proximity) {
    switch (proximity) {
      case BeaconProximity.immediate:
        return Colors.green;
      case BeaconProximity.near:
        return Colors.orange;
      case BeaconProximity.far:
        return Colors.red;
      case BeaconProximity.unknown:
        return Colors.grey;
    }
  }
}
1
likes
0
points
14
downloads

Publisher

unverified uploader

Weekly Downloads

A universal Bluetooth plugin for Flutter supporting Classic Bluetooth, BLE, and iBeacon.

Homepage

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on universal_bluetooth

Packages that implement universal_bluetooth