17 changed files with 19 additions and 2418 deletions
@ -1,10 +0,0 @@ |
|||||
{ |
|
||||
"mcpServers": { |
|
||||
"web-search": { |
|
||||
"url": "http://mcp.ideapsound.com:8000/sse" |
|
||||
}, |
|
||||
"amap-amap-sse": { |
|
||||
"url": "https://mcp.amap.com/sse?key=df7b0b6747c632fc38ddad22e7176646" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,11 +0,0 @@ |
|||||
import 'package:get/get.dart'; |
|
||||
import '../controllers/jieli_test_controller.dart'; |
|
||||
|
|
||||
class JieliTestBinding extends Bindings { |
|
||||
@override |
|
||||
void dependencies() { |
|
||||
Get.lazyPut<JieliTestController>( |
|
||||
() => JieliTestController(), |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -1,323 +0,0 @@ |
|||||
import 'dart:async'; |
|
||||
import 'dart:typed_data'; |
|
||||
import 'package:flutter/material.dart' hide ConnectionState; |
|
||||
import 'package:get/get.dart'; |
|
||||
import 'package:jieli/jieli.dart'; |
|
||||
|
|
||||
/// 杰理测试控制器 |
|
||||
class JieliTestController extends GetxController { |
|
||||
// Jieli插件实例 |
|
||||
late Jieli jieli; |
|
||||
|
|
||||
// 状态变量 |
|
||||
var isScanning = false.obs; |
|
||||
var deviceInfo = '未获取'.obs; |
|
||||
var connectStatus = '未连接'.obs; |
|
||||
var currentDevice = Rx<BleDevice?>(null); |
|
||||
var scannedDevices = RxList<BleDevice>([]); |
|
||||
var wakeupReceived = false.obs; |
|
||||
var audioDataSize = 0.obs; |
|
||||
var receivedFrames = 0.obs; |
|
||||
var audioDataReceiving = false.obs; |
|
||||
var volumeCommandResult = ''.obs; |
|
||||
|
|
||||
// 事件订阅 |
|
||||
StreamSubscription? _scanSubscription; |
|
||||
StreamSubscription? _connectionSubscription; |
|
||||
StreamSubscription? _wakeupSubscription; |
|
||||
StreamSubscription? _audioSubscription; |
|
||||
StreamSubscription? _commandResultSubscription; |
|
||||
|
|
||||
@override |
|
||||
void onInit() { |
|
||||
super.onInit(); |
|
||||
_initializeJieli(); |
|
||||
} |
|
||||
|
|
||||
@override |
|
||||
void onClose() { |
|
||||
_cancelAllSubscriptions(); |
|
||||
_disposeJieli(); |
|
||||
super.onClose(); |
|
||||
} |
|
||||
|
|
||||
/// 取消所有订阅 |
|
||||
void _cancelAllSubscriptions() { |
|
||||
_scanSubscription?.cancel(); |
|
||||
_connectionSubscription?.cancel(); |
|
||||
_wakeupSubscription?.cancel(); |
|
||||
_audioSubscription?.cancel(); |
|
||||
_commandResultSubscription?.cancel(); |
|
||||
} |
|
||||
|
|
||||
/// 初始化杰理SDK |
|
||||
void _initializeJieli() { |
|
||||
try { |
|
||||
jieli = Jieli(); |
|
||||
_setupEventListeners(); |
|
||||
} catch (e) { |
|
||||
// 初始化错误处理 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 设置事件监听器 |
|
||||
void _setupEventListeners() { |
|
||||
// 监听扫描结果 |
|
||||
_scanSubscription = jieli.scanResults.listen((devices) { |
|
||||
scannedDevices.assignAll(devices); |
|
||||
}); |
|
||||
|
|
||||
// 监听连接状态 |
|
||||
_connectionSubscription = jieli.connectionState.listen((state) { |
|
||||
switch (state) { |
|
||||
case ConnectionState.connected: |
|
||||
connectStatus.value = '已连接'; |
|
||||
break; |
|
||||
case ConnectionState.disconnected: |
|
||||
connectStatus.value = '已断开'; |
|
||||
currentDevice.value = null; |
|
||||
break; |
|
||||
case ConnectionState.connecting: |
|
||||
connectStatus.value = '连接中...'; |
|
||||
break; |
|
||||
case ConnectionState.disconnecting: |
|
||||
connectStatus.value = '断开中...'; |
|
||||
break; |
|
||||
case ConnectionState.error: |
|
||||
connectStatus.value = '连接错误'; |
|
||||
break; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 监听唤醒信号 |
|
||||
_wakeupSubscription = jieli.wakeupStream.listen((wakeup) { |
|
||||
wakeupReceived.value = true; |
|
||||
Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号'); |
|
||||
}); |
|
||||
|
|
||||
// 监听音频数据 |
|
||||
_audioSubscription = jieli.audioDataStream.listen((data) { |
|
||||
// 更新音频数据统计 |
|
||||
audioDataSize.value += data.length; |
|
||||
receivedFrames.value++; |
|
||||
audioDataReceiving.value = true; |
|
||||
}); |
|
||||
|
|
||||
// 监听命令结果 |
|
||||
_commandResultSubscription = jieli.commandResultStream.listen((result) { |
|
||||
// 处理音量调整命令结果 |
|
||||
if (result.commandType == Jieli.commandTypeVolume) { |
|
||||
volumeCommandResult.value = result.success ? '音量调整成功' : '音量调整失败'; |
|
||||
Get.snackbar( |
|
||||
result.success ? '音量调整成功' : '音量调整失败', |
|
||||
result.success ? '设备已更新音量' : '设备未能更新音量', |
|
||||
snackPosition: SnackPosition.BOTTOM, |
|
||||
backgroundColor: result.success ? Colors.green.withOpacity(0.7) : Colors.red.withOpacity(0.7), |
|
||||
colorText: Colors.white, |
|
||||
duration: Duration(seconds: 2), |
|
||||
); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/// 检查蓝牙状态 |
|
||||
Future<bool> checkBluetoothStatus() async { |
|
||||
try { |
|
||||
final isEnabled = await jieli.isBluetoothEnabled(); |
|
||||
if (!isEnabled) { |
|
||||
return await jieli.requestBluetoothEnable(); |
|
||||
} |
|
||||
return true; |
|
||||
} catch (e) { |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 开始扫描设备 |
|
||||
Future<void> startScan() async { |
|
||||
if (isScanning.value) return; |
|
||||
|
|
||||
final bluetoothReady = await checkBluetoothStatus(); |
|
||||
if (!bluetoothReady) { |
|
||||
Get.snackbar('蓝牙未启用', '请启用蓝牙后再试'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
isScanning.value = true; |
|
||||
|
|
||||
// 清空之前的设备列表 |
|
||||
scannedDevices.clear(); |
|
||||
|
|
||||
// 开始扫描,10秒后自动停止 |
|
||||
final success = await jieli.startScan(scanTimeout: 10000); |
|
||||
|
|
||||
if (!success) { |
|
||||
isScanning.value = false; |
|
||||
Get.snackbar('扫描失败', '启动扫描设备失败,请检查蓝牙权限'); |
|
||||
} |
|
||||
|
|
||||
// 10秒后自动更新UI状态 |
|
||||
Future.delayed(Duration(milliseconds: 10000), () { |
|
||||
isScanning.value = false; |
|
||||
}); |
|
||||
|
|
||||
} catch (e) { |
|
||||
isScanning.value = false; |
|
||||
Get.snackbar('扫描错误', e.toString()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 停止扫描 |
|
||||
Future<void> stopScan() async { |
|
||||
if (!isScanning.value) return; |
|
||||
|
|
||||
try { |
|
||||
await jieli.stopScan(); |
|
||||
isScanning.value = false; |
|
||||
} catch (e) { |
|
||||
// 停止扫描错误处理 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 连接到指定设备 |
|
||||
Future<void> connectToDevice(BleDevice device) async { |
|
||||
try { |
|
||||
// 先断开当前连接 |
|
||||
if (currentDevice.value != null) { |
|
||||
await disconnectDevice(); |
|
||||
} |
|
||||
|
|
||||
// 更新UI状态 |
|
||||
connectStatus.value = '连接中...'; |
|
||||
|
|
||||
// 重置音频和唤醒相关的状态 |
|
||||
wakeupReceived.value = false; |
|
||||
audioDataSize.value = 0; |
|
||||
receivedFrames.value = 0; |
|
||||
audioDataReceiving.value = false; |
|
||||
|
|
||||
// 连接设备 |
|
||||
final success = await jieli.connect(device.id); |
|
||||
|
|
||||
if (success) { |
|
||||
// 连接成功,更新当前设备 |
|
||||
currentDevice.value = device; |
|
||||
deviceInfo.value = '设备名称: ${device.name}\n设备ID: ${device.id}\nRSSI: ${device.rssi} dBm'; |
|
||||
} else { |
|
||||
deviceInfo.value = '连接失败'; |
|
||||
} |
|
||||
} catch (e) { |
|
||||
deviceInfo.value = '连接错误: ${e.toString()}'; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 断开设备连接 |
|
||||
Future<void> disconnectDevice() async { |
|
||||
if (currentDevice.value == null) return; |
|
||||
|
|
||||
try { |
|
||||
connectStatus.value = '断开中...'; |
|
||||
|
|
||||
final success = await jieli.disconnect(); |
|
||||
|
|
||||
if (success) { |
|
||||
deviceInfo.value = '未连接设备'; |
|
||||
} |
|
||||
|
|
||||
} catch (e) { |
|
||||
// 断开连接错误处理 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 发送开始录音命令 |
|
||||
Future<void> sendStartRecordingCommand() async { |
|
||||
if (currentDevice.value == null) { |
|
||||
Get.snackbar('操作失败', '未连接设备'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
final success = await jieli.sendStartRecordingCommand(); |
|
||||
|
|
||||
if (success) { |
|
||||
Get.snackbar('命令已发送', '已发送开始录音命令'); |
|
||||
} else { |
|
||||
Get.snackbar('命令发送失败', '发送开始录音命令失败'); |
|
||||
} |
|
||||
} catch (e) { |
|
||||
Get.snackbar('命令错误', e.toString()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 发送停止录音命令 |
|
||||
Future<void> sendStopRecordingCommand() async { |
|
||||
if (currentDevice.value == null) { |
|
||||
Get.snackbar('操作失败', '未连接设备'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
final success = await jieli.sendStopRecordingCommand(); |
|
||||
|
|
||||
if (success) { |
|
||||
Get.snackbar('命令已发送', '已发送停止录音命令'); |
|
||||
} else { |
|
||||
Get.snackbar('命令发送失败', '发送停止录音命令失败'); |
|
||||
} |
|
||||
} catch (e) { |
|
||||
Get.snackbar('命令错误', e.toString()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 增加音量 |
|
||||
Future<void> increaseVolume() async { |
|
||||
if (currentDevice.value == null) { |
|
||||
Get.snackbar('操作失败', '未连接设备'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
volumeCommandResult.value = '发送中...'; |
|
||||
final success = await jieli.increaseVolume(); |
|
||||
|
|
||||
if (!success) { |
|
||||
Get.snackbar('命令发送失败', '发送增加音量命令失败'); |
|
||||
volumeCommandResult.value = '发送失败'; |
|
||||
} |
|
||||
} catch (e) { |
|
||||
Get.snackbar('命令错误', e.toString()); |
|
||||
volumeCommandResult.value = '发送出错'; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 减小音量 |
|
||||
Future<void> decreaseVolume() async { |
|
||||
if (currentDevice.value == null) { |
|
||||
Get.snackbar('操作失败', '未连接设备'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
volumeCommandResult.value = '发送中...'; |
|
||||
final success = await jieli.decreaseVolume(); |
|
||||
|
|
||||
if (!success) { |
|
||||
Get.snackbar('命令发送失败', '发送减小音量命令失败'); |
|
||||
volumeCommandResult.value = '发送失败'; |
|
||||
} |
|
||||
} catch (e) { |
|
||||
Get.snackbar('命令错误', e.toString()); |
|
||||
volumeCommandResult.value = '发送出错'; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 清理资源 |
|
||||
void _disposeJieli() { |
|
||||
try { |
|
||||
jieli.dispose(); |
|
||||
} catch (e) { |
|
||||
// 释放资源错误处理 |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,568 +0,0 @@ |
|||||
import 'package:flutter/material.dart'; |
|
||||
import 'package:get/get.dart'; |
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
||||
import '../controllers/jieli_test_controller.dart'; |
|
||||
import 'package:jieli/jieli.dart'; |
|
||||
|
|
||||
class JieliTestView extends GetView<JieliTestController> { |
|
||||
const JieliTestView({Key? key}) : super(key: key); |
|
||||
|
|
||||
@override |
|
||||
Widget build(BuildContext context) { |
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|
||||
|
|
||||
return Scaffold( |
|
||||
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], |
|
||||
appBar: AppBar( |
|
||||
backgroundColor: isDarkMode ? Colors.black : Colors.white, |
|
||||
elevation: 0.5, |
|
||||
title: Text( |
|
||||
'杰理蓝牙测试', |
|
||||
style: TextStyle( |
|
||||
fontSize: 17.sp, |
|
||||
fontWeight: FontWeight.w600, |
|
||||
color: isDarkMode ? Colors.white : Colors.black, |
|
||||
), |
|
||||
), |
|
||||
centerTitle: true, |
|
||||
leading: IconButton( |
|
||||
icon: Icon( |
|
||||
Icons.arrow_back_ios, |
|
||||
color: isDarkMode ? Colors.white : Colors.black, |
|
||||
size: 20.sp, |
|
||||
), |
|
||||
onPressed: () => Get.back(), |
|
||||
), |
|
||||
), |
|
||||
body: Padding( |
|
||||
padding: EdgeInsets.all(16.w), |
|
||||
child: Column( |
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|
||||
children: [ |
|
||||
_buildStatusCard(isDarkMode), |
|
||||
SizedBox(height: 16.h), |
|
||||
_buildActionButtons(isDarkMode), |
|
||||
SizedBox(height: 16.h), |
|
||||
_buildDeviceInfo(isDarkMode), |
|
||||
SizedBox(height: 16.h), |
|
||||
_buildDevicesList(isDarkMode), |
|
||||
], |
|
||||
), |
|
||||
), |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
Widget _buildStatusCard(bool isDarkMode) { |
|
||||
return Container( |
|
||||
padding: EdgeInsets.all(16.w), |
|
||||
decoration: BoxDecoration( |
|
||||
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|
||||
borderRadius: BorderRadius.circular(12.r), |
|
||||
boxShadow: [ |
|
||||
BoxShadow( |
|
||||
color: Colors.black.withOpacity(0.05), |
|
||||
blurRadius: 10, |
|
||||
offset: const Offset(0, 2), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
child: Column( |
|
||||
crossAxisAlignment: CrossAxisAlignment.start, |
|
||||
children: [ |
|
||||
Row( |
|
||||
children: [ |
|
||||
Text( |
|
||||
'连接状态: ', |
|
||||
style: TextStyle( |
|
||||
fontSize: 14.sp, |
|
||||
color: isDarkMode ? Colors.white : Colors.black87, |
|
||||
), |
|
||||
), |
|
||||
Obx(() => Text( |
|
||||
controller.connectStatus.value, |
|
||||
style: TextStyle( |
|
||||
fontSize: 14.sp, |
|
||||
fontWeight: FontWeight.bold, |
|
||||
color: _getStatusColor(controller.connectStatus.value), |
|
||||
), |
|
||||
)), |
|
||||
], |
|
||||
), |
|
||||
SizedBox(height: 8.h), |
|
||||
Row( |
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
||||
children: [ |
|
||||
Obx(() => _buildStatusChip( |
|
||||
label: '唤醒信号', |
|
||||
isActive: controller.wakeupReceived.value, |
|
||||
activeColor: Colors.orange, |
|
||||
isDarkMode: isDarkMode, |
|
||||
)), |
|
||||
Obx(() => _buildStatusChip( |
|
||||
label: '音频数据', |
|
||||
isActive: controller.audioDataReceiving.value, |
|
||||
activeColor: Colors.green, |
|
||||
isDarkMode: isDarkMode, |
|
||||
)), |
|
||||
], |
|
||||
), |
|
||||
SizedBox(height: 8.h), |
|
||||
Obx(() => controller.audioDataReceiving.value |
|
||||
? Column( |
|
||||
crossAxisAlignment: CrossAxisAlignment.start, |
|
||||
children: [ |
|
||||
Text( |
|
||||
'音频数据统计:', |
|
||||
style: TextStyle( |
|
||||
fontSize: 12.sp, |
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
||||
), |
|
||||
), |
|
||||
SizedBox(height: 4.h), |
|
||||
Text( |
|
||||
'已接收 ${(controller.audioDataSize.value / 1024).toStringAsFixed(2)} KB', |
|
||||
style: TextStyle( |
|
||||
fontSize: 12.sp, |
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
||||
), |
|
||||
), |
|
||||
Text( |
|
||||
'共 ${controller.receivedFrames.value} 帧', |
|
||||
style: TextStyle( |
|
||||
fontSize: 12.sp, |
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
||||
), |
|
||||
), |
|
||||
], |
|
||||
) |
|
||||
: SizedBox.shrink() |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
Widget _buildStatusChip({ |
|
||||
required String label, |
|
||||
required bool isActive, |
|
||||
required Color activeColor, |
|
||||
required bool isDarkMode, |
|
||||
}) { |
|
||||
return Container( |
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), |
|
||||
decoration: BoxDecoration( |
|
||||
color: isActive |
|
||||
? activeColor |
|
||||
: (isDarkMode ? Colors.grey[800] : Colors.grey[200]), |
|
||||
borderRadius: BorderRadius.circular(16.r), |
|
||||
), |
|
||||
child: Row( |
|
||||
mainAxisSize: MainAxisSize.min, |
|
||||
children: [ |
|
||||
Container( |
|
||||
width: 8.w, |
|
||||
height: 8.w, |
|
||||
decoration: BoxDecoration( |
|
||||
color: isActive ? Colors.white : Colors.grey, |
|
||||
shape: BoxShape.circle, |
|
||||
), |
|
||||
), |
|
||||
SizedBox(width: 6.w), |
|
||||
Text( |
|
||||
label, |
|
||||
style: TextStyle( |
|
||||
fontSize: 12.sp, |
|
||||
color: isActive |
|
||||
? Colors.white |
|
||||
: (isDarkMode ? Colors.white70 : Colors.black54), |
|
||||
fontWeight: isActive ? FontWeight.bold : FontWeight.normal, |
|
||||
), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
Widget _buildActionButtons(bool isDarkMode) { |
|
||||
return Column( |
|
||||
children: [ |
|
||||
Row( |
|
||||
children: [ |
|
||||
Expanded( |
|
||||
child: Obx(() => ElevatedButton.icon( |
|
||||
onPressed: controller.isScanning.value |
|
||||
? controller.stopScan |
|
||||
: controller.startScan, |
|
||||
icon: Icon( |
|
||||
controller.isScanning.value ? Icons.stop : Icons.bluetooth_searching, |
|
||||
size: 18.sp, |
|
||||
), |
|
||||
label: Text( |
|
||||
controller.isScanning.value ? '停止扫描' : '扫描设备', |
|
||||
style: TextStyle(fontSize: 14.sp), |
|
||||
), |
|
||||
style: ElevatedButton.styleFrom( |
|
||||
backgroundColor: controller.isScanning.value |
|
||||
? Colors.red[600] |
|
||||
: Colors.blue[600], |
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h), |
|
||||
shape: RoundedRectangleBorder( |
|
||||
borderRadius: BorderRadius.circular(8.r), |
|
||||
), |
|
||||
), |
|
||||
)), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
SizedBox(height: 8.h), |
|
||||
Row( |
|
||||
children: [ |
|
||||
Expanded( |
|
||||
child: ElevatedButton.icon( |
|
||||
onPressed: controller.sendStartRecordingCommand, |
|
||||
icon: Icon( |
|
||||
Icons.mic, |
|
||||
size: 18.sp, |
|
||||
), |
|
||||
label: Text( |
|
||||
'开始录音', |
|
||||
style: TextStyle(fontSize: 14.sp), |
|
||||
), |
|
||||
style: ElevatedButton.styleFrom( |
|
||||
backgroundColor: Colors.green[600], |
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h), |
|
||||
shape: RoundedRectangleBorder( |
|
||||
borderRadius: BorderRadius.circular(8.r), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
SizedBox(width: 8.w), |
|
||||
Expanded( |
|
||||
child: ElevatedButton.icon( |
|
||||
onPressed: controller.sendStopRecordingCommand, |
|
||||
icon: Icon( |
|
||||
Icons.stop_circle, |
|
||||
size: 18.sp, |
|
||||
), |
|
||||
label: Text( |
|
||||
'停止录音', |
|
||||
style: TextStyle(fontSize: 14.sp), |
|
||||
), |
|
||||
style: ElevatedButton.styleFrom( |
|
||||
backgroundColor: Colors.red[600], |
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h), |
|
||||
shape: RoundedRectangleBorder( |
|
||||
borderRadius: BorderRadius.circular(8.r), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
SizedBox(height: 8.h), |
|
||||
Row( |
|
||||
children: [ |
|
||||
Expanded( |
|
||||
child: ElevatedButton.icon( |
|
||||
onPressed: controller.increaseVolume, |
|
||||
icon: Icon( |
|
||||
Icons.volume_up, |
|
||||
size: 18.sp, |
|
||||
), |
|
||||
label: Text( |
|
||||
'增加音量', |
|
||||
style: TextStyle(fontSize: 14.sp), |
|
||||
), |
|
||||
style: ElevatedButton.styleFrom( |
|
||||
backgroundColor: Colors.blue[600], |
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h), |
|
||||
shape: RoundedRectangleBorder( |
|
||||
borderRadius: BorderRadius.circular(8.r), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
SizedBox(width: 8.w), |
|
||||
Expanded( |
|
||||
child: ElevatedButton.icon( |
|
||||
onPressed: controller.decreaseVolume, |
|
||||
icon: Icon( |
|
||||
Icons.volume_down, |
|
||||
size: 18.sp, |
|
||||
), |
|
||||
label: Text( |
|
||||
'减小音量', |
|
||||
style: TextStyle(fontSize: 14.sp), |
|
||||
), |
|
||||
style: ElevatedButton.styleFrom( |
|
||||
backgroundColor: Colors.orange[600], |
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h), |
|
||||
shape: RoundedRectangleBorder( |
|
||||
borderRadius: BorderRadius.circular(8.r), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
SizedBox(height: 8.h), |
|
||||
Obx(() => controller.volumeCommandResult.value.isNotEmpty |
|
||||
? Container( |
|
||||
width: double.infinity, |
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w), |
|
||||
decoration: BoxDecoration( |
|
||||
color: controller.volumeCommandResult.value.contains('成功') |
|
||||
? Colors.green[100] |
|
||||
: (controller.volumeCommandResult.value == '发送中...' |
|
||||
? Colors.blue[100] |
|
||||
: Colors.red[100]), |
|
||||
borderRadius: BorderRadius.circular(8.r), |
|
||||
), |
|
||||
child: Text( |
|
||||
'音量控制: ${controller.volumeCommandResult.value}', |
|
||||
style: TextStyle( |
|
||||
fontSize: 13.sp, |
|
||||
color: controller.volumeCommandResult.value.contains('成功') |
|
||||
? Colors.green[900] |
|
||||
: (controller.volumeCommandResult.value == '发送中...' |
|
||||
? Colors.blue[900] |
|
||||
: Colors.red[900]), |
|
||||
), |
|
||||
textAlign: TextAlign.center, |
|
||||
), |
|
||||
) |
|
||||
: SizedBox.shrink() |
|
||||
), |
|
||||
], |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
Widget _buildDeviceInfo(bool isDarkMode) { |
|
||||
return Container( |
|
||||
padding: EdgeInsets.all(16.w), |
|
||||
decoration: BoxDecoration( |
|
||||
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|
||||
borderRadius: BorderRadius.circular(12.r), |
|
||||
boxShadow: [ |
|
||||
BoxShadow( |
|
||||
color: Colors.black.withOpacity(0.05), |
|
||||
blurRadius: 10, |
|
||||
offset: const Offset(0, 2), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
child: Column( |
|
||||
crossAxisAlignment: CrossAxisAlignment.start, |
|
||||
children: [ |
|
||||
Text( |
|
||||
'设备信息:', |
|
||||
style: TextStyle( |
|
||||
fontSize: 14.sp, |
|
||||
color: isDarkMode ? Colors.white : Colors.black87, |
|
||||
), |
|
||||
), |
|
||||
SizedBox(height: 8.h), |
|
||||
Container( |
|
||||
padding: EdgeInsets.all(8.w), |
|
||||
decoration: BoxDecoration( |
|
||||
color: isDarkMode ? Colors.black : Colors.grey[100], |
|
||||
borderRadius: BorderRadius.circular(8.r), |
|
||||
), |
|
||||
child: Obx(() => Text( |
|
||||
controller.deviceInfo.value, |
|
||||
style: TextStyle( |
|
||||
fontSize: 13.sp, |
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
||||
height: 1.4, |
|
||||
), |
|
||||
)), |
|
||||
), |
|
||||
SizedBox(height: 16.h), |
|
||||
Obx(() => controller.currentDevice.value != null |
|
||||
? ElevatedButton.icon( |
|
||||
onPressed: controller.disconnectDevice, |
|
||||
icon: Icon( |
|
||||
Icons.bluetooth_disabled, |
|
||||
size: 16.sp, |
|
||||
), |
|
||||
label: Text( |
|
||||
'断开连接', |
|
||||
style: TextStyle(fontSize: 13.sp), |
|
||||
), |
|
||||
style: ElevatedButton.styleFrom( |
|
||||
backgroundColor: Colors.red[600], |
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), |
|
||||
shape: RoundedRectangleBorder( |
|
||||
borderRadius: BorderRadius.circular(8.r), |
|
||||
), |
|
||||
), |
|
||||
) |
|
||||
: SizedBox.shrink() |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
Widget _buildDevicesList(bool isDarkMode) { |
|
||||
return Expanded( |
|
||||
child: Container( |
|
||||
decoration: BoxDecoration( |
|
||||
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|
||||
borderRadius: BorderRadius.circular(12.r), |
|
||||
boxShadow: [ |
|
||||
BoxShadow( |
|
||||
color: Colors.black.withOpacity(0.05), |
|
||||
blurRadius: 10, |
|
||||
offset: const Offset(0, 2), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
child: Column( |
|
||||
crossAxisAlignment: CrossAxisAlignment.start, |
|
||||
children: [ |
|
||||
Padding( |
|
||||
padding: EdgeInsets.all(16.w), |
|
||||
child: Row( |
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
||||
children: [ |
|
||||
Text( |
|
||||
'扫描到的设备', |
|
||||
style: TextStyle( |
|
||||
fontSize: 16.sp, |
|
||||
fontWeight: FontWeight.w600, |
|
||||
color: isDarkMode ? Colors.white : Colors.black87, |
|
||||
), |
|
||||
), |
|
||||
Obx(() => Text( |
|
||||
'${controller.scannedDevices.length}个设备', |
|
||||
style: TextStyle( |
|
||||
fontSize: 12.sp, |
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
||||
), |
|
||||
)), |
|
||||
], |
|
||||
), |
|
||||
), |
|
||||
Divider(height: 1, thickness: 1, color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), |
|
||||
Expanded( |
|
||||
child: Obx(() => controller.scannedDevices.isEmpty |
|
||||
? Center( |
|
||||
child: controller.isScanning.value |
|
||||
? Column( |
|
||||
mainAxisAlignment: MainAxisAlignment.center, |
|
||||
children: [ |
|
||||
CircularProgressIndicator(), |
|
||||
SizedBox(height: 16.h), |
|
||||
Text( |
|
||||
'正在扫描...', |
|
||||
style: TextStyle( |
|
||||
fontSize: 14.sp, |
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
||||
), |
|
||||
), |
|
||||
], |
|
||||
) |
|
||||
: Text( |
|
||||
'未扫描到设备', |
|
||||
style: TextStyle( |
|
||||
fontSize: 14.sp, |
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
||||
), |
|
||||
), |
|
||||
) |
|
||||
: ListView.separated( |
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h), |
|
||||
itemCount: controller.scannedDevices.length, |
|
||||
separatorBuilder: (context, index) => Divider( |
|
||||
height: 1, |
|
||||
thickness: 1, |
|
||||
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], |
|
||||
), |
|
||||
itemBuilder: (context, index) { |
|
||||
final device = controller.scannedDevices[index]; |
|
||||
return ListTile( |
|
||||
dense: true, |
|
||||
title: Text( |
|
||||
device.name.isEmpty ? '未知设备' : device.name, |
|
||||
style: TextStyle( |
|
||||
fontSize: 14.sp, |
|
||||
fontWeight: FontWeight.w500, |
|
||||
color: isDarkMode ? Colors.white : Colors.black87, |
|
||||
), |
|
||||
), |
|
||||
subtitle: Column( |
|
||||
crossAxisAlignment: CrossAxisAlignment.start, |
|
||||
children: [ |
|
||||
Text( |
|
||||
'设备ID: ${device.id}', |
|
||||
style: TextStyle( |
|
||||
fontSize: 11.sp, |
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
||||
), |
|
||||
), |
|
||||
Text( |
|
||||
'RSSI: ${device.rssi} dBm', |
|
||||
style: TextStyle( |
|
||||
fontSize: 11.sp, |
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
||||
), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
trailing: Obx(() => controller.currentDevice.value?.id == device.id |
|
||||
? Chip( |
|
||||
label: Text( |
|
||||
'已连接', |
|
||||
style: TextStyle( |
|
||||
fontSize: 12.sp, |
|
||||
color: Colors.white, |
|
||||
), |
|
||||
), |
|
||||
backgroundColor: Colors.green, |
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w), |
|
||||
) |
|
||||
: ElevatedButton( |
|
||||
onPressed: () => controller.connectToDevice(device), |
|
||||
child: Text( |
|
||||
'连接', |
|
||||
style: TextStyle(fontSize: 12.sp), |
|
||||
), |
|
||||
style: ElevatedButton.styleFrom( |
|
||||
backgroundColor: Colors.green[600], |
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), |
|
||||
shape: RoundedRectangleBorder( |
|
||||
borderRadius: BorderRadius.circular(6.r), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), |
|
||||
isThreeLine: true, |
|
||||
); |
|
||||
}, |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
], |
|
||||
), |
|
||||
), |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
Color _getStatusColor(String status) { |
|
||||
switch (status) { |
|
||||
case '连接中...': |
|
||||
return Colors.orange; |
|
||||
case '已连接': |
|
||||
return Colors.green; |
|
||||
case '连接失败': |
|
||||
return Colors.red; |
|
||||
case '已断开': |
|
||||
return Colors.grey; |
|
||||
default: |
|
||||
return Colors.grey; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,144 +0,0 @@ |
|||||
# Jieli 插件 |
|
||||
|
|
||||
一个用于连接杰理蓝牙耳机的Flutter插件,支持Android 12+的BLE通信。 |
|
||||
|
|
||||
## 功能 |
|
||||
|
|
||||
- 扫描支持特定BLE服务的耳机设备 |
|
||||
- 连接设备并发现服务 |
|
||||
- 接收耳机的唤醒通知 |
|
||||
- 向耳机发送控制命令启动录音 |
|
||||
- 接收音频数据流 |
|
||||
|
|
||||
## 技术规范 |
|
||||
|
|
||||
此插件实现了以下BLE特性: |
|
||||
|
|
||||
- **服务UUID**: 0000A100-0000-1000-8000-00805F9B34FB |
|
||||
- **音频数据特征UUID**: 0000A101-0000-1000-8000-00805F9B34FB |
|
||||
- **控制特征UUID**: 0000A102-0000-1000-8000-00805F9B34FB |
|
||||
|
|
||||
## 使用方法 |
|
||||
|
|
||||
### 初始化插件 |
|
||||
|
|
||||
```dart |
|
||||
final jieli = Jieli(); |
|
||||
``` |
|
||||
|
|
||||
### 检查蓝牙状态 |
|
||||
|
|
||||
```dart |
|
||||
bool isEnabled = await jieli.isBluetoothEnabled(); |
|
||||
if (!isEnabled) { |
|
||||
await jieli.requestBluetoothEnable(); |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### 扫描设备 |
|
||||
|
|
||||
```dart |
|
||||
// 监听扫描结果 |
|
||||
jieli.scanResults.listen((devices) { |
|
||||
// 处理扫描到的设备列表 |
|
||||
for (var device in devices) { |
|
||||
print('发现设备: ${device.name} (${device.id}), RSSI: ${device.rssi}'); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 开始扫描,10秒后自动停止 |
|
||||
await jieli.startScan(scanTimeout: 10000); |
|
||||
|
|
||||
// 手动停止扫描 |
|
||||
await jieli.stopScan(); |
|
||||
``` |
|
||||
|
|
||||
### 连接设备 |
|
||||
|
|
||||
```dart |
|
||||
// 监听连接状态 |
|
||||
jieli.connectionState.listen((state) { |
|
||||
switch (state) { |
|
||||
case ConnectionState.connected: |
|
||||
print('设备已连接'); |
|
||||
break; |
|
||||
case ConnectionState.disconnected: |
|
||||
print('设备已断开'); |
|
||||
break; |
|
||||
case ConnectionState.connecting: |
|
||||
print('正在连接...'); |
|
||||
break; |
|
||||
case ConnectionState.disconnecting: |
|
||||
print('正在断开...'); |
|
||||
break; |
|
||||
case ConnectionState.error: |
|
||||
print('连接错误'); |
|
||||
break; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 连接到设备 |
|
||||
await jieli.connect(deviceId); |
|
||||
|
|
||||
// 断开连接 |
|
||||
await jieli.disconnect(); |
|
||||
``` |
|
||||
|
|
||||
### 接收唤醒信号 |
|
||||
|
|
||||
```dart |
|
||||
// 监听唤醒信号 |
|
||||
jieli.wakeupStream.listen((wakeup) { |
|
||||
print('收到唤醒信号'); |
|
||||
// 通常在收到唤醒后自动发送开始录音命令 |
|
||||
}); |
|
||||
``` |
|
||||
|
|
||||
### 发送录音命令 |
|
||||
|
|
||||
```dart |
|
||||
// 发送开始录音命令 |
|
||||
await jieli.sendStartRecordingCommand(); |
|
||||
|
|
||||
// 发送停止录音命令 |
|
||||
await jieli.sendStopRecordingCommand(); |
|
||||
``` |
|
||||
|
|
||||
### 接收音频数据 |
|
||||
|
|
||||
```dart |
|
||||
// 监听音频数据流 |
|
||||
jieli.audioDataStream.listen((data) { |
|
||||
print('收到音频数据: ${data.length} 字节'); |
|
||||
// 处理音频数据 |
|
||||
}); |
|
||||
``` |
|
||||
|
|
||||
### 资源清理 |
|
||||
|
|
||||
```dart |
|
||||
// 在不再需要插件时释放资源 |
|
||||
await jieli.dispose(); |
|
||||
``` |
|
||||
|
|
||||
## 权限 |
|
||||
|
|
||||
使用此插件需要以下Android权限: |
|
||||
|
|
||||
- Android 12+ (API 31+): `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT` |
|
||||
- Android 11及以下: `BLUETOOTH`, `BLUETOOTH_ADMIN` |
|
||||
|
|
||||
插件会在需要时自动请求权限,但建议在应用清单文件中声明这些权限: |
|
||||
|
|
||||
```xml |
|
||||
<!-- 针对 Android 11 及以下的传统蓝牙权限 --> |
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" |
|
||||
android:maxSdkVersion="30" /> |
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" |
|
||||
android:maxSdkVersion="30" /> |
|
||||
|
|
||||
<!-- Android 12+ 新的 BLE 扫描和连接权限 --> |
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" |
|
||||
android:usesPermissionFlags="neverForLocation" /> |
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> |
|
||||
``` |
|
||||
@ -1,57 +0,0 @@ |
|||||
plugins { |
|
||||
// Android Library 插件 |
|
||||
id("com.android.library") |
|
||||
// Kotlin Android 插件 |
|
||||
id("org.jetbrains.kotlin.android") |
|
||||
id("com.github.johnrengelman.shadow") version "8.1.1" |
|
||||
} |
|
||||
|
|
||||
android { |
|
||||
// 命名空间,对应你插件的包名(需与代码内包名保持一致) |
|
||||
namespace = "com.yunqiinnovation.jieli" |
|
||||
|
|
||||
// 目标 SDK 版本 |
|
||||
compileSdk = 35 |
|
||||
|
|
||||
defaultConfig { |
|
||||
// 最低 SDK 版本 |
|
||||
minSdk = 21 |
|
||||
targetSdk = 33 |
|
||||
} |
|
||||
|
|
||||
// Java 语言级别兼容配置 |
|
||||
compileOptions { |
|
||||
sourceCompatibility = JavaVersion.VERSION_11 |
|
||||
targetCompatibility = JavaVersion.VERSION_11 |
|
||||
} |
|
||||
|
|
||||
// Kotlin 语言级别 |
|
||||
kotlinOptions { |
|
||||
jvmTarget = "11" |
|
||||
} |
|
||||
|
|
||||
// 添加packaging配置,排除冲突文件 |
|
||||
packaging { |
|
||||
resources { |
|
||||
excludes.add("META-INF/DEPENDENCIES") |
|
||||
excludes.add("META-INF/LICENSE") |
|
||||
excludes.add("META-INF/LICENSE.txt") |
|
||||
excludes.add("META-INF/license.txt") |
|
||||
excludes.add("META-INF/NOTICE") |
|
||||
excludes.add("META-INF/NOTICE.txt") |
|
||||
excludes.add("META-INF/notice.txt") |
|
||||
excludes.add("META-INF/*.kotlin_module") |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
dependencies { |
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10") |
|
||||
implementation("androidx.core:core-ktx:1.7.0") |
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0") |
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0") |
|
||||
implementation("androidx.lifecycle:lifecycle-service:2.4.0") |
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") |
|
||||
implementation(project(":azure_speech")) |
|
||||
implementation(project(":jl_opus")) |
|
||||
} |
|
||||
@ -1 +0,0 @@ |
|||||
rootProject.name = "jieli" |
|
||||
@ -1,22 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||
package="com.yunqiinnovation.jieli"> |
|
||||
|
|
||||
<!-- 声明需要蓝牙功能 --> |
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> |
|
||||
|
|
||||
<!-- 针对 Android 11 及以下的传统蓝牙权限 --> |
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" |
|
||||
android:maxSdkVersion="30" /> |
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" |
|
||||
android:maxSdkVersion="30" /> |
|
||||
|
|
||||
<!-- Android 12+ 新的 BLE 扫描和连接权限 --> |
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" |
|
||||
android:usesPermissionFlags="neverForLocation" /> |
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> |
|
||||
|
|
||||
<!-- 前台服务权限 --> |
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> |
|
||||
|
|
||||
</manifest> |
|
||||
@ -1,665 +0,0 @@ |
|||||
package com.yunqiinnovation.jieli |
|
||||
|
|
||||
import android.bluetooth.* |
|
||||
import android.bluetooth.le.* |
|
||||
import android.content.Context |
|
||||
import android.os.Handler |
|
||||
import android.os.Looper |
|
||||
import android.os.ParcelUuid |
|
||||
import android.util.Log |
|
||||
import androidx.lifecycle.LifecycleService |
|
||||
import androidx.lifecycle.MutableLiveData |
|
||||
import kotlinx.coroutines.* |
|
||||
import java.io.File |
|
||||
import java.io.FileOutputStream |
|
||||
import java.text.SimpleDateFormat |
|
||||
import java.util.* |
|
||||
import com.yunqiinnovation.azure_speech.AzureAsrHelper |
|
||||
import com.yunqiinnovation.azure_speech.AzureTtsHelper |
|
||||
import com.yunqiinnovation.jl_opus.JlOpusPlugin |
|
||||
|
|
||||
/** |
|
||||
* BLE服务类,提供核心蓝牙功能 |
|
||||
*/ |
|
||||
class BleService(private val context: Context) { |
|
||||
companion object { |
|
||||
private const val TAG = "JieliBleService" |
|
||||
|
|
||||
// 服务和特征UUID - 根据协议规范重新定义 |
|
||||
// private const val AUDIO_SERVICE_UUID = "0000faa0-0000-1000-8000-00805f9b34fb" // 主服务UUID |
|
||||
private const val AUDIO_SERVICE_UUID = "0000abc0-0000-1111-2222-123456789abc" // 主服务UUID |
|
||||
private const val NOTIFY_CHAR_UUID = |
|
||||
"0000abc2-0000-1111-2222-123456789abc" // 用于接收数据的特征(notify) |
|
||||
private const val WRITE_CHAR_UUID = |
|
||||
"0000abc1-0000-1111-2222-123456789abc" // 用于发送命令的特征(write without response) |
|
||||
private const val CLIENT_CHAR_CONFIG_UUID = |
|
||||
"00002902-0000-1000-8000-00805f9b34fb" // 标准的客户端特征配置描述符 |
|
||||
|
|
||||
// 连接状态常量 |
|
||||
const val STATE_DISCONNECTED = 0 |
|
||||
const val STATE_CONNECTING = 1 |
|
||||
const val STATE_CONNECTED = 2 |
|
||||
const val STATE_DISCONNECTING = 3 |
|
||||
const val STATE_ERROR = 4 |
|
||||
|
|
||||
// 扫描结果更新间隔(毫秒) |
|
||||
private const val SCAN_RESULT_UPDATE_INTERVAL = 500L |
|
||||
|
|
||||
// 命令类型常量 |
|
||||
const val COMMAND_VOLUME = 1 |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 回调接口 |
|
||||
*/ |
|
||||
interface Callback { |
|
||||
fun onScanResult(devices: List<Map<String, Any>>) |
|
||||
fun onConnectionStateChanged(state: Int) |
|
||||
fun onAudioDataReceived(data: ByteArray) |
|
||||
fun onWakeupSignalReceived() |
|
||||
fun onCommandResult(commandType: Int, success: Boolean) |
|
||||
} |
|
||||
|
|
||||
// region 文件相关 |
|
||||
private var outputFile: File? = null |
|
||||
private var fos: FileOutputStream? = null |
|
||||
|
|
||||
// Azure服务 |
|
||||
private var azureAsrHelper: AzureAsrHelper? = null |
|
||||
private var azureTtsHelper: AzureTtsHelper? = null |
|
||||
|
|
||||
// 获取插件实例 |
|
||||
private val opusPlugin = JlOpusPlugin() |
|
||||
|
|
||||
// 蓝牙管理器和适配器 |
|
||||
private val bluetoothManager = |
|
||||
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager |
|
||||
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter |
|
||||
private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner |
|
||||
|
|
||||
// GATT连接和特征 |
|
||||
private var bluetoothGatt: BluetoothGatt? = null |
|
||||
private var notifyChar: BluetoothGattCharacteristic? = null |
|
||||
private var writeChar: BluetoothGattCharacteristic? = null |
|
||||
|
|
||||
// 扫描相关 |
|
||||
private val scanHandler = Handler(Looper.getMainLooper()) |
|
||||
private val scanResults = ArrayList<ScanResult>() |
|
||||
private var isScanning = false |
|
||||
private var isPendingScanResultUpdate = false |
|
||||
private val scanResultUpdateRunnable = Runnable { |
|
||||
notifyScanResults() |
|
||||
isPendingScanResultUpdate = false |
|
||||
} |
|
||||
|
|
||||
// 连接状态LiveData |
|
||||
val connectionState = MutableLiveData<Int>(STATE_DISCONNECTED) |
|
||||
|
|
||||
// 统一回调接口 |
|
||||
var callback: Callback? = null |
|
||||
|
|
||||
/** |
|
||||
* 开始扫描BLE设备 |
|
||||
*/ |
|
||||
fun startScan(scanTimeout: Long = 5000): Boolean { |
|
||||
if (bluetoothLeScanner == null || isScanning) { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
Log.i(TAG, "开始扫描BLE设备...") |
|
||||
scanResults.clear() |
|
||||
|
|
||||
val serviceUuid = UUID.fromString(AUDIO_SERVICE_UUID) |
|
||||
val scanFilter = ScanFilter.Builder() |
|
||||
// .setServiceUuid(ParcelUuid(serviceUuid)) |
|
||||
.build() |
|
||||
val scanFilters = listOf(scanFilter) |
|
||||
|
|
||||
val scanSettings = ScanSettings.Builder() |
|
||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) |
|
||||
.build() |
|
||||
|
|
||||
bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback) |
|
||||
isScanning = true |
|
||||
|
|
||||
// 设置超时停止扫描 |
|
||||
scanHandler.postDelayed({ |
|
||||
Log.d( |
|
||||
TAG, |
|
||||
"定时器触发,准备停止扫描 [主线程:${Looper.myLooper() == Looper.getMainLooper()}]" |
|
||||
) |
|
||||
stopScan() |
|
||||
}, scanTimeout) |
|
||||
|
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 停止扫描 |
|
||||
*/ |
|
||||
fun stopScan(): Boolean { |
|
||||
Log.i(TAG, "停止扫描BLE设备") |
|
||||
|
|
||||
if (bluetoothLeScanner == null || !isScanning) { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
bluetoothLeScanner.stopScan(scanCallback) |
|
||||
scanHandler.removeCallbacksAndMessages(null) |
|
||||
isScanning = false |
|
||||
|
|
||||
// 最后一次发送扫描结果 |
|
||||
notifyScanResults() |
|
||||
|
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 扫描回调 |
|
||||
*/ |
|
||||
private val scanCallback = object : ScanCallback() { |
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) { |
|
||||
val device = result.device |
|
||||
val deviceName = device.name ?: "未知设备" |
|
||||
val deviceAddress = device.address |
|
||||
val rssi = result.rssi |
|
||||
val scanRecord = result.scanRecord |
|
||||
if (!deviceName.contains("zxd")) { |
|
||||
return |
|
||||
} |
|
||||
Log.d(TAG, "============发现设备: $deviceName [$deviceAddress], RSSI: $rssi") |
|
||||
|
|
||||
// 打印所有发现的UUID以便调试 |
|
||||
val serviceUuids = scanRecord?.serviceUuids |
|
||||
if (serviceUuids != null) { |
|
||||
for (uuid in serviceUuids) { |
|
||||
Log.d(TAG, "设备UUID: ${uuid.uuid}") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 检查制造商数据 |
|
||||
for (i in 0 until 65535) { |
|
||||
val manufacturerData = scanRecord?.getManufacturerSpecificData(i) |
|
||||
if (manufacturerData != null) { |
|
||||
// Log.d(TAG, "发现厂商ID: $i, 数据长度: ${manufacturerData.size}") |
|
||||
// 这里可以根据已知的厂商ID和格式解析数据 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 避免重复添加 |
|
||||
|
|
||||
val existingIndex = scanResults.indexOfFirst { it.device.address == deviceAddress } |
|
||||
|
|
||||
if (existingIndex >= 0) { |
|
||||
|
|
||||
scanResults[existingIndex] = result |
|
||||
} else { |
|
||||
Log.d(TAG, "扫描回调A, scanResults[existingIndex]ADD") |
|
||||
scanResults.add(result) |
|
||||
} |
|
||||
|
|
||||
// 如果没有待处理的更新,安排一个 |
|
||||
if (!isPendingScanResultUpdate) { |
|
||||
isPendingScanResultUpdate = true |
|
||||
scanHandler.postDelayed(scanResultUpdateRunnable, SCAN_RESULT_UPDATE_INTERVAL) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onScanFailed(errorCode: Int) { |
|
||||
Log.e(TAG, "扫描失败,错误码: $errorCode") |
|
||||
isScanning = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 将扫描结果通知回调 |
|
||||
*/ |
|
||||
private fun notifyScanResults() { |
|
||||
val devicesJson = scanResults.map { result -> |
|
||||
mapOf( |
|
||||
"id" to result.device.address, |
|
||||
"name" to (result.device.name ?: "未知设备"), |
|
||||
"rssi" to result.rssi |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
callback?.onScanResult(devicesJson) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 连接到指定设备 |
|
||||
*/ |
|
||||
fun connect(deviceAddress: String) { |
|
||||
if (bluetoothAdapter == null) { |
|
||||
Log.e(TAG, "蓝牙适配器不可用") |
|
||||
updateConnectionState(STATE_ERROR) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 先断开现有连接 |
|
||||
bluetoothGatt?.close() |
|
||||
bluetoothGatt = null |
|
||||
|
|
||||
try { |
|
||||
val device = bluetoothAdapter.getRemoteDevice(deviceAddress) |
|
||||
updateConnectionState(STATE_CONNECTING) |
|
||||
|
|
||||
// 连接GATT服务器 |
|
||||
Log.i(TAG, "正在连接设备: ${device.name ?: "未知设备"} [$deviceAddress]") |
|
||||
bluetoothGatt = |
|
||||
device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) |
|
||||
} catch (e: Exception) { |
|
||||
Log.e(TAG, "连接设备时发生错误: ${e.message}") |
|
||||
updateConnectionState(STATE_ERROR) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 断开连接 |
|
||||
*/ |
|
||||
fun disconnect() { |
|
||||
bluetoothGatt?.disconnect() |
|
||||
updateConnectionState(STATE_DISCONNECTING) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 更新连接状态并通知回调 |
|
||||
*/ |
|
||||
private fun updateConnectionState(state: Int) { |
|
||||
connectionState.postValue(state) |
|
||||
callback?.onConnectionStateChanged(state) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* GATT回调 |
|
||||
*/ |
|
||||
private val gattCallback = object : BluetoothGattCallback() { |
|
||||
|
|
||||
|
|
||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { |
|
||||
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { |
|
||||
Log.i(TAG, "设备已连接,开始发现服务...") |
|
||||
updateConnectionState(STATE_CONNECTED) |
|
||||
|
|
||||
// 开始搜索服务 |
|
||||
gatt.discoverServices() |
|
||||
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) { |
|
||||
Log.i(TAG, "设备已断开连接") |
|
||||
updateConnectionState(STATE_DISCONNECTED) |
|
||||
|
|
||||
// 清理资源 |
|
||||
gatt.close() |
|
||||
bluetoothGatt = null |
|
||||
notifyChar = null |
|
||||
writeChar = null |
|
||||
} else if (status != BluetoothGatt.GATT_SUCCESS) { |
|
||||
Log.e(TAG, "连接失败,status=$status") |
|
||||
updateConnectionState(STATE_ERROR) |
|
||||
|
|
||||
gatt.close() |
|
||||
bluetoothGatt = null |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { |
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) { |
|
||||
Log.i(TAG, "服务发现成功") |
|
||||
|
|
||||
// 查找目标服务 |
|
||||
val audioService = gatt.getService(UUID.fromString(AUDIO_SERVICE_UUID)) |
|
||||
if (audioService != null) { |
|
||||
Log.i(TAG, "找到音频服务,开始遍历特征...") |
|
||||
audioService.characteristics.forEach { char -> |
|
||||
Log.i(TAG, "特征UUID: ${char.uuid}") |
|
||||
} |
|
||||
Log.i(TAG, "找到音频服务$") |
|
||||
|
|
||||
// 获取特征 |
|
||||
notifyChar = audioService.getCharacteristic(UUID.fromString(NOTIFY_CHAR_UUID)) |
|
||||
writeChar = audioService.getCharacteristic(UUID.fromString(WRITE_CHAR_UUID)) |
|
||||
if (notifyChar != null && writeChar != null) { |
|
||||
Log.i(TAG, "找到所需特征") |
|
||||
|
|
||||
// 设置特征写入类型为无响应模式 |
|
||||
writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE |
|
||||
|
|
||||
// 注册特征通知 |
|
||||
setupNotifications(gatt) |
|
||||
|
|
||||
opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object : |
|
||||
JlOpusPlugin.CallbackOpus { |
|
||||
override fun onAudioDataReceivedOpus(pcmData: ByteArray) { |
|
||||
// Log.i(TAG, "收到解码控制通知: ${pcmData.contentToString()}") |
|
||||
callback?.onAudioDataReceived(pcmData) |
|
||||
// 处理解码后的 PCM 数据(如播放或写入文件) |
|
||||
} |
|
||||
}) |
|
||||
} else { |
|
||||
Log.e(TAG, "未找到所需特征") |
|
||||
updateConnectionState(STATE_ERROR) |
|
||||
} |
|
||||
} else { |
|
||||
Log.e(TAG, "未找到音频服务") |
|
||||
updateConnectionState(STATE_ERROR) |
|
||||
} |
|
||||
} else { |
|
||||
Log.e(TAG, "服务发现失败,status=$status") |
|
||||
updateConnectionState(STATE_ERROR) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 添加扩展函数(在文件顶部声明) |
|
||||
fun List<Byte>.toByteArray(): ByteArray { |
|
||||
return ByteArray(this.size) { this[it] } |
|
||||
} |
|
||||
|
|
||||
override fun onCharacteristicChanged( |
|
||||
gatt: BluetoothGatt, |
|
||||
characteristic: BluetoothGattCharacteristic |
|
||||
) { |
|
||||
val characteristicUuid = characteristic.uuid.toString() |
|
||||
// Log.d(TAG, "收到特征变化通知: $characteristicUuid") |
|
||||
|
|
||||
when (characteristicUuid) { |
|
||||
// NOTIFY_CHAR_UUID -> { |
|
||||
// // 收到音频数据 |
|
||||
// val audioData = characteristic.value |
|
||||
// Log.d(TAG, "收到音频数据帧,大小: ${audioData.size} 字节") |
|
||||
|
|
||||
// callback?.onAudioDataReceived(audioData) |
|
||||
// } |
|
||||
NOTIFY_CHAR_UUID -> { |
|
||||
// 处理控制消息 |
|
||||
val value = characteristic.value |
|
||||
// Log.i(TAG, "收到控制通知: ${value.size}") |
|
||||
|
|
||||
// 判断是否收到音频数据 |
|
||||
if (value.isNotEmpty() && (value[0].toInt() and 0xFF == 0x4b || value[0].toInt() and 0xFF == 0x48)) { |
|
||||
// 处理设备返回的协议数据 |
|
||||
fos?.write(value) |
|
||||
|
|
||||
opusPlugin?.handleDecodeRequest(value) |
|
||||
|
|
||||
|
|
||||
} |
|
||||
// 判断是否是协议格式响应 |
|
||||
if (value.isNotEmpty() && value[0].toInt() and 0xFF == 0xA5) { |
|
||||
// 处理设备返回的协议数据 |
|
||||
processResponseData(value) |
|
||||
} |
|
||||
// 假设0x01表示唤醒信号 |
|
||||
else if (value.isNotEmpty() && value[0].toInt() == 0x01) { |
|
||||
Log.i(TAG, "收到唤醒信号") |
|
||||
|
|
||||
callback?.onWakeupSignalReceived() |
|
||||
|
|
||||
// 收到唤醒后自动发送开始录音命令 |
|
||||
sendStartRecordingCommand() |
|
||||
|
|
||||
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onCharacteristicWrite( |
|
||||
gatt: BluetoothGatt, |
|
||||
characteristic: BluetoothGattCharacteristic, |
|
||||
status: Int |
|
||||
) { |
|
||||
if (characteristic.uuid.toString() == WRITE_CHAR_UUID) { |
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) { |
|
||||
Log.i(TAG, "命令发送成功: ${characteristic.value.contentToString()}") |
|
||||
} else { |
|
||||
Log.e(TAG, "命令发送失败,status=$status") |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onDescriptorWrite( |
|
||||
gatt: BluetoothGatt, |
|
||||
descriptor: BluetoothGattDescriptor, |
|
||||
status: Int |
|
||||
) { |
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) { |
|
||||
Log.i(TAG, "描述符写入成功: ${descriptor.uuid}") |
|
||||
Log.i(TAG, "特征通知设置成功,设备就绪") |
|
||||
} else { |
|
||||
Log.e(TAG, "描述符写入失败,status=$status") |
|
||||
updateConnectionState(STATE_ERROR) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 设置特征通知 |
|
||||
*/ |
|
||||
private fun setupNotifications(gatt: BluetoothGatt) { |
|
||||
// 启用 notify 特征通知 |
|
||||
if (notifyChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) { |
|
||||
gatt.setCharacteristicNotification(notifyChar, true) |
|
||||
|
|
||||
val descriptor = notifyChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID)) |
|
||||
descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE |
|
||||
|
|
||||
if (descriptor != null) { |
|
||||
gatt.writeDescriptor(descriptor) |
|
||||
Log.i(TAG, "设置数据通知特征通知") |
|
||||
} |
|
||||
} else { |
|
||||
Log.e(TAG, "数据通知特征不支持通知") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* 处理设备返回的数据 |
|
||||
* 协议格式: |
|
||||
* 协议头(0xA5) + 字段类型(1byte) + 数据长度(1byte) + 数据域(N byte) + 校验和(1byte) |
|
||||
*/ |
|
||||
private fun processResponseData(data: ByteArray) { |
|
||||
if (data.size < 4) { |
|
||||
Log.e(TAG, "返回数据格式错误,长度不足") |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 判断协议头 |
|
||||
if (data[0].toInt() and 0xFF != 0xA5) { |
|
||||
Log.e(TAG, "返回数据协议头错误: ${data[0].toInt() and 0xFF}") |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
val fieldType = data[1].toInt() and 0xFF |
|
||||
val dataLen = data[2].toInt() and 0xFF |
|
||||
|
|
||||
// 验证长度 |
|
||||
if (data.size != dataLen + 4) { // 协议头+字段类型+数据长度+数据域+校验和 |
|
||||
Log.e(TAG, "返回数据长度不匹配: 应为${dataLen + 4},实际为${data.size}") |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 计算校验和 |
|
||||
var checksum = 0 |
|
||||
for (i in 0 until data.size - 1) { |
|
||||
checksum += data[i].toInt() and 0xFF |
|
||||
} |
|
||||
checksum = checksum and 0xFF |
|
||||
|
|
||||
val receivedChecksum = data[data.size - 1].toInt() and 0xFF |
|
||||
|
|
||||
if (checksum != receivedChecksum) { |
|
||||
Log.e(TAG, "校验和错误: 计算值=$checksum, 接收值=$receivedChecksum") |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 解析字段类型 |
|
||||
when (fieldType) { |
|
||||
0x01 -> { |
|
||||
// 音量调整命令的响应 |
|
||||
val result = data[3].toInt() and 0xFF |
|
||||
val success = result == 0 |
|
||||
Log.i(TAG, "音量调整命令响应: ${if (success) "成功" else "失败"}") |
|
||||
callback?.onCommandResult(COMMAND_VOLUME, success) |
|
||||
} |
|
||||
// 其他命令类型可在此处添加 |
|
||||
else -> { |
|
||||
Log.d(TAG, "未知命令类型: ${String.format("0x%02X", fieldType)}") |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 发送开始录音命令 |
|
||||
*/ |
|
||||
fun sendStartRecordingCommand(): Boolean { |
|
||||
if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) { |
|
||||
Log.e(TAG, "无法发送开始录音命令,设备未连接") |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
val commandData = byteArrayOf(0x01) |
|
||||
writeChar?.value = commandData |
|
||||
// 获取外部存储目录路径 |
|
||||
// 生成带时间戳的文件名(格式:yyyyMMdd_HHmmss_audio.opus) |
|
||||
val timeStamp = |
|
||||
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) |
|
||||
val fileName = "${timeStamp}_audio.opus" |
|
||||
val externalDir = context.getExternalFilesDir(null) |
|
||||
outputFile = File(externalDir, fileName) |
|
||||
fos = FileOutputStream(outputFile).also { |
|
||||
Log.i(TAG, "音频文件创建成功:${outputFile?.absolutePath}") |
|
||||
} |
|
||||
|
|
||||
// val subscriptionKey = call.argument<String>("subscriptionKey") ?: "" |
|
||||
// val region = call.argument<String>("region") ?: "" |
|
||||
// val supportedLanguages = call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN") |
|
||||
|
|
||||
// val success = azureAsrHelper.initialize(subscriptionKey, region, supportedLanguages.toTypedArray()) |
|
||||
// result.success(success) |
|
||||
|
|
||||
|
|
||||
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false |
|
||||
|
|
||||
if (success) { |
|
||||
Log.i(TAG, "开始录音命令已发送") |
|
||||
} else { |
|
||||
Log.e(TAG, "开始录音命令发送失败") |
|
||||
} |
|
||||
|
|
||||
return success |
|
||||
} catch (e: Exception) { |
|
||||
Log.e(TAG, "发送开始录音命令时发生异常: ${e.message}") |
|
||||
return false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 发送停止录音命令 |
|
||||
*/ |
|
||||
fun sendStopRecordingCommand(): Boolean { |
|
||||
if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) { |
|
||||
Log.e(TAG, "无法发送停止录音命令,设备未连接") |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
val commandData = byteArrayOf(0x02) |
|
||||
writeChar?.value = commandData |
|
||||
|
|
||||
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false |
|
||||
fos?.close() |
|
||||
if (success) { |
|
||||
Log.i(TAG, "停止录音命令已发送") |
|
||||
} else { |
|
||||
Log.e(TAG, "停止录音命令发送失败") |
|
||||
} |
|
||||
|
|
||||
return success |
|
||||
} catch (e: Exception) { |
|
||||
Log.e(TAG, "发送停止录音命令时发生异常: ${e.message}") |
|
||||
return false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 发送音量调整命令 |
|
||||
* 协议格式: |
|
||||
* 协议头(0xAA) + 字段类型(0x01) + 数据长度(0x01) + 音量值(0~16) + 校验和 |
|
||||
*/ |
|
||||
fun adjustVolume(volume: Int): Boolean { |
|
||||
Log.e( |
|
||||
TAG, |
|
||||
"adjustVolume,bluetoothGatt=${bluetoothGatt},writeChar=${writeChar},connectionState=${connectionState.value}" |
|
||||
) |
|
||||
if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) { |
|
||||
Log.e(TAG, "无法发送音量调整命令,设备未连接") |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
// 限制音量范围为0-16 |
|
||||
val safeVolume = volume.coerceIn(0, 16) |
|
||||
|
|
||||
try { |
|
||||
// 构建命令数据 |
|
||||
val header: Byte = 0xAA.toByte() |
|
||||
val fieldType: Byte = 0x01 |
|
||||
val dataLen: Byte = 0x01 |
|
||||
val volumeData: Byte = safeVolume.toByte() |
|
||||
|
|
||||
// 计算校验和 |
|
||||
val checksum = ((header.toInt() and 0xFF) + |
|
||||
(fieldType.toInt() and 0xFF) + |
|
||||
(dataLen.toInt() and 0xFF) + |
|
||||
(volumeData.toInt() and 0xFF)) and 0xFF |
|
||||
|
|
||||
// 创建完整命令 |
|
||||
val commandData = |
|
||||
byteArrayOf(header, fieldType, dataLen, volumeData, checksum.toByte()) |
|
||||
|
|
||||
// 写入特征 |
|
||||
writeChar?.value = commandData |
|
||||
|
|
||||
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false |
|
||||
|
|
||||
if (success) { |
|
||||
Log.i(TAG, "音量调整命令已发送: $safeVolume") |
|
||||
} else { |
|
||||
Log.e(TAG, "音量调整命令发送失败") |
|
||||
} |
|
||||
|
|
||||
return success |
|
||||
} catch (e: Exception) { |
|
||||
Log.e(TAG, "发送音量调整命令时发生异常: ${e.message}") |
|
||||
return false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 增加音量 |
|
||||
*/ |
|
||||
fun increaseVolume(): Boolean { |
|
||||
// 假设当前音量保存在设备中,我们只需发送增加命令 |
|
||||
return adjustVolume(16) // 发送最大音量命令 |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 减小音量 |
|
||||
*/ |
|
||||
fun decreaseVolume(): Boolean { |
|
||||
// 假设当前音量保存在设备中,我们只需发送减小命令 |
|
||||
return adjustVolume(0) // 发送最小音量命令 |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 清理资源 |
|
||||
*/ |
|
||||
fun cleanup() { |
|
||||
stopScan() |
|
||||
scanHandler.removeCallbacks(scanResultUpdateRunnable) |
|
||||
bluetoothGatt?.close() |
|
||||
bluetoothGatt = null |
|
||||
notifyChar = null |
|
||||
writeChar = null |
|
||||
} |
|
||||
} |
|
||||
@ -1,294 +0,0 @@ |
|||||
package com.yunqiinnovation.jieli |
|
||||
|
|
||||
import android.app.Activity |
|
||||
import android.bluetooth.BluetoothAdapter |
|
||||
import android.bluetooth.BluetoothManager |
|
||||
import android.content.Context |
|
||||
import android.content.Intent |
|
||||
import android.content.pm.PackageManager |
|
||||
import android.util.Log |
|
||||
import androidx.annotation.NonNull |
|
||||
import androidx.core.app.ActivityCompat |
|
||||
import androidx.core.content.ContextCompat |
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin |
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware |
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding |
|
||||
import io.flutter.plugin.common.EventChannel |
|
||||
import io.flutter.plugin.common.MethodCall |
|
||||
import io.flutter.plugin.common.MethodChannel |
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler |
|
||||
import io.flutter.plugin.common.MethodChannel.Result |
|
||||
import kotlinx.coroutines.* |
|
||||
|
|
||||
/** JieliPlugin */ |
|
||||
class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel.StreamHandler { |
|
||||
/// 方法通道 |
|
||||
private lateinit var methodChannel: MethodChannel |
|
||||
/// 统一事件通道 |
|
||||
private lateinit var eventChannel: EventChannel |
|
||||
|
|
||||
private lateinit var context: Context |
|
||||
private var activity: Activity? = null |
|
||||
|
|
||||
/// BLE服务实例 |
|
||||
private lateinit var bleService: BleService |
|
||||
|
|
||||
// 协程作用域 |
|
||||
private val pluginScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) |
|
||||
|
|
||||
// 事件接收器 |
|
||||
private var eventSink: EventChannel.EventSink? = null |
|
||||
|
|
||||
// 事件类型常量 |
|
||||
companion object { |
|
||||
private const val EVENT_TYPE_AUDIO_DATA = "audio_data" |
|
||||
private const val EVENT_TYPE_CONNECTION_STATE = "connection_state" |
|
||||
private const val EVENT_TYPE_SCAN_RESULT = "scan_result" |
|
||||
private const val EVENT_TYPE_WAKEUP_SIGNAL = "wakeup_signal" |
|
||||
private const val EVENT_TYPE_COMMAND_RESULT = "command_result" |
|
||||
} |
|
||||
|
|
||||
// 蓝牙相关请求码 |
|
||||
private val REQUEST_ENABLE_BT = 1 |
|
||||
private val REQUEST_PERMISSION_BLE = 2 |
|
||||
|
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { |
|
||||
val binaryMessenger = flutterPluginBinding.binaryMessenger |
|
||||
|
|
||||
// 设置方法通道 |
|
||||
methodChannel = MethodChannel(binaryMessenger, "com.yunqiinnovation.jieli") |
|
||||
methodChannel.setMethodCallHandler(this) |
|
||||
|
|
||||
// 设置统一事件通道 |
|
||||
eventChannel = EventChannel(binaryMessenger, "com.yunqiinnovation.jieli/events") |
|
||||
eventChannel.setStreamHandler(this) |
|
||||
|
|
||||
context = flutterPluginBinding.applicationContext |
|
||||
|
|
||||
// 初始化BLE服务 |
|
||||
bleService = BleService(context) |
|
||||
|
|
||||
// 设置回调 |
|
||||
setupBleCallbacks() |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 设置BLE服务回调 |
|
||||
*/ |
|
||||
private fun setupBleCallbacks() { |
|
||||
// 设置统一回调接口 |
|
||||
bleService.callback = object : BleService.Callback { |
|
||||
override fun onScanResult(devices: List<Map<String, Any>>) { |
|
||||
pluginScope.launch(Dispatchers.Main) { |
|
||||
sendEvent(EVENT_TYPE_SCAN_RESULT, devices) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onConnectionStateChanged(state: Int) { |
|
||||
pluginScope.launch(Dispatchers.Main) { |
|
||||
sendEvent(EVENT_TYPE_CONNECTION_STATE, state) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onAudioDataReceived(data: ByteArray) { |
|
||||
pluginScope.launch(Dispatchers.Main) { |
|
||||
// Log.d("","数据流onAudioDataReceived${data}") |
|
||||
sendEvent(EVENT_TYPE_AUDIO_DATA, data) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onWakeupSignalReceived() { |
|
||||
pluginScope.launch(Dispatchers.Main) { |
|
||||
sendEvent(EVENT_TYPE_WAKEUP_SIGNAL, true) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onCommandResult(commandType: Int, success: Boolean) { |
|
||||
pluginScope.launch(Dispatchers.Main) { |
|
||||
sendEvent(EVENT_TYPE_COMMAND_RESULT, mapOf( |
|
||||
"commandType" to commandType, |
|
||||
"success" to success |
|
||||
)) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 发送事件到Flutter端 |
|
||||
*/ |
|
||||
private fun sendEvent(eventType: String, data: Any?) { |
|
||||
eventSink?.success(mapOf( |
|
||||
"type" to eventType, |
|
||||
"data" to data |
|
||||
)) |
|
||||
} |
|
||||
|
|
||||
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { |
|
||||
when (call.method) { |
|
||||
"startScan" -> { |
|
||||
val scanTimeout = call.argument<Int>("scanTimeout") ?: 10000 |
|
||||
|
|
||||
if (!hasRequiredPermissions()) { |
|
||||
requestBlePermissions() |
|
||||
result.error("PERMISSION_DENIED", "需要蓝牙权限才能扫描设备", null) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
val success = bleService.startScan(scanTimeout.toLong()) |
|
||||
result.success(success) |
|
||||
} |
|
||||
"stopScan" -> { |
|
||||
val success = bleService.stopScan() |
|
||||
result.success(success) |
|
||||
} |
|
||||
"connect" -> { |
|
||||
val deviceId = call.argument<String>("deviceId") |
|
||||
if (deviceId == null) { |
|
||||
result.error("INVALID_ARGUMENT", "设备ID不能为空", null) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
if (!hasRequiredPermissions()) { |
|
||||
requestBlePermissions() |
|
||||
result.error("PERMISSION_DENIED", "需要蓝牙权限才能连接设备", null) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
bleService.connect(deviceId) |
|
||||
result.success(true) |
|
||||
} |
|
||||
"disconnect" -> { |
|
||||
bleService.disconnect() |
|
||||
result.success(true) |
|
||||
} |
|
||||
"sendStartRecordingCommand" -> { |
|
||||
val success = bleService.sendStartRecordingCommand() |
|
||||
result.success(success) |
|
||||
} |
|
||||
"sendStopRecordingCommand" -> { |
|
||||
val success = bleService.sendStopRecordingCommand() |
|
||||
result.success(success) |
|
||||
} |
|
||||
"isBluetoothEnabled" -> { |
|
||||
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager |
|
||||
val bluetoothAdapter = bluetoothManager.adapter |
|
||||
result.success(bluetoothAdapter?.isEnabled == true) |
|
||||
} |
|
||||
"requestBluetoothEnable" -> { |
|
||||
if (activity == null) { |
|
||||
result.error("ACTIVITY_NOT_AVAILABLE", "Activity不可用,无法请求打开蓝牙", null) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager |
|
||||
val bluetoothAdapter = bluetoothManager.adapter |
|
||||
|
|
||||
if (bluetoothAdapter == null) { |
|
||||
result.error("BLUETOOTH_UNAVAILABLE", "设备不支持蓝牙", null) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
if (bluetoothAdapter.isEnabled) { |
|
||||
result.success(true) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 请求打开蓝牙 |
|
||||
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) |
|
||||
activity?.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) |
|
||||
result.success(true) |
|
||||
} |
|
||||
"adjustVolume" -> { |
|
||||
val volume = call.argument<Int>("volume") ?: 0 |
|
||||
val success = bleService.adjustVolume(volume) |
|
||||
result.success(success) |
|
||||
} |
|
||||
"increaseVolume" -> { |
|
||||
val success = bleService.increaseVolume() |
|
||||
result.success(success) |
|
||||
} |
|
||||
"decreaseVolume" -> { |
|
||||
val success = bleService.decreaseVolume() |
|
||||
result.success(success) |
|
||||
} |
|
||||
else -> { |
|
||||
result.notImplemented() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { |
|
||||
methodChannel.setMethodCallHandler(null) |
|
||||
eventChannel.setStreamHandler(null) |
|
||||
|
|
||||
bleService.cleanup() |
|
||||
pluginScope.cancel() |
|
||||
} |
|
||||
|
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) { |
|
||||
activity = binding.activity |
|
||||
} |
|
||||
|
|
||||
override fun onDetachedFromActivityForConfigChanges() { |
|
||||
activity = null |
|
||||
} |
|
||||
|
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { |
|
||||
activity = binding.activity |
|
||||
} |
|
||||
|
|
||||
override fun onDetachedFromActivity() { |
|
||||
activity = null |
|
||||
} |
|
||||
|
|
||||
/// EventChannel.StreamHandler接口实现 |
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|
||||
eventSink = events |
|
||||
} |
|
||||
|
|
||||
override fun onCancel(arguments: Any?) { |
|
||||
eventSink = null |
|
||||
} |
|
||||
|
|
||||
/// 检查是否拥有所需权限 |
|
||||
private fun hasRequiredPermissions(): Boolean { |
|
||||
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { |
|
||||
hasPermission(android.Manifest.permission.BLUETOOTH_SCAN) && |
|
||||
hasPermission(android.Manifest.permission.BLUETOOTH_CONNECT) |
|
||||
} else { |
|
||||
hasPermission(android.Manifest.permission.BLUETOOTH) && |
|
||||
hasPermission(android.Manifest.permission.BLUETOOTH_ADMIN) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 检查单个权限 |
|
||||
private fun hasPermission(permission: String): Boolean { |
|
||||
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED |
|
||||
} |
|
||||
|
|
||||
/// 请求BLE权限 |
|
||||
private fun requestBlePermissions() { |
|
||||
activity?.let { |
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { |
|
||||
ActivityCompat.requestPermissions( |
|
||||
it, |
|
||||
arrayOf( |
|
||||
android.Manifest.permission.BLUETOOTH_SCAN, |
|
||||
android.Manifest.permission.BLUETOOTH_CONNECT |
|
||||
), |
|
||||
REQUEST_PERMISSION_BLE |
|
||||
) |
|
||||
} else { |
|
||||
ActivityCompat.requestPermissions( |
|
||||
it, |
|
||||
arrayOf( |
|
||||
android.Manifest.permission.BLUETOOTH, |
|
||||
android.Manifest.permission.BLUETOOTH_ADMIN |
|
||||
), |
|
||||
REQUEST_PERMISSION_BLE |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,283 +0,0 @@ |
|||||
import 'dart:async'; |
|
||||
import 'dart:typed_data'; |
|
||||
|
|
||||
import 'package:flutter/services.dart'; |
|
||||
|
|
||||
/// 设备类,代表一个BLE设备 |
|
||||
class BleDevice { |
|
||||
final String id; |
|
||||
final String name; |
|
||||
final int rssi; |
|
||||
|
|
||||
BleDevice({required this.id, required this.name, required this.rssi}); |
|
||||
|
|
||||
factory BleDevice.fromJson(Map<String, dynamic> json) { |
|
||||
return BleDevice( |
|
||||
id: json['id'] as String, |
|
||||
name: json['name'] as String? ?? 'Unknown Device', |
|
||||
rssi: json['rssi'] as int? ?? 0, |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
Map<String, dynamic> toJson() { |
|
||||
return { |
|
||||
'id': id, |
|
||||
'name': name, |
|
||||
'rssi': rssi, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
@override |
|
||||
String toString() => 'BleDevice(id: $id, name: $name, rssi: $rssi)'; |
|
||||
} |
|
||||
|
|
||||
/// 连接状态 |
|
||||
enum ConnectionState { |
|
||||
disconnected, |
|
||||
connecting, |
|
||||
connected, |
|
||||
disconnecting, |
|
||||
error |
|
||||
} |
|
||||
|
|
||||
/// 事件类型 |
|
||||
class EventType { |
|
||||
static const String audioData = 'audio_data'; |
|
||||
static const String connectionState = 'connection_state'; |
|
||||
static const String scanResult = 'scan_result'; |
|
||||
static const String wakeupSignal = 'wakeup_signal'; |
|
||||
static const String commandResult = 'command_result'; |
|
||||
} |
|
||||
|
|
||||
/// 杰理BLE插件主类 |
|
||||
class Jieli { |
|
||||
static const MethodChannel _methodChannel = |
|
||||
MethodChannel('com.yunqiinnovation.jieli'); |
|
||||
static const EventChannel _eventChannel = |
|
||||
EventChannel('com.yunqiinnovation.jieli/events'); |
|
||||
|
|
||||
/// 扫描结果流 |
|
||||
static final StreamController<List<BleDevice>> _scanController = |
|
||||
StreamController<List<BleDevice>>.broadcast(); |
|
||||
Stream<List<BleDevice>> get scanResults => _scanController.stream; |
|
||||
|
|
||||
/// 连接状态变化流 |
|
||||
static final StreamController<ConnectionState> _connectionController = |
|
||||
StreamController<ConnectionState>.broadcast(); |
|
||||
Stream<ConnectionState> get connectionState => _connectionController.stream; |
|
||||
|
|
||||
/// 音频数据流 |
|
||||
static final StreamController<Uint8List> _audioController = |
|
||||
StreamController<Uint8List>.broadcast(); |
|
||||
Stream<Uint8List> get audioDataStream => _audioController.stream; |
|
||||
|
|
||||
/// 唤醒信号流 |
|
||||
static final StreamController<bool> _wakeupController = |
|
||||
StreamController<bool>.broadcast(); |
|
||||
Stream<bool> get wakeupStream => _wakeupController.stream; |
|
||||
|
|
||||
/// 命令结果流 |
|
||||
static final StreamController<CommandResult> _commandResultController = |
|
||||
StreamController<CommandResult>.broadcast(); |
|
||||
Stream<CommandResult> get commandResultStream => |
|
||||
_commandResultController.stream; |
|
||||
|
|
||||
/// 原始事件流 |
|
||||
Stream<Map<String, dynamic>>? _eventStream; |
|
||||
|
|
||||
/// 事件流订阅 |
|
||||
StreamSubscription? _eventSubscription; |
|
||||
|
|
||||
/// 命令类型常量 |
|
||||
static const int commandTypeVolume = 1; |
|
||||
|
|
||||
/// 构造函数,初始化平台通道和事件监听 |
|
||||
Jieli() { |
|
||||
_setupEventListener(); |
|
||||
} |
|
||||
|
|
||||
/// 设置事件监听器 |
|
||||
void _setupEventListener() { |
|
||||
_eventSubscription = |
|
||||
_eventChannel.receiveBroadcastStream().listen(_handleEvent); |
|
||||
} |
|
||||
|
|
||||
/// 处理来自原生平台的事件 |
|
||||
void _handleEvent(dynamic event) { |
|
||||
if (event is! Map<dynamic, dynamic>) return; |
|
||||
|
|
||||
final eventData = Map<String, dynamic>.from(event); |
|
||||
final String eventType = eventData['type'] as String; |
|
||||
final dynamic data = eventData['data']; |
|
||||
|
|
||||
switch (eventType) { |
|
||||
case EventType.scanResult: |
|
||||
if (data is List<dynamic>) { |
|
||||
final deviceList = data |
|
||||
.map( |
|
||||
(item) => BleDevice.fromJson(Map<String, dynamic>.from(item))) |
|
||||
.toList(); |
|
||||
_scanController.add(deviceList); |
|
||||
} |
|
||||
break; |
|
||||
case EventType.connectionState: |
|
||||
if (data is int && data >= 0 && data < ConnectionState.values.length) { |
|
||||
_connectionController.add(ConnectionState.values[data]); |
|
||||
} |
|
||||
break; |
|
||||
case EventType.audioData: |
|
||||
if (data is Uint8List) { |
|
||||
//print('收到ble通道消息: $data'); |
|
||||
_audioController.add(data); |
|
||||
} |
|
||||
break; |
|
||||
case EventType.wakeupSignal: |
|
||||
if (data is bool) { |
|
||||
_wakeupController.add(data); |
|
||||
} |
|
||||
break; |
|
||||
case EventType.commandResult: |
|
||||
if (data is Map<dynamic, dynamic>) { |
|
||||
final commandType = data['commandType'] as int; |
|
||||
final success = data['success'] as bool; |
|
||||
_commandResultController.add(CommandResult( |
|
||||
commandType: commandType, |
|
||||
success: success, |
|
||||
)); |
|
||||
} |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 开始扫描BLE设备 |
|
||||
Future<bool> startScan({int scanTimeout = 10000}) async { |
|
||||
final result = await _methodChannel.invokeMethod<bool>( |
|
||||
'startScan', |
|
||||
{'scanTimeout': scanTimeout}, |
|
||||
); |
|
||||
return result ?? false; |
|
||||
} |
|
||||
|
|
||||
/// 停止扫描BLE设备 |
|
||||
Future<bool> stopScan() async { |
|
||||
final result = await _methodChannel.invokeMethod<bool>('stopScan'); |
|
||||
return result ?? false; |
|
||||
} |
|
||||
|
|
||||
/// 连接到指定设备 |
|
||||
Future<bool> connect(String deviceId) async { |
|
||||
final result = await _methodChannel.invokeMethod<bool>( |
|
||||
'connect', |
|
||||
{'deviceId': deviceId}, |
|
||||
); |
|
||||
return result ?? false; |
|
||||
} |
|
||||
|
|
||||
/// 断开连接 |
|
||||
Future<bool> disconnect() async { |
|
||||
final result = await _methodChannel.invokeMethod<bool>('disconnect'); |
|
||||
return result ?? false; |
|
||||
} |
|
||||
|
|
||||
/// 发送开始录音命令 |
|
||||
Future<bool> sendStartRecordingCommand() async { |
|
||||
final result = |
|
||||
await _methodChannel.invokeMethod<bool>('sendStartRecordingCommand'); |
|
||||
return result ?? false; |
|
||||
} |
|
||||
|
|
||||
/// 发送停止录音命令 |
|
||||
Future<bool> sendStopRecordingCommand() async { |
|
||||
final result = |
|
||||
await _methodChannel.invokeMethod<bool>('sendStopRecordingCommand'); |
|
||||
return result ?? false; |
|
||||
} |
|
||||
|
|
||||
/// 关闭插件,释放资源 |
|
||||
Future<void> dispose() async { |
|
||||
await stopScan(); |
|
||||
// await disconnect(); |
|
||||
// _eventSubscription?.cancel(); |
|
||||
// _scanController.close(); |
|
||||
// _connectionController.close(); |
|
||||
// _audioController.close(); |
|
||||
// _wakeupController.close(); |
|
||||
// _commandResultController.close(); |
|
||||
} |
|
||||
|
|
||||
/// 检查蓝牙是否已启用 |
|
||||
Future<bool> isBluetoothEnabled() async { |
|
||||
final result = |
|
||||
await _methodChannel.invokeMethod<bool>('isBluetoothEnabled'); |
|
||||
return result ?? false; |
|
||||
} |
|
||||
|
|
||||
/// 请求打开蓝牙 |
|
||||
Future<bool> requestBluetoothEnable() async { |
|
||||
final result = |
|
||||
await _methodChannel.invokeMethod<bool>('requestBluetoothEnable'); |
|
||||
return result ?? false; |
|
||||
} |
|
||||
|
|
||||
/// 调整音量 |
|
||||
/// [volume] 音量值,范围0-16 |
|
||||
Future<bool> adjustVolume(int volume) async { |
|
||||
try { |
|
||||
final result = await _methodChannel.invokeMethod<bool>( |
|
||||
'adjustVolume', |
|
||||
{'volume': volume}, |
|
||||
); |
|
||||
return result ?? false; |
|
||||
} catch (e) { |
|
||||
print('调整音量失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 增加音量 |
|
||||
Future<bool> increaseVolume() async { |
|
||||
try { |
|
||||
final result = await _methodChannel.invokeMethod<bool>('increaseVolume'); |
|
||||
return result ?? false; |
|
||||
} catch (e) { |
|
||||
print('增加音量失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 减小音量 |
|
||||
Future<bool> decreaseVolume() async { |
|
||||
try { |
|
||||
final result = await _methodChannel.invokeMethod<bool>('decreaseVolume'); |
|
||||
return result ?? false; |
|
||||
} catch (e) { |
|
||||
print('减小音量失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 命令结果回调 |
|
||||
Stream<CommandResult> get onCommandResult => commandResultStream |
|
||||
.where((event) => event.commandType == commandTypeVolume) |
|
||||
.map((event) => event); |
|
||||
} |
|
||||
|
|
||||
/// 蓝牙设备模型 |
|
||||
class BluetoothDevice { |
|
||||
// ... existing code ... |
|
||||
} |
|
||||
|
|
||||
/// 命令结果 |
|
||||
class CommandResult { |
|
||||
final int commandType; |
|
||||
final bool success; |
|
||||
|
|
||||
CommandResult({ |
|
||||
required this.commandType, |
|
||||
required this.success, |
|
||||
}); |
|
||||
|
|
||||
@override |
|
||||
String toString() => |
|
||||
'CommandResult{commandType: $commandType, success: $success}'; |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
name: jieli |
|
||||
description: A Flutter plugin for connecting with Jieli BLE earphones |
|
||||
version: 0.0.1 |
|
||||
homepage: |
|
||||
|
|
||||
environment: |
|
||||
sdk: ">=2.12.0 <3.0.0" |
|
||||
flutter: ">=2.0.0" |
|
||||
|
|
||||
dependencies: |
|
||||
flutter: |
|
||||
sdk: flutter |
|
||||
|
|
||||
dev_dependencies: |
|
||||
flutter_test: |
|
||||
sdk: flutter |
|
||||
|
|
||||
flutter: |
|
||||
plugin: |
|
||||
platforms: |
|
||||
android: |
|
||||
package: com.yunqiinnovation.jieli |
|
||||
pluginClass: JieliPlugin |
|
||||
Loading…
Reference in new issue