Browse Source

add

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
8d1e9c567d
  1. 10
      .mcp copy.json
  2. 11
      lib/modules/jieli_test/bindings/jieli_test_binding.dart
  3. 323
      lib/modules/jieli_test/controllers/jieli_test_controller.dart
  4. 568
      lib/modules/jieli_test/views/jieli_test_view.dart
  5. 3
      lib/modules/splash/controllers/splash_controller.dart
  6. 9
      lib/routes/app_pages.dart
  7. 1
      local_plugins/ble_service/android/build.gradle.kts
  8. 144
      local_plugins/jieli/README.md
  9. 57
      local_plugins/jieli/android/build.gradle.kts
  10. 1
      local_plugins/jieli/android/settings.gradle.kts
  11. 22
      local_plugins/jieli/android/src/main/AndroidManifest.xml
  12. 665
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt
  13. 294
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt
  14. 283
      local_plugins/jieli/lib/jieli.dart
  15. 23
      local_plugins/jieli/pubspec.yaml
  16. 4
      pubspec.yaml
  17. 19
      test.json

10
.mcp copy.json

@ -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"
}
}
}

11
lib/modules/jieli_test/bindings/jieli_test_binding.dart

@ -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(),
);
}
}

323
lib/modules/jieli_test/controllers/jieli_test_controller.dart

@ -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) {
// 释放资源错误处理
}
}
}

568
lib/modules/jieli_test/views/jieli_test_view.dart

@ -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;
}
}
}

3
lib/modules/splash/controllers/splash_controller.dart

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:amap_flutter/amap_flutter.dart';
import 'package:deep_voice/data/models/appconfig.dart'; import 'package:deep_voice/data/models/appconfig.dart';
import 'package:deep_voice/data/models/appconfig_model.dart'; import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,7 +8,7 @@ import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../core/themes/theme_service.dart'; import '../../../core/themes/theme_service.dart';
import '../../../data/services/network/api.dart'; import '../../../data/services/network/api.dart';
import 'package:amap_flutter/amap_flutter.dart';
class SplashController extends GetxController { class SplashController extends GetxController {
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
// 进度条动画控制 // 进度条动画控制

9
lib/routes/app_pages.dart

@ -29,8 +29,6 @@ import '../modules/pairing/bindings/pairing_binding.dart';
import './app_routes.dart'; import './app_routes.dart';
import '../modules/settings/views/settings_view.dart'; import '../modules/settings/views/settings_view.dart';
import '../modules/settings/bindings/settings_binding.dart'; import '../modules/settings/bindings/settings_binding.dart';
import '../modules/jieli_test/views/jieli_test_view.dart';
import '../modules/jieli_test/bindings/jieli_test_binding.dart';
import '../modules/opus_test/views/opus_test_view.dart'; import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/opus_test/bindings/opus_test_binding.dart'; import '../modules/opus_test/bindings/opus_test_binding.dart';
import '../modules/speech_test/views/speech_test_view.dart'; import '../modules/speech_test/views/speech_test_view.dart';
@ -162,13 +160,6 @@ abstract class AppPages {
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), transitionDuration: Duration(milliseconds: 250),
), ),
GetPage(
name: Routes.jieliTest,
page: () => const JieliTestView(),
binding: JieliTestBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage( GetPage(
name: Routes.opusTest, name: Routes.opusTest,
page: () => const OpusTestView(), page: () => const OpusTestView(),

1
local_plugins/ble_service/android/build.gradle.kts

@ -31,5 +31,4 @@ dependencies {
// 使用从本地Maven仓库引用的AAR依赖 // 使用从本地Maven仓库引用的AAR依赖
implementation("com.jieli.audio:jl_audio_decode:2.0.0") implementation("com.jieli.audio:jl_audio_decode:2.0.0")
implementation("com.jieli.jl_bt_ota:jl_bt_ota:1.9.2") implementation("com.jieli.jl_bt_ota:jl_bt_ota:1.9.2")
implementation(project(":deepvoice_utils"))
} }

144
local_plugins/jieli/README.md

@ -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" />
```

57
local_plugins/jieli/android/build.gradle.kts

@ -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
local_plugins/jieli/android/settings.gradle.kts

@ -1 +0,0 @@
rootProject.name = "jieli"

22
local_plugins/jieli/android/src/main/AndroidManifest.xml

@ -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>

665
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt

@ -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
}
}

294
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt

@ -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
)
}
}
}
}

283
local_plugins/jieli/lib/jieli.dart

@ -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}';
}

23
local_plugins/jieli/pubspec.yaml

@ -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

4
pubspec.yaml

@ -69,8 +69,6 @@ dependencies:
path: local_plugins/azure_speech path: local_plugins/azure_speech
open_ai_service: open_ai_service:
path: local_plugins/open_ai_service path: local_plugins/open_ai_service
jieli:
path: local_plugins/jieli
chat_storage: chat_storage:
path: local_plugins/chat_storage path: local_plugins/chat_storage
spotify_sdk: spotify_sdk:
@ -91,8 +89,6 @@ dependencies:
path: local_plugins/agent_service path: local_plugins/agent_service
music_service: music_service:
path: local_plugins/music_service path: local_plugins/music_service
# amap_service:
# path: local_plugins/amap_service
dev_dependencies: dev_dependencies:

19
test.json

@ -15,4 +15,21 @@ adb shell am broadcast \
adb shell dumpsys package com.yunqiinnovation.deepsound | grep receiver adb shell dumpsys package com.yunqiinnovation.deepsound | grep receiver
adb logcat -v time | grep -i BleForegroundService adb logcat -v time | grep -i BleForegroundService
mvn install:install-file \
-Dfile=
-DgroupId=com.jieli.audio \
-DartifactId=jl_audio_decode \
-Dversion=2.0.0 \
-Dpackaging=aar
mvn install:install-file \
-Dfile=/Users/wolfplus/Developer/Doc/Jieli_OTA_SDK_Android_1.9.2/libs/jl_bt_ota_V1.9.2-release.aar \
-DgroupId=com.jieli.jl_bt_ota \
-DartifactId=jl_bt_ota \
-Dversion=1.9.2 \
-Dpackaging=aar
Loading…
Cancel
Save