feat(bluetooth): 실제 BLE 기반 맛집 리스트 공유 구현
- ble_peripheral 패키지 추가 (Peripheral/GATT Server 역할) - flutter_blue_plus 패키지 활용 (Central/스캔/연결 역할) 변경 사항: - BleConstants: BLE UUID 및 설정 상수 정의 - BluetoothService: Mock에서 실제 BLE 통신으로 전면 재작성 - Receiver: GATT Server로 광고, Write callback으로 데이터 수신 - Sender: 서비스 UUID로 스캔, 연결 후 Characteristic에 데이터 전송 - 청크 기반 대용량 데이터 전송 프로토콜 구현 - ShareDevice: BLE 디바이스 정보 필드 추가 (remoteId, rssi, bleDevice) - Android/iOS: BLE 권한 및 기능 선언 추가 데이터 전송 프로토콜: - 'CHUNK:index:total:data' 형식으로 500바이트 단위 분할 전송 - 수신 측에서 청크 조립 후 JSON 파싱
This commit is contained in:
32
lib/core/constants/ble_constants.dart
Normal file
32
lib/core/constants/ble_constants.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
/// BLE 공유 기능에서 사용하는 상수 정의
|
||||
class BleConstants {
|
||||
BleConstants._();
|
||||
|
||||
/// LunchPick 전용 서비스 UUID
|
||||
static const String serviceUuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
||||
|
||||
/// 맛집 데이터 전송용 Characteristic UUID
|
||||
static const String dataCharacteristicUuid =
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567891';
|
||||
|
||||
/// 공유 코드 확인용 Characteristic UUID (매칭용)
|
||||
static const String codeCharacteristicUuid =
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567892';
|
||||
|
||||
/// 청크 사이즈 (MTU - 3, 보수적으로 설정)
|
||||
/// BLE 표준 MTU는 23바이트지만, 협상을 통해 더 커질 수 있음
|
||||
/// 안전하게 500바이트로 설정
|
||||
static const int chunkSize = 500;
|
||||
|
||||
/// 광고 이름 prefix
|
||||
static const String advertiseNamePrefix = 'LP-';
|
||||
|
||||
/// 스캔 타임아웃 (초)
|
||||
static const int scanTimeoutSeconds = 10;
|
||||
|
||||
/// 연결 타임아웃 (초)
|
||||
static const int connectionTimeoutSeconds = 15;
|
||||
|
||||
/// 청크 전송 간 딜레이 (밀리초)
|
||||
static const int chunkDelayMs = 50;
|
||||
}
|
||||
@@ -1,89 +1,452 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ble_peripheral/ble_peripheral.dart' as ble_peripheral;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:lunchpick/core/constants/ble_constants.dart';
|
||||
import 'package:lunchpick/domain/entities/restaurant.dart';
|
||||
import 'package:lunchpick/domain/entities/share_device.dart';
|
||||
|
||||
/// 실제 Bluetooth 통신을 대체하는 간단한 모의(Mock) 서비스.
|
||||
/// BLE 기반 맛집 리스트 공유 서비스
|
||||
///
|
||||
/// - Receiver (공유받기): ble_peripheral 패키지로 GATT Server 역할
|
||||
/// - Sender (공유하기): flutter_blue_plus 패키지로 Central 역할
|
||||
class BluetoothService {
|
||||
// === Peripheral (Receiver) 관련 ===
|
||||
bool _isPeripheralInitialized = false;
|
||||
String? _currentShareCode;
|
||||
final _incomingDataController = StreamController<String>.broadcast();
|
||||
final Map<String, ShareDevice> _listeningDevices = {};
|
||||
final Random _random = Random();
|
||||
final _receivedChunks = <int, String>{};
|
||||
// ignore: unused_field - 디버깅 및 진행률 표시용
|
||||
int _expectedTotalChunks = 0;
|
||||
|
||||
// === Central (Sender) 관련 ===
|
||||
final List<ShareDevice> _discoveredDevices = [];
|
||||
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
||||
BluetoothDevice? _connectedDevice;
|
||||
|
||||
// === 진행 상태 콜백 ===
|
||||
Function(int current, int total)? onSendProgress;
|
||||
Function(int current, int total)? onReceiveProgress;
|
||||
|
||||
/// 수신된 데이터 스트림
|
||||
Stream<String> get onDataReceived => _incomingDataController.stream;
|
||||
|
||||
/// 특정 코드로 수신 대기를 시작한다.
|
||||
Future<void> startListening(String code) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
stopListening();
|
||||
final shareDevice = ShareDevice(
|
||||
code: code,
|
||||
deviceId: 'LP-${_random.nextInt(900000) + 100000}',
|
||||
discoveredAt: DateTime.now(),
|
||||
);
|
||||
_listeningDevices[code] = shareDevice;
|
||||
}
|
||||
/// 현재 광고 중인 공유 코드
|
||||
String? get currentShareCode => _currentShareCode;
|
||||
|
||||
/// 더 이상 수신 대기하지 않는다.
|
||||
void stopListening() {
|
||||
if (_listeningDevices.isEmpty) return;
|
||||
final codes = List<String>.from(_listeningDevices.keys);
|
||||
for (final code in codes) {
|
||||
_listeningDevices.remove(code);
|
||||
/// BLE 지원 여부 확인
|
||||
Future<bool> get isSupported async {
|
||||
if (!Platform.isAndroid && !Platform.isIOS) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await FlutterBluePlus.isSupported;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 현재 주변에서 수신 대기 중인 기기 목록을 반환한다.
|
||||
Future<List<ShareDevice>> scanNearbyDevices() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
return _listeningDevices.values.toList();
|
||||
/// Bluetooth 어댑터 상태 확인
|
||||
Future<bool> get isBluetoothOn async {
|
||||
try {
|
||||
final state = await FlutterBluePlus.adapterState.first;
|
||||
return state == BluetoothAdapterState.on;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 대상 코드로 맛집 리스트를 전송한다. 실제 BT 대신 JSON 문자열을 브로드캐스트한다.
|
||||
// =========================================================================
|
||||
// Receiver (Peripheral) 메서드 - ble_peripheral 사용
|
||||
// =========================================================================
|
||||
|
||||
/// Peripheral 초기화
|
||||
Future<void> initializePeripheral() async {
|
||||
if (_isPeripheralInitialized) return;
|
||||
if (!Platform.isAndroid && !Platform.isIOS) {
|
||||
throw UnsupportedError('BLE Peripheral은 Android/iOS에서만 지원됩니다.');
|
||||
}
|
||||
|
||||
try {
|
||||
await ble_peripheral.BlePeripheral.initialize();
|
||||
_setupWriteCallback();
|
||||
_isPeripheralInitialized = true;
|
||||
debugPrint('[BLE] Peripheral 초기화 완료');
|
||||
} catch (e) {
|
||||
debugPrint('[BLE] Peripheral 초기화 실패: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 공유 코드로 수신 대기 시작
|
||||
Future<void> startListening(String code) async {
|
||||
await initializePeripheral();
|
||||
|
||||
// 기존 서비스 정리
|
||||
await _cleanupPeripheral();
|
||||
|
||||
_currentShareCode = code;
|
||||
_receivedChunks.clear();
|
||||
_expectedTotalChunks = 0;
|
||||
|
||||
try {
|
||||
// GATT 서비스 추가
|
||||
await ble_peripheral.BlePeripheral.addService(
|
||||
ble_peripheral.BleService(
|
||||
uuid: BleConstants.serviceUuid,
|
||||
primary: true,
|
||||
characteristics: [
|
||||
// 데이터 수신용 Characteristic (writable)
|
||||
ble_peripheral.BleCharacteristic(
|
||||
uuid: BleConstants.dataCharacteristicUuid,
|
||||
properties: [
|
||||
ble_peripheral.CharacteristicProperties.write.index,
|
||||
ble_peripheral.CharacteristicProperties.writeWithoutResponse.index,
|
||||
],
|
||||
permissions: [ble_peripheral.AttributePermissions.writeable.index],
|
||||
value: null,
|
||||
),
|
||||
// 공유 코드 확인용 Characteristic (readable)
|
||||
ble_peripheral.BleCharacteristic(
|
||||
uuid: BleConstants.codeCharacteristicUuid,
|
||||
properties: [ble_peripheral.CharacteristicProperties.read.index],
|
||||
permissions: [ble_peripheral.AttributePermissions.readable.index],
|
||||
value: Uint8List.fromList(utf8.encode(code)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// 광고 시작
|
||||
await ble_peripheral.BlePeripheral.startAdvertising(
|
||||
services: [BleConstants.serviceUuid],
|
||||
localName: '${BleConstants.advertiseNamePrefix}$code',
|
||||
);
|
||||
|
||||
debugPrint('[BLE] 광고 시작: ${BleConstants.advertiseNamePrefix}$code');
|
||||
} catch (e) {
|
||||
debugPrint('[BLE] 광고 시작 실패: $e');
|
||||
_currentShareCode = null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Write 콜백 설정
|
||||
void _setupWriteCallback() {
|
||||
ble_peripheral.BlePeripheral.setWriteRequestCallback((
|
||||
deviceId,
|
||||
characteristicId,
|
||||
offset,
|
||||
value,
|
||||
) {
|
||||
if (characteristicId.toLowerCase() ==
|
||||
BleConstants.dataCharacteristicUuid.toLowerCase()) {
|
||||
_handleReceivedChunk(value ?? Uint8List(0));
|
||||
}
|
||||
// null 반환 = 성공 (ble_peripheral 패키지 규약)
|
||||
return null;
|
||||
});
|
||||
|
||||
debugPrint('[BLE] Write 콜백 설정 완료');
|
||||
}
|
||||
|
||||
/// 수신된 청크 처리
|
||||
void _handleReceivedChunk(Uint8List data) {
|
||||
try {
|
||||
final decoded = utf8.decode(data);
|
||||
debugPrint('[BLE] 청크 수신: ${decoded.substring(0, decoded.length.clamp(0, 50))}...');
|
||||
|
||||
// 프로토콜: "CHUNK:index:total:data"
|
||||
if (decoded.startsWith('CHUNK:')) {
|
||||
final colonIndex1 = decoded.indexOf(':', 6);
|
||||
final colonIndex2 = decoded.indexOf(':', colonIndex1 + 1);
|
||||
|
||||
if (colonIndex1 == -1 || colonIndex2 == -1) {
|
||||
debugPrint('[BLE] 잘못된 청크 형식');
|
||||
return;
|
||||
}
|
||||
|
||||
final index = int.tryParse(decoded.substring(6, colonIndex1));
|
||||
final total = int.tryParse(decoded.substring(colonIndex1 + 1, colonIndex2));
|
||||
final chunkData = decoded.substring(colonIndex2 + 1);
|
||||
|
||||
if (index == null || total == null) {
|
||||
debugPrint('[BLE] 청크 인덱스/전체 파싱 실패');
|
||||
return;
|
||||
}
|
||||
|
||||
_expectedTotalChunks = total;
|
||||
_receivedChunks[index] = chunkData;
|
||||
|
||||
// 진행 상태 콜백
|
||||
onReceiveProgress?.call(_receivedChunks.length, total);
|
||||
|
||||
debugPrint('[BLE] 청크 $index/$total 수신 완료');
|
||||
|
||||
// 모든 청크 수신 완료
|
||||
if (_receivedChunks.length == total) {
|
||||
_assembleAndEmit();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[BLE] 청크 처리 오류: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 청크 조립 후 스트림으로 전송
|
||||
void _assembleAndEmit() {
|
||||
final sorted = _receivedChunks.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
final fullData = sorted.map((e) => e.value).join();
|
||||
|
||||
debugPrint('[BLE] 데이터 조립 완료: ${fullData.length} bytes');
|
||||
|
||||
_incomingDataController.add(fullData);
|
||||
_receivedChunks.clear();
|
||||
_expectedTotalChunks = 0;
|
||||
}
|
||||
|
||||
/// Peripheral 정리
|
||||
Future<void> _cleanupPeripheral() async {
|
||||
try {
|
||||
await ble_peripheral.BlePeripheral.stopAdvertising();
|
||||
await ble_peripheral.BlePeripheral.clearServices();
|
||||
} catch (e) {
|
||||
debugPrint('[BLE] Peripheral 정리 중 오류 (무시됨): $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 수신 대기 중지
|
||||
Future<void> stopListening() async {
|
||||
await _cleanupPeripheral();
|
||||
_currentShareCode = null;
|
||||
_receivedChunks.clear();
|
||||
_expectedTotalChunks = 0;
|
||||
debugPrint('[BLE] 광고 중지');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Sender (Central) 메서드 - flutter_blue_plus 사용
|
||||
// =========================================================================
|
||||
|
||||
/// 주변 LunchPick 기기 스캔
|
||||
Future<List<ShareDevice>> scanNearbyDevices() async {
|
||||
_discoveredDevices.clear();
|
||||
|
||||
// Bluetooth 상태 확인
|
||||
if (!await isBluetoothOn) {
|
||||
throw Exception('블루투스가 꺼져 있습니다. 블루투스를 켜주세요.');
|
||||
}
|
||||
|
||||
debugPrint('[BLE] 스캔 시작...');
|
||||
|
||||
// 스캔 결과 리스닝
|
||||
_scanSubscription = FlutterBluePlus.scanResults.listen(
|
||||
(results) {
|
||||
for (final result in results) {
|
||||
_processScannedDevice(result);
|
||||
}
|
||||
},
|
||||
onError: (e) {
|
||||
debugPrint('[BLE] 스캔 오류: $e');
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
// 서비스 UUID로 필터링하여 스캔
|
||||
await FlutterBluePlus.startScan(
|
||||
withServices: [Guid(BleConstants.serviceUuid)],
|
||||
timeout: Duration(seconds: BleConstants.scanTimeoutSeconds),
|
||||
);
|
||||
|
||||
// 스캔 완료 대기
|
||||
await Future.delayed(
|
||||
Duration(seconds: BleConstants.scanTimeoutSeconds + 1),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[BLE] 스캔 시작 오류: $e');
|
||||
} finally {
|
||||
await _scanSubscription?.cancel();
|
||||
_scanSubscription = null;
|
||||
}
|
||||
|
||||
debugPrint('[BLE] 스캔 완료: ${_discoveredDevices.length}개 기기 발견');
|
||||
return List.from(_discoveredDevices);
|
||||
}
|
||||
|
||||
/// 스캔된 디바이스 처리
|
||||
void _processScannedDevice(ScanResult result) {
|
||||
// 서비스 UUID 확인
|
||||
final hasService = result.advertisementData.serviceUuids.any(
|
||||
(uuid) =>
|
||||
uuid.toString().toLowerCase() ==
|
||||
BleConstants.serviceUuid.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!hasService) return;
|
||||
|
||||
// 광고 이름에서 공유 코드 추출
|
||||
final name = result.advertisementData.advName;
|
||||
if (name.isEmpty) return;
|
||||
|
||||
final code = name.startsWith(BleConstants.advertiseNamePrefix)
|
||||
? name.substring(BleConstants.advertiseNamePrefix.length)
|
||||
: name;
|
||||
|
||||
// 중복 방지
|
||||
if (_discoveredDevices.any((d) => d.deviceId == result.device.remoteId.str)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final device = ShareDevice(
|
||||
code: code,
|
||||
deviceId: result.device.remoteId.str,
|
||||
remoteId: result.device.remoteId.str,
|
||||
rssi: result.rssi,
|
||||
discoveredAt: DateTime.now(),
|
||||
bleDevice: result.device,
|
||||
);
|
||||
|
||||
_discoveredDevices.add(device);
|
||||
debugPrint('[BLE] 기기 발견: $code (RSSI: ${result.rssi})');
|
||||
}
|
||||
|
||||
/// 맛집 리스트 전송
|
||||
Future<void> sendRestaurantList(
|
||||
String targetCode,
|
||||
List<Restaurant> restaurants,
|
||||
) async {
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
if (!_listeningDevices.containsKey(targetCode)) {
|
||||
throw Exception('해당 코드를 찾을 수 없습니다.');
|
||||
// 대상 디바이스 찾기
|
||||
final targetDevice = _discoveredDevices.firstWhere(
|
||||
(d) => d.code == targetCode,
|
||||
orElse: () => throw Exception('대상 기기를 찾을 수 없습니다: $targetCode'),
|
||||
);
|
||||
|
||||
final bleDevice = targetDevice.bleDevice;
|
||||
if (bleDevice == null) {
|
||||
throw Exception('BLE 디바이스 정보가 없습니다.');
|
||||
}
|
||||
|
||||
final payload = jsonEncode(
|
||||
restaurants
|
||||
.map((restaurant) => _serializeRestaurant(restaurant))
|
||||
.toList(),
|
||||
debugPrint('[BLE] 연결 시작: $targetCode');
|
||||
|
||||
// 연결
|
||||
await bleDevice.connect(
|
||||
timeout: Duration(seconds: BleConstants.connectionTimeoutSeconds),
|
||||
);
|
||||
_incomingDataController.add(payload);
|
||||
_connectedDevice = bleDevice;
|
||||
|
||||
try {
|
||||
// 서비스 탐색
|
||||
debugPrint('[BLE] 서비스 탐색 중...');
|
||||
final services = await bleDevice.discoverServices();
|
||||
|
||||
final targetService = services.firstWhere(
|
||||
(s) =>
|
||||
s.uuid.toString().toLowerCase() ==
|
||||
BleConstants.serviceUuid.toLowerCase(),
|
||||
orElse: () => throw Exception('LunchPick 서비스를 찾을 수 없습니다.'),
|
||||
);
|
||||
|
||||
final dataCharacteristic = targetService.characteristics.firstWhere(
|
||||
(c) =>
|
||||
c.uuid.toString().toLowerCase() ==
|
||||
BleConstants.dataCharacteristicUuid.toLowerCase(),
|
||||
orElse: () => throw Exception('데이터 Characteristic을 찾을 수 없습니다.'),
|
||||
);
|
||||
|
||||
// JSON 직렬화
|
||||
final payload = jsonEncode(
|
||||
restaurants.map(_serializeRestaurant).toList(),
|
||||
);
|
||||
|
||||
debugPrint('[BLE] 전송 데이터 크기: ${payload.length} bytes');
|
||||
|
||||
// 청킹 및 전송
|
||||
final chunks = _splitIntoChunks(payload, BleConstants.chunkSize);
|
||||
debugPrint('[BLE] 청크 수: ${chunks.length}');
|
||||
|
||||
for (var i = 0; i < chunks.length; i++) {
|
||||
final chunkMessage = 'CHUNK:$i:${chunks.length}:${chunks[i]}';
|
||||
final data = utf8.encode(chunkMessage);
|
||||
|
||||
await dataCharacteristic.write(
|
||||
data,
|
||||
withoutResponse: false,
|
||||
);
|
||||
|
||||
// 진행 상태 콜백
|
||||
onSendProgress?.call(i + 1, chunks.length);
|
||||
|
||||
debugPrint('[BLE] 청크 ${i + 1}/${chunks.length} 전송 완료');
|
||||
|
||||
// 청크 간 딜레이
|
||||
if (i < chunks.length - 1) {
|
||||
await Future.delayed(
|
||||
Duration(milliseconds: BleConstants.chunkDelayMs),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('[BLE] 전송 완료!');
|
||||
} finally {
|
||||
// 연결 해제
|
||||
await bleDevice.disconnect();
|
||||
_connectedDevice = null;
|
||||
debugPrint('[BLE] 연결 해제');
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _serializeRestaurant(Restaurant restaurant) {
|
||||
return {
|
||||
'id': restaurant.id,
|
||||
'name': restaurant.name,
|
||||
'category': restaurant.category,
|
||||
'subCategory': restaurant.subCategory,
|
||||
'description': restaurant.description,
|
||||
'phoneNumber': restaurant.phoneNumber,
|
||||
'roadAddress': restaurant.roadAddress,
|
||||
'jibunAddress': restaurant.jibunAddress,
|
||||
'latitude': restaurant.latitude,
|
||||
'longitude': restaurant.longitude,
|
||||
'lastVisitDate': restaurant.lastVisitDate?.toIso8601String(),
|
||||
'source': restaurant.source.name,
|
||||
'createdAt': restaurant.createdAt.toIso8601String(),
|
||||
'updatedAt': restaurant.updatedAt.toIso8601String(),
|
||||
'naverPlaceId': restaurant.naverPlaceId,
|
||||
'naverUrl': restaurant.naverUrl,
|
||||
'businessHours': restaurant.businessHours,
|
||||
'lastVisited': restaurant.lastVisited?.toIso8601String(),
|
||||
'visitCount': restaurant.visitCount,
|
||||
};
|
||||
/// 문자열을 청크로 분할
|
||||
List<String> _splitIntoChunks(String data, int chunkSize) {
|
||||
final chunks = <String>[];
|
||||
for (var i = 0; i < data.length; i += chunkSize) {
|
||||
final end = (i + chunkSize > data.length) ? data.length : i + chunkSize;
|
||||
chunks.add(data.substring(i, end));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/// Restaurant를 JSON Map으로 변환
|
||||
Map<String, dynamic> _serializeRestaurant(Restaurant r) => {
|
||||
'id': r.id,
|
||||
'name': r.name,
|
||||
'category': r.category,
|
||||
'subCategory': r.subCategory,
|
||||
'description': r.description,
|
||||
'phoneNumber': r.phoneNumber,
|
||||
'roadAddress': r.roadAddress,
|
||||
'jibunAddress': r.jibunAddress,
|
||||
'latitude': r.latitude,
|
||||
'longitude': r.longitude,
|
||||
'lastVisitDate': r.lastVisitDate?.toIso8601String(),
|
||||
'source': r.source.name,
|
||||
'createdAt': r.createdAt.toIso8601String(),
|
||||
'updatedAt': r.updatedAt.toIso8601String(),
|
||||
'naverPlaceId': r.naverPlaceId,
|
||||
'naverUrl': r.naverUrl,
|
||||
'businessHours': r.businessHours,
|
||||
'lastVisited': r.lastVisited?.toIso8601String(),
|
||||
'visitCount': r.visitCount,
|
||||
};
|
||||
|
||||
/// 스캔 중지
|
||||
Future<void> stopScan() async {
|
||||
await FlutterBluePlus.stopScan();
|
||||
await _scanSubscription?.cancel();
|
||||
_scanSubscription = null;
|
||||
_discoveredDevices.clear();
|
||||
debugPrint('[BLE] 스캔 중지');
|
||||
}
|
||||
|
||||
/// 리소스 정리
|
||||
void dispose() {
|
||||
_incomingDataController.close();
|
||||
_listeningDevices.clear();
|
||||
_scanSubscription?.cancel();
|
||||
_connectedDevice?.disconnect();
|
||||
_cleanupPeripheral();
|
||||
debugPrint('[BLE] BluetoothService disposed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,68 @@
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
|
||||
/// BLE 공유를 위한 디바이스 정보
|
||||
class ShareDevice {
|
||||
/// 공유 코드 (6자리 숫자)
|
||||
final String code;
|
||||
|
||||
/// 디바이스 고유 ID
|
||||
final String deviceId;
|
||||
|
||||
/// BLE Remote ID (MAC 주소 또는 UUID)
|
||||
final String? remoteId;
|
||||
|
||||
/// 신호 강도 (RSSI)
|
||||
final int? rssi;
|
||||
|
||||
/// 발견 시각
|
||||
final DateTime discoveredAt;
|
||||
|
||||
/// flutter_blue_plus BluetoothDevice 참조
|
||||
/// 실제 BLE 연결 시 사용
|
||||
final BluetoothDevice? bleDevice;
|
||||
|
||||
ShareDevice({
|
||||
required this.code,
|
||||
required this.deviceId,
|
||||
this.remoteId,
|
||||
this.rssi,
|
||||
required this.discoveredAt,
|
||||
this.bleDevice,
|
||||
});
|
||||
|
||||
/// 신호 강도 레벨 (0-4)
|
||||
/// RSSI 값을 기반으로 계산
|
||||
int get signalLevel {
|
||||
if (rssi == null) return 0;
|
||||
if (rssi! >= -50) return 4; // 매우 강함
|
||||
if (rssi! >= -60) return 3; // 강함
|
||||
if (rssi! >= -70) return 2; // 보통
|
||||
if (rssi! >= -80) return 1; // 약함
|
||||
return 0; // 매우 약함
|
||||
}
|
||||
|
||||
/// 디버그용 문자열
|
||||
@override
|
||||
String toString() {
|
||||
return 'ShareDevice(code: $code, deviceId: $deviceId, rssi: $rssi)';
|
||||
}
|
||||
|
||||
/// 복사본 생성
|
||||
ShareDevice copyWith({
|
||||
String? code,
|
||||
String? deviceId,
|
||||
String? remoteId,
|
||||
int? rssi,
|
||||
DateTime? discoveredAt,
|
||||
BluetoothDevice? bleDevice,
|
||||
}) {
|
||||
return ShareDevice(
|
||||
code: code ?? this.code,
|
||||
deviceId: deviceId ?? this.deviceId,
|
||||
remoteId: remoteId ?? this.remoteId,
|
||||
rssi: rssi ?? this.rssi,
|
||||
discoveredAt: discoveredAt ?? this.discoveredAt,
|
||||
bleDevice: bleDevice ?? this.bleDevice,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,11 +211,11 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
||||
Text('이 코드를 상대방에게 알려주세요', style: AppTypography.caption(isDark)),
|
||||
const SizedBox(height: 16),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
_shareCode = null;
|
||||
});
|
||||
ref.read(bluetoothServiceProvider).stopListening();
|
||||
await ref.read(bluetoothServiceProvider).stopListening();
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text('취소'),
|
||||
@@ -551,7 +551,7 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
||||
setState(() {
|
||||
_shareCode = null;
|
||||
});
|
||||
ref.read(bluetoothServiceProvider).stopListening();
|
||||
await ref.read(bluetoothServiceProvider).stopListening();
|
||||
}
|
||||
|
||||
void _showLoadingDialog(String message) {
|
||||
|
||||
Reference in New Issue
Block a user