lxm 1 year ago
parent
commit
7dbb6fa76c
  1. 1
      android/app/build.gradle.kts
  2. 4
      android/settings.gradle.kts
  3. 4
      ios/Runner/Info.plist
  4. 29
      lib/data/services/ble_manager.dart
  5. 239
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  6. 285
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  7. 39
      lib/modules/settings/controllers/settings_controller.dart
  8. 24
      lib/modules/settings/views/settings_view.dart
  9. 2
      local_plugins/agent_service/android/build.gradle.kts
  10. 2
      local_plugins/agent_service/android/src/main/AndroidManifest.xml
  11. 133
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  12. 329
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  13. 48
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  14. 116
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt
  15. 13
      local_plugins/chat_api/README.md
  16. 70
      local_plugins/chat_api/android/README.md
  17. 50
      local_plugins/chat_api/android/build.gradle.kts
  18. 1
      local_plugins/chat_api/android/settings.gradle.kts
  19. 8
      local_plugins/chat_api/android/src/main/AndroidManifest.xml
  20. 252
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt
  21. 799
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  22. 42
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
  23. 3
      local_plugins/chat_api/pubspec.yaml
  24. 33
      local_plugins/jl_ota/.gitignore
  25. 33
      local_plugins/jl_ota/.metadata
  26. 3
      local_plugins/jl_ota/CHANGELOG.md
  27. 1
      local_plugins/jl_ota/LICENSE
  28. 15
      local_plugins/jl_ota/README.md
  29. 4
      local_plugins/jl_ota/analysis_options.yaml
  30. 9
      local_plugins/jl_ota/android/.gitignore
  31. 68
      local_plugins/jl_ota/android/build.gradle
  32. BIN
      local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.2-release.aar
  33. 1
      local_plugins/jl_ota/android/settings.gradle
  34. 3
      local_plugins/jl_ota/android/src/main/AndroidManifest.xml
  35. 76
      local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt
  36. 38
      local_plugins/jl_ota/ios/.gitignore
  37. 0
      local_plugins/jl_ota/ios/Assets/.gitkeep
  38. 19
      local_plugins/jl_ota/ios/Classes/JlOtaPlugin.swift
  39. 14
      local_plugins/jl_ota/ios/Resources/PrivacyInfo.xcprivacy
  40. 29
      local_plugins/jl_ota/ios/jl_ota.podspec
  41. 8
      local_plugins/jl_ota/lib/jl_ota.dart
  42. 17
      local_plugins/jl_ota/lib/jl_ota_method_channel.dart
  43. 29
      local_plugins/jl_ota/lib/jl_ota_platform_interface.dart
  44. 72
      local_plugins/jl_ota/pubspec.yaml
  45. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  46. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  47. 26
      local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt
  48. 376
      local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt
  49. 43
      local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt
  50. 11
      local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java
  51. 139
      local_plugins/ota/lib/ota.dart
  52. 20
      local_plugins/ota/lib/ota_method_channel.dart
  53. 12
      local_plugins/ota/lib/ota_platform_interface.dart
  54. 1
      local_plugins/ota/pubspec.yaml
  55. 2
      pubspec.yaml

1
android/app/build.gradle.kts

@ -5,7 +5,6 @@ repositories {
mavenCentral() mavenCentral()
flatDir { flatDir {
dirs(project(":spotify_sdk").projectDir.resolve("libs")) dirs(project(":spotify_sdk").projectDir.resolve("libs"))
dirs(project(":jl_ota").projectDir.resolve("libs"))
} }
} }

4
android/settings.gradle.kts

@ -39,7 +39,7 @@ include(":ble_service")
include(":classic_bluetooth") include(":classic_bluetooth")
include(":agent_service") include(":agent_service")
include(":speech") include(":speech")
include(":open_ai_service") include(":chat_api")
// 设置azure_speech项目的路径 // 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
@ -50,4 +50,4 @@ project(":ble_service").projectDir = file("../local_plugins/ble_service/android"
project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android") project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android")
project(":agent_service").projectDir = file("../local_plugins/agent_service/android") project(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":speech").projectDir = file("../local_plugins/speech/android") project(":speech").projectDir = file("../local_plugins/speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android") project(":chat_api").projectDir = file("../local_plugins/chat_api/android")

4
ios/Runner/Info.plist

@ -123,9 +123,9 @@
<key>NSBluetoothPeripheralUsageDescription</key> <key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string> <string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string> <string>语音助手需要位置权限来为您提供基于位置的信息和服务</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string> <string>语音助手需要位置权限来为您提供基于位置的信息和服务</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string> <string>需要麦克风权限用于语音识别和录音功能</string>
<key>NSCalendarsUsageDescription</key> <key>NSCalendarsUsageDescription</key>

29
lib/data/services/ble_manager.dart

@ -179,6 +179,19 @@ class BleManager extends GetxService {
state == BleConnectionState.error) { state == BleConnectionState.error) {
Logger.info('BLE设备已断开或发生错误'); Logger.info('BLE设备已断开或发生错误');
_isConnected.value = false; _isConnected.value = false;
// 更新设备信息
_updateDeviceInfoIfNeeded(
batteryLevel: 0,
leftEarBattery: 0,
rightEarBattery: 0,
caseBattery: 0,
leftEarCharging: false,
rightEarCharging: false,
caseCharging: false,
firmwareVersion: '',
firmwareProductId: '',
firmwareProductColor: '',
);
} }
} }
@ -418,7 +431,8 @@ class BleManager extends GetxService {
} else { } else {
Logger.warning('电池信息获取指令发送失败'); Logger.warning('电池信息获取指令发送失败');
} }
// 添加延迟
await Future.delayed(const Duration(seconds: 1));
// 获取设备版本信息 // 获取设备版本信息
final versionResult = await _bleService.getDeviceVersion(); final versionResult = await _bleService.getDeviceVersion();
if (versionResult) { if (versionResult) {
@ -426,7 +440,8 @@ class BleManager extends GetxService {
} else { } else {
Logger.warning('版本信息获取指令发送失败'); Logger.warning('版本信息获取指令发送失败');
} }
// 添加延迟
await Future.delayed(const Duration(seconds: 1));
// 获取产品id // 获取产品id
final productIdResult = await _bleService.getProductId(); final productIdResult = await _bleService.getProductId();
if (productIdResult) { if (productIdResult) {
@ -732,6 +747,16 @@ class BleManager extends GetxService {
} }
} }
/// 清除BLE配对设备
Future<bool> clearPairedDevices() async {
try {
return await _bleService.clearAssociations();
} catch (e) {
Logger.error('清除BLE配对设备失败: ${e.toString()}');
return false;
}
}
/// 注册服务 /// 注册服务
static Future<BleManager> init() async { static Future<BleManager> init() async {
if (!Get.isRegistered<BleManager>()) { if (!Get.isRegistered<BleManager>()) {

239
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -7,82 +7,148 @@ import '../model/ota_upgrade_model.dart';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:jl_ota/jl_ota.dart';
import 'package:ota/ota.dart'; import 'package:ota/ota.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
class OtaUpgradeController extends GetxController { class OtaUpgradeController extends GetxController {
static const int NOT_STARTED = -1; // 未开始
static const int VALIDATION_FILE = 0; // 验证文件
static const int ONUPGRADE = 1; // 升级中
static const int START_UPGRADING = 2; // 开始升级
static const int UPGRADE_SUCCESS = 3; // 升级成功
static const int UPGRADE_FAILED = 4; // 升级失败
static const int CONNECTION_DISCONNECTION = 5; // 连接断开
static const int CONNECTION_SUCCESS = 6; // 连接成功
static const int DOWNLOADING_UP_FILES = 7; // 下载升级文件
// 错误信息,用于存储错误提示
final RxString errorMessage = ''.obs; final RxString errorMessage = ''.obs;
final RxBool isFetchingDeviceInfo = false.obs;
final Dio _dio = Dio(); // 最新版本号
final RxString latestVersion = ''.obs; final RxString latestVersion = ''.obs;
// 当前设备固件版本号
final RxString currentVersion = ''.obs; final RxString currentVersion = ''.obs;
// 更新包地址,用于下载更新文件
String updatepackageaddress = ''; String updatepackageaddress = '';
// 下载进度,范围为 0.0 到 1.0
final RxDouble downloadProgress = 0.0.obs; final RxDouble downloadProgress = 0.0.obs;
final RxBool isUpdating = false.obs;
// 是否有可用更新的状态
final isUpdateAvailable = true.obs; final isUpdateAvailable = true.obs;
final RxDouble progress = 0.0.obs; // 当前升级进度,范围为 0.0 到 1.0
var jlota = Ota(); // final RxDouble progress = 0.0.obs;
// 蓝牙服务
// OTA 单例实例,用于处理升级相关逻辑
final jlota = Ota.instance;
// 是否正在下载更新包的状态
final RxBool isDownloading = false.obs;
// 蓝牙管理服务,用于获取设备信息和通信
final _bleManager = Get.find<BleManager>(); final _bleManager = Get.find<BleManager>();
final maxRetryAttempts = 3; // 最大重试次数
int currentRetryCount = 0;
final RxBool isFetchingDeviceInfo = true.obs;
final RxDouble progress = 0.0.obs; // 控制器自己的可观察属性
final RxInt type = OtaEvent.NOT_STARTED.obs;
final RxBool isUpgrade = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_getCurrentVersion(); _getCurrentVersion();
} }
@override
void onClose() {
jlota.onOtaClose();
isUpgrade.value = false;
super.onClose();
}
// 添加重试方法 // 添加重试方法
void retryConnection() { void retryConnection() {
errorMessage.value = ''; errorMessage.value = '';
_getCurrentVersion(); _getCurrentVersion();
} }
String getStatusMessage(int type) {
switch (type) {
case NOT_STARTED: // -1
return "未开始".tr;
case VALIDATION_FILE: // 0
return "验证文件中".tr;
case ONUPGRADE: // 1
return "升级中".tr;
case START_UPGRADING: // 2
return "开始升级".tr;
case UPGRADE_SUCCESS: // 3
return "升级成功".tr;
case UPGRADE_FAILED: // 4
return "升级失败".tr;
case CONNECTION_DISCONNECTION: // 5
return "连接断开".tr;
case CONNECTION_SUCCESS: // 6
return "连接成功".tr;
case DOWNLOADING_UP_FILES: // 7 (新增)
return "下载升级文件".tr;
default:
return "未知状态".tr; // 更安全的默认值
}
}
// 添加更新方法 // 添加更新方法
Future<void> startUpdate() async { Future<void> startUpdate() async {
try { try {
isUpdating.value = true;
checkUpdate(); checkUpdate();
// 实现更新逻辑... // 实现更新逻辑...
} catch (e) { } catch (e) {
errorMessage.value = e.toString(); errorMessage.value = e.toString();
Get.snackbar('更新失败', e.toString()); Get.snackbar('更新失败', e.toString());
} finally {
isUpdating.value = false;
} }
} }
Future<void> _getCurrentVersion() async { Future<void> _getCurrentVersion() async {
_listenToEvents();
try { try {
jlota.getPlatformVersion(); isFetchingDeviceInfo.value = true;
errorMessage.value = '';
// 添加获取设备信息的重试机制 // 添加获取设备信息的重试机制
DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; DeviceInfo? deviceInfo = _bleManager.deviceInfo.value;
// 第一次检查设备信息 // // 第一次检查设备信息
if (deviceInfo == null) { // if (deviceInfo == null) {
// 显示加载提示 // // 尝试重新获取设备信息
isFetchingDeviceInfo.value = true; // await _bleManager.fetchDeviceInfo();
// }
// 尝试重新获取设备信息 while (currentRetryCount < maxRetryAttempts) {
await _bleManager.fetchDeviceInfo();
// 等待后再次获取设备信息
deviceInfo = _bleManager.deviceInfo.value; deviceInfo = _bleManager.deviceInfo.value;
isFetchingDeviceInfo.value = false;
// 仍然为空则抛出异常 // 检查信息有效性
if (deviceInfo == null) { if (deviceInfo != null &&
throw const FormatException('无法获取设备信息,请检查设备连接'); deviceInfo.firmwareVersion.isNotEmpty &&
deviceInfo.firmwareProductId.isNotEmpty) {
break;
} }
}
// 检查固件信息有效性 // 获取设备信息
if (deviceInfo.firmwareVersion.isEmpty || await _bleManager.fetchDeviceInfo();
await Future.delayed(const Duration(seconds: 5));
currentRetryCount++;
}
// 最终验证
if (deviceInfo == null ||
deviceInfo.firmwareVersion.isEmpty ||
deviceInfo.firmwareProductId.isEmpty) { deviceInfo.firmwareProductId.isEmpty) {
throw const FormatException('设备固件信息不完整'); throw "获取设备信息失败,请检查连接";
} }
// 正常流程 // 正常流程
currentVersion.value = deviceInfo.firmwareVersion; currentVersion.value = deviceInfo.firmwareVersion;
print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
@ -98,7 +164,8 @@ class OtaUpgradeController extends GetxController {
final resp = OtaConfigResp.fromJson(response); final resp = OtaConfigResp.fromJson(response);
print('getequipmentinfo,resp.systemversion: ${resp.systemversion}'); print('getequipmentinfo,resp.systemversion: ${resp.systemversion}');
latestVersion.value = "1"; latestVersion.value = resp.systemversion!;
updatepackageaddress = resp.updatepackageaddress!; updatepackageaddress = resp.updatepackageaddress!;
// 版本检查 // 版本检查
@ -141,9 +208,9 @@ class OtaUpgradeController extends GetxController {
// 版本更新 // 版本更新
Future<void> checkUpdate() async { Future<void> checkUpdate() async {
try { try {
if (latestVersion.value != "") if (latestVersion.value != "") {
_showUpdateDialog(latestVersion.value); _showUpdateDialog(latestVersion.value);
else { } else {
Get.snackbar('错误', '获取最新版本信息失败'); Get.snackbar('错误', '获取最新版本信息失败');
} }
} catch (e) { } catch (e) {
@ -151,20 +218,28 @@ class OtaUpgradeController extends GetxController {
} }
} }
// 显示更新对话框 void _showUpdateDialog(String latest) async {
void _showUpdateDialog(String latest) { print('显示更新对话框');
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
title: Text('发现新版本 $latest'), title: Text('发现新版本 $latest'),
content: Text('是否立即更新?'), content: Text('是否立即更新?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Get.back(), onPressed: () async {
print('取消按钮被点击');
Get.back();
Future.delayed(Duration(milliseconds: 300));
},
child: Text('取消'), child: Text('取消'),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
print('更新按钮被点击');
Get.back(); Get.back();
Future.delayed(Duration(milliseconds: 300));
startDownload(); startDownload();
}, },
child: Text('更新'), child: Text('更新'),
@ -176,7 +251,7 @@ class OtaUpgradeController extends GetxController {
// 修改后的startDownload方法 // 修改后的startDownload方法
Future<void> startDownload() async { Future<void> startDownload() async {
isUpdating.value = true; isDownloading.value = true;
int lastIndex = updatepackageaddress.lastIndexOf('/'); int lastIndex = updatepackageaddress.lastIndexOf('/');
String fileName = updatepackageaddress.substring(lastIndex + 1); String fileName = updatepackageaddress.substring(lastIndex + 1);
String directoryName = "ota_upgrades"; String directoryName = "ota_upgrades";
@ -188,8 +263,7 @@ class OtaUpgradeController extends GetxController {
headers: {'Accept-Encoding': 'gzip'}, headers: {'Accept-Encoding': 'gzip'},
)); ));
Get.snackbar('升级命令已发送', '开始下载...'); type.value = DOWNLOADING_UP_FILES;
try { try {
final Directory? downloadsDir = await getDownloadsDirectory(); final Directory? downloadsDir = await getDownloadsDirectory();
if (downloadsDir == null) { if (downloadsDir == null) {
@ -218,14 +292,14 @@ class OtaUpgradeController extends GetxController {
} }
} catch (e) { } catch (e) {
Get.snackbar('错误', '检查本地文件失败: $e'); Get.snackbar('错误', '检查本地文件失败: $e');
isUpdating.value = false;
return; return;
} }
if (existingSameFile) { if (existingSameFile) {
Get.snackbar('文件已存在', '跳过下载,直接更新'); Get.snackbar('文件已存在', '跳过下载,直接更新');
isUpdating.value = false;
_installApp(savePath); startUpgrading(savePath);
return; return;
} }
@ -239,7 +313,7 @@ class OtaUpgradeController extends GetxController {
} }
} catch (e) { } catch (e) {
Get.snackbar('错误', '删除旧文件失败: $e'); Get.snackbar('错误', '删除旧文件失败: $e');
isUpdating.value = false; //isUpgrade.value = false;
return; return;
} }
@ -250,59 +324,80 @@ class OtaUpgradeController extends GetxController {
followRedirects: true, followRedirects: true,
maxRedirects: 5, maxRedirects: 5,
); );
// 修改下载部分
DateTime lastUpdate = DateTime.now();
await dio.download( await dio.download(
updatepackageaddress, updatepackageaddress,
savePath, savePath,
options: options, options: options,
onReceiveProgress: (received, total) { onReceiveProgress: (received, total) {
final now = DateTime.now(); if (total != -1) {
if (now.difference(lastUpdate).inMilliseconds > 1000) { downloadProgress.value = received / total;
lastUpdate = now;
String progressMsg = total != -1
? '${(received / total * 100).toStringAsFixed(1)}%'
: '已下载:${(received / 1024 / 1024).toStringAsFixed(1)}MB';
Get.snackbar(
'升级进度',
progressMsg,
duration: const Duration(seconds: 1),
);
} }
}, },
); );
File file = File(savePath); File file = File(savePath);
if (await file.exists()) { if (await file.exists()) {
Get.snackbar('下载完成', '文件已保存至: $savePath'); Get.snackbar('下载完成', '文件已保存至: $savePath');
} else { } else {
Get.snackbar('错误', '文件未找到,请检查写入权限'); Get.snackbar('错误', '文件未找到,请检查写入权限');
} }
isUpdating.value = false; isDownloading.value = false;
_installApp(savePath); startUpgrading(savePath);
return;
} on DioException catch (e) { } on DioException catch (e) {
Get.snackbar('下载失败', '错误: ${e.message}'); Get.snackbar('下载失败', '错误: ${e.message}');
isUpdating.value = false;
} on FileSystemException catch (e) { } on FileSystemException catch (e) {
Get.snackbar('存储失败', '无法写入文件: ${e.message}'); Get.snackbar('存储失败', '无法写入文件: ${e.message}');
isUpdating.value = false;
} catch (e) { } catch (e) {
Get.snackbar('未知错误', e.toString()); Get.snackbar('未知错误', e.toString());
isUpdating.value = false;
} }
} }
// 开始升级 // 开始升级
void _installApp(String path) async { void startUpgrading(String path) async {
jlota.searchDevices(); jlota.startUpgrading(path);
jlota.startUpgrade(); return;
// JlOtaPlugin() // isUpgrade.value = false;
// final File file = File(path); }
// if (await file.exists()) {
// // OpenResult result = await OpenFile.open(path); // 停止升级
// // if (result.type != ResultType.done) { void stopUpgrading() async {
// // Get.snackbar('错误', '安装失败: ${result.message}'); jlota.onOtaClose();
// // } return;
// } // isUpgrade.value = false;
}
void _listenToEvents() {
Ota.instance.eventStream.listen((event) {
if (event.event == 'onProgress' &&
event.type != null &&
event.progress != null) {
progress.value = event.progress!;
type.value = event.type!;
isUpgrade.value = true; // 开始升级
}
if (event.event == 'onStartOTA') {
type.value = OtaEvent.START_UPGRADING;
isUpgrade.value = true; // 开始升级
}
if (event.event == 'onStopOTA') {
type.value = OtaEvent.UPGRADE_SUCCESS;
isUpgrade.value = false;
Get.snackbar('升级成功', '请退出重新连接设备');
_getCurrentVersion();
}
if (event.event == 'onError') {
type.value = OtaEvent.UPGRADE_FAILED;
isUpgrade.value = false;
}
if (event.event == 'NOCONNECTION') {
type.value = OtaEvent.CONNECTION_DISCONNECTION;
isUpgrade.value = false;
}
if (event.event == 'CONNECTION_SUCCESS') {
type.value = OtaEvent.CONNECTION_SUCCESS;
isUpgrade.value = true; // 开始升级
}
});
} }
} }

285
lib/modules/ota_upgrade/views/ota_upgrade_view.dart

@ -10,70 +10,125 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return WillPopScope(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[100], onWillPop: () async {
appBar: AppBar( if (controller.isUpgrade.value) {
automaticallyImplyLeading: false, // 显示确认弹窗
centerTitle: true, await Get.dialog(
elevation: 0, AlertDialog(
backgroundColor: isDarkMode ? Colors.black : Colors.white, title: Text('提示'),
leading: IconButton( content: Text('当前正在升级,退出将中断升级进程,确定要退出吗?'),
icon: Icon( actions: [
Icons.arrow_back_ios, TextButton(
size: 20.sp, onPressed: () => Get.back(), // 关闭弹窗
color: isDarkMode ? Colors.white : Colors.black, child: Text('取消'),
),
TextButton(
onPressed: () {
controller.stopUpgrading();
Get.back(); // 关闭弹窗
Get.back(); // 返回上一页
},
child: Text('确定'),
),
],
),
);
return false; // 阻止默认返回
}
return true; // 允许返回
},
child: Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[100],
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
elevation: 0,
backgroundColor: isDarkMode ? Colors.black : Colors.white,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: () {
if (controller.isUpgrade.value) {
// 显示确认弹窗
Get.dialog(
AlertDialog(
title: Text('提示'),
content: Text('当前正在升级,退出将中断升级进程,确定要退出吗?'),
actions: [
TextButton(
onPressed: () => Get.back(), // 关闭弹窗
child: Text('取消'),
),
TextButton(
onPressed: () {
controller.stopUpgrading();
Get.back(); // 关闭弹窗
Get.back(); // 返回上一页
},
child: Text('确定'),
),
],
),
);
} else {
Get.back(); // 正常返回
}
},
), ),
onPressed: () => Get.back(), title: Text(
), '固件升级'.tr,
title: Text( style: TextStyle(
'固件升级'.tr, fontSize: 20.sp,
style: TextStyle( color: isDarkMode ? Colors.white : Colors.black,
fontSize: 20.sp, ),
color: isDarkMode ? Colors.white : Colors.black,
), ),
), ),
), body: SingleChildScrollView(
body: SingleChildScrollView( child: Padding(
child: Padding( padding: EdgeInsets.all(20),
padding: EdgeInsets.all(20), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ // 设备信息卡片
// 设备信息卡片 Card(
Card( elevation: 2,
elevation: 2, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16), ),
), color: isDarkMode ? Colors.grey[900] : Colors.white,
color: isDarkMode ? Colors.grey[900] : Colors.white, child: Padding(
child: Padding( padding: EdgeInsets.all(20),
padding: EdgeInsets.all(20), child: Column(
child: Column( children: [
children: [ _buildDeviceIcon(isDarkMode),
_buildDeviceIcon(isDarkMode), SizedBox(height: 20),
SizedBox(height: 20), _buildVersionInfo(isDarkMode),
_buildVersionInfo(isDarkMode), ],
], ),
), ),
), ),
), SizedBox(height: 20),
SizedBox(height: 20),
// 更新说明 // 更新说明
_buildUpdateNotes(isDarkMode), _buildUpdateNotes(isDarkMode),
SizedBox(height: 20), SizedBox(height: 20),
// 状态显示区域 // 状态显示区域
Obx(() { Obx(() {
if (controller.isFetchingDeviceInfo.value) { if (controller.isFetchingDeviceInfo.value) {
return _buildLoadingIndicator(isDarkMode); return _buildLoadingIndicator(isDarkMode);
} }
if (controller.errorMessage.value.isNotEmpty) { if (controller.errorMessage.value.isNotEmpty) {
return _buildErrorInfo(isDarkMode); return _buildErrorInfo(isDarkMode);
} }
return _buildUpdateButton(isDarkMode); return _buildUpdateButton(isDarkMode);
}), }),
], ],
),
), ),
), ),
), ),
@ -101,7 +156,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
fontSize: 18, fontSize: 18,
color: isDarkMode ? Colors.grey[300] : Colors.black)), color: isDarkMode ? Colors.grey[300] : Colors.black)),
SizedBox(width: 8), // 改为width SizedBox(width: 8), // 改为width
Obx(() => Text('v${controller.currentVersion}', Obx(() => Text('V ${controller.currentVersion}',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
@ -117,11 +172,11 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
fontSize: 18, fontSize: 18,
color: isDarkMode ? Colors.grey[300] : Colors.black)), color: isDarkMode ? Colors.grey[300] : Colors.black)),
SizedBox(width: 8), // 改为width SizedBox(width: 8), // 改为width
Text('v${controller.latestVersion}', Obx(() => Text('V ${controller.latestVersion}',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: isDarkMode ? Colors.green[300] : Colors.green)), color: isDarkMode ? Colors.blue[300] : Colors.blue))),
], ],
), ),
], ],
@ -182,7 +237,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
isDarkMode ? Colors.blue[300] : Colors.blue), isDarkMode ? Colors.blue[300] : Colors.blue),
), ),
SizedBox(height: 20), SizedBox(height: 20),
Text('正在连接设备...', Text('正在获取设备信息...',
style: style:
TextStyle(color: isDarkMode ? Colors.grey[300] : Colors.grey)) TextStyle(color: isDarkMode ? Colors.grey[300] : Colors.grey))
], ],
@ -227,7 +282,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
); );
Widget _buildUpdateButton(bool isDarkMode) { Widget _buildUpdateButton(bool isDarkMode) {
return controller.isUpdating.value return controller.isUpgrade.value
? _buildProgressIndicator(isDarkMode) ? _buildProgressIndicator(isDarkMode)
: Container( : Container(
width: double.infinity, width: double.infinity,
@ -262,7 +317,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
if (controller.isUpdateAvailable.value) ...[ if (controller.isUpdateAvailable.value) ...[
SizedBox(height: 8), SizedBox(height: 8),
Text( Text(
'建议连接WiFi并保持充电状态'.tr, '取出双耳机,并保证电量均为30%以上的状态'.tr,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -274,61 +329,67 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
); );
} }
Widget _buildProgressIndicator(bool isDarkMode) => Container( Widget _buildProgressIndicator(bool isDarkMode) {
padding: EdgeInsets.all(20), return Obx(() {
decoration: BoxDecoration( final progress = controller.progress.value;
color: final type = controller.type.value;
isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50],
borderRadius: BorderRadius.circular(16), // // 处理升级失败状态:弹出 snackbar 并显示错误提示
), // if (type == OtaUpgradeController.UPGRADE_FAILED ||
// type == OtaUpgradeController.CONNECTION_DISCONNECTION) {
// // 使用延迟确保在 build 完成后执行
// Future.delayed(Duration.zero, () {
// Get.snackbar('升级失败', '重新连接设备后重试');
// });
// return Container(
// width: double.infinity,
// padding: const EdgeInsets.symmetric(vertical: 10),
// child: Text(
// '升级失败,请重新连接设备',
// style: TextStyle(
// fontSize: 18,
// fontWeight: FontWeight.bold,
// color: Colors.red[700],
// ),
// ),
// );
// }
// 正常升级状态
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column( child: Column(
children: [ children: [
Text('正在下载更新...'.tr, Text(
style: TextStyle( controller.getStatusMessage(type),
fontSize: 18, style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 18,
color: isDarkMode ? Colors.blue[300] : Colors.blue[700])), fontWeight: FontWeight.bold,
SizedBox(height: 16), color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
//SizedBox(
//width: 240,
Container(
decoration: BoxDecoration(
border: Border.all(
color: isDarkMode ? Colors.blue[400]! : Colors.blue[300]!,
width: 1.5,
),
borderRadius: BorderRadius.circular(12),
), ),
child: LinearProgressIndicator( ),
value: controller.downloadProgress.value, const SizedBox(height: 16),
backgroundColor: LinearProgressIndicator(
isDarkMode ? Colors.grey[800] : Colors.grey[200], value: progress / 100.0, // 转换为 0.0-1.0
valueColor: AlwaysStoppedAnimation( backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200],
isDarkMode ? Colors.blue[300] : Colors.blue), valueColor: AlwaysStoppedAnimation<Color>(
minHeight: 12, isDarkMode ? Colors.blue[300]! : Colors.blue,
borderRadius: BorderRadius.circular(10), ),
),
const SizedBox(height: 8),
Text(
'${progress.toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
), ),
), ),
SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.downloading,
color: isDarkMode ? Colors.blue[300] : Colors.blue,
size: 20,
),
SizedBox(width: 8),
Text(
'${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color:
isDarkMode ? Colors.blue[300] : Colors.blue[700])),
],
)
], ],
), ),
); );
});
}
} }

39
lib/modules/settings/controllers/settings_controller.dart

@ -8,6 +8,7 @@ import '../../../data/services/speech_factory.dart';
import '../models/language_model.dart'; import '../models/language_model.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import '../../../data/models/user_Info.dart'; import '../../../data/models/user_Info.dart';
import '../../../data/services/ble_manager.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'dart:io'; import 'dart:io';
@ -402,4 +403,42 @@ class SettingsController extends GetxController {
sourceLanguage.value = targetLanguage.value; sourceLanguage.value = targetLanguage.value;
targetLanguage.value = temp; targetLanguage.value = temp;
} }
/// 清除BLE配对设备
Future<void> clearBleDevices() async {
// 显示确认对话框
final confirmed = await Get.dialog<bool>(
AlertDialog(
title: const Text('清除配对设备'),
content: const Text('确定要清除所有BLE配对设备吗?此操作不可撤销。'),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () => Get.back(result: true),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('确定'),
),
],
),
);
if (confirmed == true) {
try {
final bleManager = Get.find<BleManager>();
final result = await bleManager.clearPairedDevices();
if (result) {
Get.snackbar('成功', 'BLE配对设备已清除');
} else {
Get.snackbar('失败', '清除BLE配对设备失败');
}
} catch (e) {
Get.snackbar('错误', '清除BLE配对设备时发生错误');
}
}
}
} }

24
lib/modules/settings/views/settings_view.dart

@ -481,6 +481,30 @@ class SettingsView extends GetView<SettingsController> {
color: isDarkMode color: isDarkMode
? Colors.white.withOpacity(0.1) ? Colors.white.withOpacity(0.1)
: Colors.grey[200]), : Colors.grey[200]),
// 清除配对设备
_buildSimpleNavigationSetting(
title: '清除配对设备',
subtitle: '清除所有BLE配对设备记录',
icon: Icons.bluetooth_disabled,
iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3)
: Colors.red[100]!,
iconColor:
isDarkMode ? Colors.red[300]! : Colors.red[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
controller.clearBleDevices();
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 杰理测试 // 杰理测试
_buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
title: '杰理蓝牙测试', title: '杰理蓝牙测试',

2
local_plugins/agent_service/android/build.gradle.kts

@ -48,5 +48,5 @@ dependencies {
add("compileOnly", project(":music_service")) add("compileOnly", project(":music_service"))
add("compileOnly", project(":amap_service")) add("compileOnly", project(":amap_service"))
add("compileOnly", project(":speech")) add("compileOnly", project(":speech"))
add("compileOnly", project(":open_ai_service")) add("compileOnly", project(":chat_api"))
} }

2
local_plugins/agent_service/android/src/main/AndroidManifest.xml

@ -13,7 +13,7 @@
android:name=".BleMediaService" android:name=".BleMediaService"
android:exported="true" android:exported="true"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="mediaPlayback"> android:foregroundServiceType="connectedDevice">
<!-- <intent-filter> <!-- <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> --> </intent-filter> -->

133
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -4,7 +4,8 @@ import android.content.Context
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.open_ai_service.OpenAIService import com.yunqiinnovation.chat_api.ChatApiService
import com.yunqiinnovation.chat_api.StreamCallback
import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.amap_service.NavigationServiceHelper import com.yunqiinnovation.amap_service.NavigationServiceHelper
@ -53,8 +54,8 @@ object AgentService : CoroutineScope {
// 使用AzureTtsHelper作为唯一的TTS实现 // 使用AzureTtsHelper作为唯一的TTS实现
private var ttsService: AzureTtsHelper? = null private var ttsService: AzureTtsHelper? = null
// OpenAI服务 - 修改为使用OpenAIService // ChatAPI服务 - 使用新的ChatApiService
private lateinit var openAIService: OpenAIService private lateinit var chatApiService: ChatApiService
// 聊天存储服务 // 聊天存储服务
private lateinit var chatStorageHelper: ChatStorageHelper private lateinit var chatStorageHelper: ChatStorageHelper
@ -121,7 +122,7 @@ object AgentService : CoroutineScope {
try { try {
// 初始化上下文和服务 // 初始化上下文和服务
this.context = appContext.applicationContext this.context = appContext.applicationContext
this.openAIService = OpenAIService(context) this.chatApiService = ChatApiService(context)
this.chatStorageHelper = ChatStorageHelper(context) this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context) this.audioPlayer = AudioPlayer(context)
@ -146,8 +147,8 @@ object AgentService : CoroutineScope {
// 初始化TTS服务 // 初始化TTS服务
initializeTtsEngine(config) initializeTtsEngine(config)
// 初始化OpenAI服务 // 初始化ChatAPI服务
openAIService.initialize( chatApiService.initialize(
config["openaiApiKey"]?.toString() ?: "", config["openaiApiKey"]?.toString() ?: "",
config["openaiBaseUrl"]?.toString() ?: "", config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "", config["openaiModel"]?.toString() ?: "",
@ -181,9 +182,9 @@ object AgentService : CoroutineScope {
azureAsrHelper?.dispose() azureAsrHelper?.dispose()
// 释放OpenAI服务 // 释放ChatAPI服务
if (::openAIService.isInitialized) { if (::chatApiService.isInitialized) {
openAIService.cancelAll() chatApiService.cancelCurrentStream()
} }
// 释放TTS服务 // 释放TTS服务
@ -401,7 +402,7 @@ object AgentService : CoroutineScope {
"language" to detectedLanguage "language" to detectedLanguage
)) ))
processWithOpenAI(result) processWithChatApi(result)
} }
// 重置状态,继续识别 // 重置状态,继续识别
@ -513,8 +514,8 @@ object AgentService : CoroutineScope {
currentAiJob?.cancel() currentAiJob?.cancel()
currentAiJob = null currentAiJob = null
// 通知OpenAI服务终止当前流式请求 // 通知ChatAPI服务终止当前流式请求
openAIService.cancelCurrentStream() chatApiService.cancelCurrentStream()
// 记录日志 // 记录日志
Log.d(TAG, "AI流输出已停止") Log.d(TAG, "AI流输出已停止")
@ -543,43 +544,43 @@ object AgentService : CoroutineScope {
return false return false
} }
// 使用OpenAI处理文本 // 使用ChatAPI处理文本
processWithOpenAI(text, speakResponse) processWithChatApi(text, speakResponse)
return true return true
} }
/** /**
* 使用OpenAI处理语音识别结果 * 使用ChatAPI处理语音识别结果
*/ */
private fun processWithOpenAI(text: String) { private fun processWithChatApi(text: String) {
processWithOpenAI(text, true) processWithChatApi(text, true)
} }
/** /**
* 使用OpenAI处理文本消息 * 使用ChatAPI处理文本消息
* *
* @param text 用户输入文本 * @param text 用户输入文本
* @param speakResponse 是否使用TTS朗读回复 * @param speakResponse 是否使用TTS朗读回复
*/ */
private fun processWithOpenAI( private fun processWithChatApi(
text: String, text: String,
speakResponse: Boolean = true speakResponse: Boolean = true
) { ) {
Log.d(TAG, "用户问题: $text") Log.d(TAG, "用户问题: $text")
// 创建用户文本消息并处理 // 创建用户文本消息并处理
val userMessage = openAIService.createUserMessage(text) val userMessage = createUserMessage(text)
processWithOpenAIInternal(userMessage, text, speakResponse) processWithChatApiInternal(userMessage, text, speakResponse)
} }
/** /**
* 使用OpenAI处理图片 * 使用ChatAPI处理图片
* *
* @param imageBase64 Base64编码的图片数据 * @param imageBase64 Base64编码的图片数据
* @param text 可选的文本描述或问题 * @param text 可选的文本描述或问题
* @param speakResponse 是否朗读回复 * @param speakResponse 是否朗读回复
*/ */
private fun processImageWithOpenAI( private fun processImageWithChatApi(
imageBase64: String, imageBase64: String,
text: String = "", text: String = "",
speakResponse: Boolean = false speakResponse: Boolean = false
@ -587,14 +588,14 @@ object AgentService : CoroutineScope {
Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
// 创建带图片的用户消息并处理 // 创建带图片的用户消息并处理
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) val userMessage = createUserMessageWithImage(text, imageBase64)
// 图片描述用于存储 // 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" } val displayText = text.ifEmpty { "[图片]" }
processWithOpenAIInternal(userMessage, displayText, speakResponse, true) processWithChatApiInternal(userMessage, displayText, speakResponse, true)
} }
/** /**
* 内部方法:通用的OpenAI处理逻辑 * 内部方法:通用的ChatAPI处理逻辑
* *
* @param userMessage 用户消息(可以是文本或图片格式) * @param userMessage 用户消息(可以是文本或图片格式)
* @param displayText 用于显示和存储的文本 * @param displayText 用于显示和存储的文本
@ -602,7 +603,7 @@ object AgentService : CoroutineScope {
* @param hasImage 是否包含图片 * @param hasImage 是否包含图片
* @param imagePath 图片路径(如果有) * @param imagePath 图片路径(如果有)
*/ */
private fun processWithOpenAIInternal( private fun processWithChatApiInternal(
userMessage: JSONObject, userMessage: JSONObject,
displayText: String, displayText: String,
speakResponse: Boolean = true, speakResponse: Boolean = true,
@ -634,7 +635,7 @@ object AgentService : CoroutineScope {
// 先添加系统提示词 // 先添加系统提示词
if (systemPrompt.isNotEmpty()) { if (systemPrompt.isNotEmpty()) {
messagesWithSystemPrompt.put(openAIService.createSystemMessage(systemPrompt)) messagesWithSystemPrompt.put(createSystemMessage(systemPrompt))
// Log.d(TAG, "系统提示词: ${systemPrompt}") // Log.d(TAG, "系统提示词: ${systemPrompt}")
} }
@ -672,9 +673,9 @@ object AgentService : CoroutineScope {
addToHistoryMessages(userMessage) addToHistoryMessages(userMessage)
} }
openAIService.sendMessageStream( chatApiService.sendMessageStream(
messagesWithSystemPrompt, messagesWithSystemPrompt,
object : OpenAIService.StreamCallback { object : StreamCallback {
override fun onToken(token: String) { override fun onToken(token: String) {
responseBuilder.append(token) responseBuilder.append(token)
if (speakResponse && !nobroadcast) { if (speakResponse && !nobroadcast) {
@ -704,7 +705,7 @@ object AgentService : CoroutineScope {
sendEvent("assistant_response", responseData) sendEvent("assistant_response", responseData)
// 添加AI回复到历史记录 // 添加AI回复到历史记录
addToHistoryMessages(openAIService.createAssistantMessage(response)) addToHistoryMessages(createAssistantMessage(response))
// 保存聊天记录 // 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString()) saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
@ -796,9 +797,9 @@ object AgentService : CoroutineScope {
val content = message["message"] as String val content = message["message"] as String
if (sender == "user") { if (sender == "user") {
addToHistoryMessages(openAIService.createUserMessage(content)) addToHistoryMessages(createUserMessage(content))
} else if (sender == "assistant") { } else if (sender == "assistant") {
addToHistoryMessages(openAIService.createAssistantMessage(content)) addToHistoryMessages(createAssistantMessage(content))
} }
} }
@ -945,6 +946,62 @@ object AgentService : CoroutineScope {
return result == android.content.pm.PackageManager.PERMISSION_GRANTED return result == android.content.pm.PackageManager.PERMISSION_GRANTED
} }
/**
* 创建用户消息
*/
private fun createUserMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "user")
put("content", content)
}
}
/**
* 创建系统消息
*/
private fun createSystemMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "system")
put("content", content)
}
}
/**
* 创建助手消息
*/
private fun createAssistantMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "assistant")
put("content", content)
}
}
/**
* 创建带图片的用户消息
*/
private fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject {
val content = JSONArray().apply {
if (text.isNotEmpty()) {
put(JSONObject().apply {
put("type", "text")
put("text", text)
})
}
put(JSONObject().apply {
put("type", "image_url")
put("image_url", JSONObject().apply {
put("url", "data:image/jpeg;base64,$imageBase64")
put("detail", "auto")
})
})
}
return JSONObject().apply {
put("role", "user")
put("content", content)
}
}
/** /**
* 处理图片输入 * 处理图片输入
*/ */
@ -971,7 +1028,7 @@ object AgentService : CoroutineScope {
launch { launch {
try { try {
// 将图片转换为Base64格式 // 将图片转换为Base64格式
val imageBase64 = openAIService.fileToBase64(imagePath) ?: run { val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run {
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "IMAGE_CONVERSION_FAILED", "code" to "IMAGE_CONVERSION_FAILED",
"message" to "图片转换失败" "message" to "图片转换失败"
@ -980,9 +1037,9 @@ object AgentService : CoroutineScope {
} }
// 创建带图片的用户消息并处理 // 创建带图片的用户消息并处理
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) val userMessage = createUserMessageWithImage(text, imageBase64)
// 图片描述用于存储 // 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" } val displayText = text.ifEmpty { "[图片]" }
// 通知图片准备完成 // 通知图片准备完成
sendEvent("image_ready", mapOf( sendEvent("image_ready", mapOf(
@ -991,7 +1048,7 @@ object AgentService : CoroutineScope {
)) ))
// 处理消息并传入存储路径 // 处理消息并传入存储路径
processWithOpenAIInternal(userMessage, displayText, speakResponse, true, imagePath) processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "处理图片失败: ${e.message}") Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(

329
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -5,46 +5,35 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanSettings
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.agent_service.R
import android.util.Log import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import android.view.KeyEvent
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import com.yunqiinnovation.ble_service.BleConst import com.yunqiinnovation.ble_service.BleConst
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
/** /**
* 语音助手媒体服务 * BLE代理服务
* 职责: * 职责:
* 1. 接收并处理媒体按钮事件(播放/暂停等) * 1. 语音代理初始化和管理
* 2. 维持媒体会话,使系统可以通过媒体按钮唤醒应用 * 2. BLE设备连接管理
* 3. 最小化实现,不直接对接Flutter层 * 3. 后台扫描结果处理
*/ */
class BleMediaService : Service(), CoroutineScope { class BleMediaService : Service(), CoroutineScope {
companion object { companion object {
private const val TAG = "BleMediaService" private const val TAG = "BleAgentService"
private const val CHANNEL_ID = "ble_media_service_channel" private const val CHANNEL_ID = "ble_agent_service_channel"
private const val NOTIFICATION_ID = 1002 private const val NOTIFICATION_ID = 1002
private const val PREF_NAME = "deep_voice_config" private const val PREF_NAME = "deep_voice_config"
@ -71,17 +60,9 @@ class BleMediaService : Service(), CoroutineScope {
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job get() = Dispatchers.IO + job
private lateinit var mediaSession: MediaSessionCompat // BLE扫描防抖
private lateinit var audioManager: AudioManager
// 媒体操作防抖
private var lastActionTime = 0L
private var lastScanTime = 0L private var lastScanTime = 0L
private val ACTION_DEBOUNCE_MS = 800L // 媒体操作防抖时间(毫秒) private val SCAN_DEBOUNCE_MS = 800L // BLE扫描防抖时间(毫秒)
// 添加音频播放器
private var audioPlayer: MiniMediaPlayer? = null
// 语音代理是否已初始化 // 语音代理是否已初始化
private var isChatAgentInitialized = false private var isChatAgentInitialized = false
@ -91,106 +72,85 @@ class BleMediaService : Service(), CoroutineScope {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.i(TAG, "BleMediaService正在创建...") Log.i(TAG, "BLE代理服务正在创建...")
// 设置服务状态 // 设置服务状态
isRunning.set(true) isRunning.set(true)
startForeground(NOTIFICATION_ID, createNotification()) startForeground(NOTIFICATION_ID, createNotification())
// 初始化音频管理器
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 创建并启动媒体会话
setupMediaSession()
// 注册媒体按钮接收器 Log.i(TAG, "BLE代理服务已启动")
// registerMediaButtonReceiver()
// // 注册后台扫描
// registerBackgroundScan()
// 创建音频播放器,并播放静音文件
audioPlayer = MiniMediaPlayer(this)
audioPlayer?.play(R.raw.silence, 0f)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
Log.i(TAG, "媒体服务已启动")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") Log.i(TAG, "BleAgentService.onStartCommand: ${intent?.action}")
// 保存当前Intent,用于后续初始化 // 保存当前Intent,用于后续初始化
currentIntent = intent currentIntent = intent
// 初始化语音代理
// 初始化语音代理
if (!isChatAgentInitialized) { if (!isChatAgentInitialized) {
initializeChatAgent() initializeChatAgent()
} }
// 如果是媒体按钮事件,交给MediaButtonReceiver处理
if (Intent.ACTION_MEDIA_BUTTON == intent?.action) {
MediaButtonReceiver.handleIntent(mediaSession, intent)
return START_STICKY
}
// 处理蓝牙扫描结果事件 // 处理蓝牙扫描结果事件
if (intent?.action == BleConst.ACTION_SCAN_RESULT) { if (intent?.action == BleConst.ACTION_SCAN_RESULT) {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastScanTime >= ACTION_DEBOUNCE_MS) { if (currentTime - lastScanTime >= SCAN_DEBOUNCE_MS) {
lastScanTime = currentTime lastScanTime = currentTime
checkBleConnection() checkBleConnection()
} }
return START_STICKY return START_STICKY
} }
return START_STICKY return START_STICKY
} }
/** /**
* 初始化语音交互代理 * 初始化语音交互代理
*/ */
private fun initializeChatAgent() { private fun initializeChatAgent() {
try { try {
// 获取SharedPreferences // 获取SharedPreferences
val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
// 尝试从Intent中获取参数 // 尝试从Intent中获取参数
val configFromIntent = extractConfigFromIntent(currentIntent) val configFromIntent = extractConfigFromIntent(currentIntent)
// 如果Intent中有参数,则保存到SharedPreferences // 如果Intent中有参数,则保存到SharedPreferences
if (configFromIntent.isNotEmpty()) { if (configFromIntent.isNotEmpty()) {
Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
saveConfigToPreferences(configFromIntent, prefs) saveConfigToPreferences(configFromIntent, prefs)
} }
// 合并Intent和SharedPreferences中的参数 // 合并Intent和SharedPreferences中的参数
val config = getConfigFromPreferences(prefs) val config = getConfigFromPreferences(prefs)
Log.d(TAG, "当前配置参数: $config") Log.d(TAG, "当前配置参数: $config")
// 检查必要的配置是否存在
if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
Log.w(TAG, "语音代理初始化失败: API密钥未配置")
return
}
if (isChatAgentInitialized) { // 检查必要的配置是否存在
Log.d(TAG, "ChatAgent已经初始化,跳过") if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
return Log.w(TAG, "语音代理初始化失败: API密钥未配置")
} return
if(config.isNotEmpty()) { }
// 初始化ChatAgent
Log.i(TAG, "开始初始化ChatAgent") if (isChatAgentInitialized) {
val success = BleAgent.initialize(applicationContext, config) Log.d(TAG, "ChatAgent已经初始化,跳过")
if (success) { return
isChatAgentInitialized = true }
Log.i(TAG, "语音交互代理初始化成功")
} else { if(config.isNotEmpty()) {
Log.e(TAG, "语音交互代理初始化失败") // 初始化ChatAgent
} Log.i(TAG, "开始初始化ChatAgent")
val success = BleAgent.initialize(applicationContext, config)
if (success) {
isChatAgentInitialized = true
Log.i(TAG, "语音交互代理初始化成功")
} else {
Log.e(TAG, "语音交互代理初始化失败")
} }
} catch (e: Exception) {
Log.e(TAG, "初始化语音交互代理时发生异常", e)
} }
} catch (e: Exception) {
Log.e(TAG, "初始化语音交互代理时发生异常", e)
}
} }
/** /**
@ -255,23 +215,7 @@ class BleMediaService : Service(), CoroutineScope {
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() { override fun onDestroy() {
Log.i(TAG, "BleMediaService正在销毁...") Log.i(TAG, "BLE代理服务正在销毁...")
// 取消注册媒体按钮接收器
try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
Log.i(TAG, "媒体按钮接收器已取消注册")
} catch (e: Exception) {
Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e)
}
// 释放媒体会话
mediaSession.release()
// 停止并释放音频播放器
audioPlayer?.release()
audioPlayer = null
// 取消所有协程 // 取消所有协程
cancel() cancel()
@ -282,61 +226,11 @@ class BleMediaService : Service(), CoroutineScope {
super.onDestroy() super.onDestroy()
} }
/**
* 设置媒体会话,处理媒体按钮事件
*/
private fun setupMediaSession() {
mediaSession = MediaSessionCompat(this, "BleMediaSession").apply {
setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
// 设置回调,处理媒体按钮事件
setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean {
// 使用系统默认的媒体按钮处理
return super.onMediaButtonEvent(mediaButtonIntent)
}
})
// 设置初始播放状态
val stateBuilder = PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
.setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f)
setPlaybackState(stateBuilder.build())
// 激活会话,使其可以接收媒体按钮事件
isActive = true
}
}
/**
* 注册媒体按钮接收器
*/
private fun registerMediaButtonReceiver() {
try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
} catch (e: Exception) {
Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
}
}
private fun checkBleConnection() { private fun checkBleConnection() {
Log.i(TAG, "checkBleConnection收到连接请求") Log.i(TAG, "checkBleConnection收到连接请求")
try { try {
// 示例:使用协程异步处理连接 // 使用协程异步处理连接
launch { launch {
if(!BleService.isConnected()) { if(!BleService.isConnected()) {
BleService.connect() BleService.connect()
@ -347,44 +241,6 @@ class BleMediaService : Service(), CoroutineScope {
} }
} }
/**
* 更新通知内容
*/
private fun updateNotification() {
try {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, createNotification())
} catch (e: Exception) {
Log.e(TAG, "更新通知异常: ${e.message}")
}
}
/**
* 设置播放状态
*/
private fun setPlaybackState(state: Int) {
val playbackStateBuilder = PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_PLAY_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
PlaybackStateCompat.ACTION_STOP or
PlaybackStateCompat.ACTION_SEEK_TO or
PlaybackStateCompat.ACTION_FAST_FORWARD or
PlaybackStateCompat.ACTION_REWIND
)
// 使用更高的播放速度,让系统认为是真正的播放
.setState(state, 0, 1.0f, System.currentTimeMillis())
mediaSession.setPlaybackState(playbackStateBuilder.build())
// 确保MediaSession激活
if (!mediaSession.isActive) {
mediaSession.isActive = true
}
}
/** /**
* 创建前台服务通知 * 创建前台服务通知
*/ */
@ -395,7 +251,7 @@ class BleMediaService : Service(), CoroutineScope {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel( val channel = NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
"语音助手媒体服务", "语音助手代理服务",
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
) )
manager.createNotificationChannel(channel) manager.createNotificationChannel(channel)
@ -419,77 +275,12 @@ class BleMediaService : Service(), CoroutineScope {
// 构建通知 // 构建通知
val builder = NotificationCompat.Builder(this, CHANNEL_ID) val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound") .setContentTitle("DeepSound")
// .setContentText(if (isPlaying) "正在播放" else "已暂停") .setContentText("语音助手代理服务运行中")
.setSmallIcon(android.R.drawable.ic_media_play) .setSmallIcon(android.R.drawable.ic_dialog_info)
// 设置点击意图 // 设置点击意图
pendingIntent?.let { builder.setContentIntent(it) } pendingIntent?.let { builder.setContentIntent(it) }
return builder.build() return builder.build()
} }
/**
* 简易媒体播放器,用于播放静音资源
*/
inner class MiniMediaPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
/**
* 播放资源文件
* @param resId 资源ID
* @param volume 音量 (0.0-1.0)
*/
fun play(resId: Int, volume: Float = 0f) {
try {
// 释放之前的资源
release()
// 创建播放器并设置资源
mediaPlayer = MediaPlayer().apply {
// 设置资源
context.resources.openRawResourceFd(resId)?.use { fd ->
setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
}
// 设置音量
setVolume(volume, volume)
// // 播放完成后自动循环播放,保持媒体会话活跃
// setOnCompletionListener {
// Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃")
// play(resId, volume)
// }
// 准备并播放
prepare()
start()
}
Log.i(TAG, "开始播放静音资源")
} catch (e: Exception) {
Log.e(TAG, "播放静音资源异常: ${e.message}", e)
release()
}
}
/**
* 释放资源
*/
fun release() {
try {
mediaPlayer?.apply {
if (isPlaying) stop()
release()
}
mediaPlayer = null
} catch (e: Exception) {
// 忽略异常
}
}
/**
* 是否正在播放
*/
fun isPlaying() = mediaPlayer?.isPlaying == true
}
} }

48
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -30,7 +30,6 @@ import com.jieli.jl_audio_decode.callback.OnStateCallback
import com.jieli.jl_audio_decode.opus.OpusManager import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException import com.jieli.jl_audio_decode.exceptions.OpusException
import com.yunqiinnovation.ble_service.BleCompanionManager import com.yunqiinnovation.ble_service.BleCompanionManager
import com.yunqiinnovation.ble_service.OTAManager
import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure; import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure;
import com.jieli.jl_bt_ota.constant.ErrorCode; import com.jieli.jl_bt_ota.constant.ErrorCode;
import com.jieli.jl_bt_ota.constant.StateCode; import com.jieli.jl_bt_ota.constant.StateCode;
@ -89,7 +88,8 @@ object BleService {
private lateinit var scanHandler: Handler private lateinit var scanHandler: Handler
private val scanResults = ArrayList<ScanResult>() private val scanResults = ArrayList<ScanResult>()
private var isScanning = false private var isScanning = false
// 是否是ota模式
var isEnterOta = false
// 连接状态 // 连接状态
val connectionState = MutableLiveData(BleConst.STATE_DISCONNECTED) val connectionState = MutableLiveData(BleConst.STATE_DISCONNECTED)
@ -354,10 +354,14 @@ object BleService {
return return
} }
Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
if(!isEnterOta)
{
Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
// 调用现有的 connect 方法 // 调用现有的 connect 方法
connect(pairedMac) connect(pairedMac)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "自动连接已配对设备失败: ${e.message}", e) Log.e(TAG, "自动连接已配对设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
@ -428,44 +432,6 @@ object BleService {
} }
} }
// fun startOTA() {
// try {
// otaManager?.startOTA(CustomUpdateCallback(context, null))
// Log.i(TAG, "OTA 升级已启动")
// } catch (e: IllegalStateException) {
// Log.e(TAG, "OTA 启动失败: ${e.message}")
// disconnectGatt()
// }
// }
// // 配置OTA
// fun configureOTA() {
// try {
// val firmwarePath =
// context.getExternalFilesDir("downloads/ota_upgrades")?.path + "/1.ufw"
// val file = File(firmwarePath)
// if (!file.exists()) {
// // throw IOException("Firmware file not found")
// Log.i(TAG, "Firmware file not found")
// }
// val bluetoothOption = BluetoothOTAConfigure().apply {
// priority = BluetoothOTAConfigure.PREFER_BLE
// isUseReconnect = true
// isUseAuthDevice = true
// mtu = 512
// isNeedChangeMtu = false // 确保MTU请求生效
// isUseJLServer = false
// firmwareFilePath = firmwarePath
// }
// otaManager?.configure(bluetoothOption) ?: run {
// Log.e(TAG, "OTA Manager未初始化")
// }
// } catch (e: Exception) {
// Log.e(TAG, "OTA配置失败: ${e.message}")
// // 处理错误
// }
// }
/** /**
* 更新连接状态并通知回调 * 更新连接状态并通知回调

116
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt

@ -1,116 +0,0 @@
package com.yunqiinnovation.ble_service
import io.flutter.embedding.engine.plugins.FlutterPlugin
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 android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothProfile
import android.content.Context
import com.jieli.jl_bt_ota.constant.ErrorCode;
import com.jieli.jl_bt_ota.constant.StateCode;
import com.jieli.jl_bt_ota.impl.BluetoothOTAManager;
import com.jieli.jl_bt_ota.interfaces.BtEventCallback;
import com.jieli.jl_bt_ota.interfaces.IActionCallback;
import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback;
import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure;
import com.jieli.jl_bt_ota.model.base.BaseError;
import com.jieli.jl_bt_ota.model.response.TargetInfoResponse;
import android.util.Log
import java.util.*
class CustomUpdateCallback(
val context: Context,
val device: BluetoothDevice?
) : IUpgradeCallback {
companion object {
private const val TAG = "JlOtaPlugin"
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota"
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events"
}
override fun onStartOTA() {
Log.d(TAG, "onStartOTA开始升级")
// viewModel.otaStateMLD.value = OTAStart(device)
}
override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) {
Log.d(TAG, "onNeedReconnect需要重新链接")
}
override fun onProgress(type: Int, progress: Float) {
Log.d(TAG, "onProgress正在进展")
//viewModel.otaStateMLD.value = OTAWorking(device, type, progress)
}
override fun onStopOTA() {
Log.d(TAG, "onStartOTA停止升级")
}
override fun onCancelOTA() {
Log.d(TAG, "onCancelOTA取消升级")
}
override fun onError(error: BaseError?) {
val errorMessage = error?.message ?: "Unknown error"
Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}")
// 如果需要打印完整错误对象信息(假设BaseError有重写toString())
Log.d(TAG, "完整错误对象: ${error ?: "null"}")
}
}
class OTAManager(context: Context) : BluetoothOTAManager(context) {
// 定义回调接口
interface DataCallback {
fun onDataReceived(device: BluetoothDevice?, data: ByteArray?)
}
// 保存回调的实例
private var dataCallback: DataCallback? = null
// 设置回调的方法
fun setDataCallback(callback: DataCallback) {
this.dataCallback = callback
}
override fun getConnectedDevice(): BluetoothDevice? {
// 这里可以添加空实现的注释说明
Log.i(TAG, "ota数据发送getConnectedDevice")
return null
}
override fun getConnectedBluetoothGatt(): BluetoothGatt? {
// 这里可以添加空实现的注释说明
Log.i(TAG, "ota数据发送getConnectedBluetoothGatt")
return null
}
override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) {
// 这里可以添加空实现的注释说明
Log.i(TAG, "ota数据发送connectBluetoothDevice")
}
override fun disconnectBluetoothDevice(bluetoothDevice: BluetoothDevice?) {
// 这里可以添加空实现的注释说明
Log.i(TAG, "ota数据发送disconnectBluetoothDevice")
}
/** 数据发送方法(协议无关) */
override fun sendDataToDevice(
bluetoothDevice: BluetoothDevice?,
bytes: ByteArray?
): Boolean {
Log.i(TAG, "ota数据发送${bluetoothDevice},${bytes?.size}")
// 触发回调,将数据回传给调用者
dataCallback?.onDataReceived(bluetoothDevice, bytes)
// context.onReceiveDeviceData(device, data);
return true
}
}

13
local_plugins/chat_api/README.md

@ -15,6 +15,7 @@
## 平台支持 ## 平台支持
- iOS 16.0+ - iOS 16.0+
- Android (API 21+)
## MCP 功能 ## MCP 功能
@ -138,6 +139,7 @@ chatApi.eventStream.listen((event) {
## 架构 ## 架构
### iOS
``` ```
ChatApiService (Flutter层) ChatApiService (Flutter层)
↓ ↓
@ -150,3 +152,14 @@ ChatApiService (iOS原生服务)
└── SystemFunctionHandler (内置系统功能) └── SystemFunctionHandler (内置系统功能)
``` ```
### Android
```
ChatApiService (Flutter层)
↓
ChatApiPlugin (Android插件桥接)
↓
ChatApiService (Android原生服务)
├── OpenAI客户端 (aallam/openai-kotlin 4.0.1)
└── MCP功能 (暂时留空,接口预留)
```

70
local_plugins/chat_api/android/README.md

@ -0,0 +1,70 @@
# ChatAPI Android 实现
基于 [openai-kotlin 4.0.1](https://github.com/aallam/openai-kotlin/tree/4.0.1) 的 ChatAPI 插件 Android 实现。
## 特性
- ✅ 基于 openai-kotlin 4.0.1 的 OpenAI API 客户端
- ✅ 与 iOS 版本接口完全一致
- ✅ 支持流式和非流式对话
- ✅ 支持多模态内容(文本和图像识别)
- ✅ 工具调用架构预留(MCP 功能暂时留空)
- ✅ 协程支持和异步处理
- ✅ 完整的错误处理和取消机制
## 依赖
- openai-kotlin 4.0.1
- ktor-client-okhttp 2.3.2
- kotlinx-coroutines
- gson
## 主要文件
- `ChatApiPlugin.kt` - Flutter 插件桥接层
- `ChatApiService.kt` - 核心 ChatAPI 服务实现
- `StreamCallback.kt` - 流式回调接口
## 与 iOS 版本的一致性
Android 实现完全遵循 iOS 版本的接口定义:
1. **初始化方法**: `initialize(apiKey, baseUrl, model, mcpServer)`
2. **消息创建**: `createUserMessage()`, `createAssistantMessage()`
3. **消息发送**: `sendMessage()` (非流式), `sendMessageStream()` (流式)
4. **函数注册**: `registerFunction()`
5. **MCP 相关**: 接口保留,功能暂时留空
6. **流式事件**: 完全一致的事件类型和回调
## MCP 功能状态
MCP (Model Context Protocol) 相关功能在 Android 版本中暂时留空,包括:
- `initializeMcpClient()` - 返回 false
- `isMcpInitialized()` - 返回 false
- `handleMcpToolCall()` - 返回占位符消息
- 工具调用会通知上层但不会实际执行
## 使用示例
```kotlin
// 初始化
chatApiService.initialize(
apiKey = "your-api-key",
baseUrl = "https://api.openai.com/v1/",
model = "gpt-3.5-turbo",
mcpServer = ""
)
// 流式对话
chatApiService.sendMessageStream(listOf(
mapOf("role" to "user", "content" to "Hello!")
))
```
## 注意事项
1. 需要网络权限 `INTERNET` 和 `ACCESS_NETWORK_STATE`
2. 所有网络请求在后台线程执行
3. 流式回调在主线程触发
4. 支持取消正在进行的请求

50
local_plugins/chat_api/android/build.gradle.kts

@ -0,0 +1,50 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
kotlin("plugin.serialization") version "1.9.24"
}
android {
namespace = "com.yunqiinnovation.chat_api"
compileSdk = 35
defaultConfig {
minSdk = 21
targetSdk = 33
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
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.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
// OpenAI Kotlin 4.0.1
implementation("com.aallam.openai:openai-client:4.0.1")
implementation("io.ktor:ktor-client-okhttp:2.3.2")
// JSON处理
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("com.google.code.gson:gson:2.10.1")
}

1
local_plugins/chat_api/android/settings.gradle.kts

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

8
local_plugins/chat_api/android/src/main/AndroidManifest.xml

@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.chat_api">
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

252
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt

@ -0,0 +1,252 @@
package com.yunqiinnovation.chat_api
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.engine.plugins.FlutterPlugin
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.*
/**
* ChatApiPlugin
*
* 基于 openai-kotlin 4.0.1 的 ChatAPI 插件 Android 实现
* 与 iOS 版本接口完全一致
*/
class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private var eventSink: EventChannel.EventSink? = null
private val chatApiService = ChatApiService()
private val mainHandler = Handler(Looper.getMainLooper())
// 协程作用域
private val pluginScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/methods")
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/events")
eventChannel.setStreamHandler(this)
// 设置流式回调
chatApiService.setStreamCallback(object : com.yunqiinnovation.chat_api.StreamCallback {
override fun onToken(token: String) {
sendEvent("token", token)
}
override fun onComplete() {
sendEvent("complete", null)
}
override fun onError(error: Exception) {
sendEvent("error", error.message ?: "未知错误")
}
override fun onFunctionCall(functionCall: org.json.JSONObject) {
try {
val jsonString = functionCall.toString()
sendEvent("functionCall", jsonString)
} catch (e: Exception) {
sendEvent("error", "Failed to serialize function call: ${e.message}")
}
}
override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) {
try {
val meta = org.json.JSONObject().apply {
put("functionCall", functionCall)
put("functionCallResult", functionCallResult)
}
val metaString = meta.toString()
val context = functionCallResult.optString("context", "")
sendEvent("functionCall", context, metaString)
} catch (e: Exception) {
sendEvent("error", "Failed to serialize function call result: ${e.message}")
}
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
pluginScope.cancel()
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"initialize" -> handleInitialize(call, result)
"createUserMessage" -> handleCreateUserMessage(call, result)
"createAssistantMessage" -> handleCreateAssistantMessage(call, result)
"sendMessage" -> handleSendMessage(call, result)
"sendMessageStream" -> handleSendMessageStream(call, result)
"sendFunctionCallResult" -> handleSendFunctionCallResult(call, result)
"cancelCurrentStream" -> handleCancelCurrentStream(result)
"registerFunction" -> handleRegisterFunction(call, result)
"initializeMcpClient" -> handleInitializeMcpClient(call, result)
"isMcpInitialized" -> handleIsMcpInitialized(result)
"closeMcpClient" -> handleCloseMcpClient(result)
"handleMcpToolCall" -> handleMcpToolCall(call, result)
else -> result.notImplemented()
}
}
// MARK: - Method Handlers
private fun handleInitialize(call: MethodCall, result: Result) {
val apiKey = call.argument<String>("apiKey")
if (apiKey.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
val baseUrl = call.argument<String>("baseUrl") ?: ""
val model = call.argument<String>("model") ?: ""
val mcpServer = call.argument<String>("mcpServer") ?: ""
val success = chatApiService.initialize(apiKey, baseUrl, model, mcpServer)
result.success(success)
}
private fun handleCreateUserMessage(call: MethodCall, result: Result) {
val content = call.argument<String>("content")
if (content.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
val message = chatApiService.createUserMessage(content)
result.success(message)
}
private fun handleCreateAssistantMessage(call: MethodCall, result: Result) {
val content = call.argument<String>("content")
if (content.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
val message = chatApiService.createAssistantMessage(content)
result.success(message)
}
private fun handleSendMessage(call: MethodCall, result: Result) {
@Suppress("UNCHECKED_CAST")
val messages = call.argument<List<Map<String, Any>>>("messages")
if (messages == null) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
pluginScope.launch {
try {
val response = chatApiService.sendMessage(messages)
mainHandler.post {
result.success(response)
}
} catch (e: Exception) {
mainHandler.post {
result.error("SEND_ERROR", e.message, null)
}
}
}
}
private fun handleSendMessageStream(call: MethodCall, result: Result) {
@Suppress("UNCHECKED_CAST")
val messages = call.argument<List<Map<String, Any>>>("messages")
if (messages == null) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
chatApiService.sendMessageStream(messages)
result.success(true)
}
private fun handleSendFunctionCallResult(call: MethodCall, result: Result) {
result.error("DEPRECATED", "sendFunctionCallResult已废弃,工具调用结果现在自动处理", null)
}
private fun handleCancelCurrentStream(result: Result) {
val success = chatApiService.cancelCurrentStream()
result.success(success)
}
private fun handleRegisterFunction(call: MethodCall, result: Result) {
val name = call.argument<String>("name")
val description = call.argument<String>("description")
@Suppress("UNCHECKED_CAST")
val parameters = call.argument<Map<String, Any>>("parameters")
if (name.isNullOrEmpty() || description.isNullOrEmpty() || parameters == null) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
val success = chatApiService.registerFunction(name, description, parameters)
result.success(success)
}
private fun handleInitializeMcpClient(call: MethodCall, result: Result) {
val serverUrl = call.argument<String>("serverUrl")
if (serverUrl.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
// MCP功能暂时留空
result.success(false)
}
private fun handleIsMcpInitialized(result: Result) {
// MCP功能暂时留空
result.success(false)
}
private fun handleCloseMcpClient(result: Result) {
// MCP功能暂时留空
result.success(true)
}
private fun handleMcpToolCall(call: MethodCall, result: Result) {
val functionCallJson = call.argument<String>("functionCall")
if (functionCallJson.isNullOrEmpty()) {
result.error("INVALID_ARGS", "缺少必要参数", null)
return
}
// MCP功能暂时留空
result.success("MCP功能暂未实现")
}
// MARK: - Event Stream Handler
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
private fun sendEvent(type: String, content: Any?, meta: String? = null) {
mainHandler.post {
eventSink?.let { sink ->
val event = mutableMapOf<String, Any>(
"type" to type
)
content?.let { event["content"] = it }
meta?.let { event["meta"] = it }
sink.success(event)
}
}
}
}

799
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -0,0 +1,799 @@
package com.yunqiinnovation.chat_api
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import com.aallam.openai.api.chat.*
import com.aallam.openai.api.core.Role
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.api.http.Timeout
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import com.google.gson.Gson
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import java.io.ByteArrayOutputStream
import java.io.File
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
import kotlin.math.sqrt
import kotlin.time.Duration.Companion.seconds
import android.util.Log
/**
* ChatAPI服务异常
*/
class ChatApiException(message: String) : Exception("ChatApiException: $message")
/**
* 流式回调接口
* 与 iOS 版本 StreamCallback 协议保持完全一致
*/
interface StreamCallback {
/**
* 接收到新的token
*/
fun onToken(token: String)
/**
* 流式响应完成
*/
fun onComplete()
/**
* 出现错误
*/
fun onError(error: Exception)
/**
* 函数调用 - 兼容JSONObject格式
*/
fun onFunctionCall(functionCall: org.json.JSONObject)
/**
* 函数调用结果 - 兼容JSONObject格式
*/
fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject)
}
/**
* 工具调用信息
*/
private data class ToolCallInfo(
var id: String = "",
var name: String = "",
var arguments: String = ""
) {
fun isValid(): Boolean = id.isNotEmpty() && name.isNotEmpty()
}
/**
* ChatAPI服务的Android实现
* 基于 openai-kotlin 4.0.1,与 iOS 版本接口完全一致
*/
class ChatApiService(private val context: android.content.Context? = null) : CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob()
// MARK: - 属性
private var baseUrl = "https://api.openai.com/v1/"
private var apiKey = ""
private var model = "gpt-3.5-turbo"
private var visionModel = "gpt-4-vision-preview"
private var isInitialized = false
// OpenAI 客户端
private var openAI: OpenAI? = null
// 流式请求相关
private var currentStreamJob: Job? = null
private var streamCallback: StreamCallback? = null
private var currentMessages: List<Map<String, Any>> = emptyList()
private var toolCalls: MutableMap<Int, ToolCallInfo> = mutableMapOf()
private var isCanceled = false
// JSON处理
private val gson = Gson()
// MARK: - 公共方法
/**
* 设置流式回调
*/
fun setStreamCallback(callback: StreamCallback) {
streamCallback = callback
}
/**
* 初始化ChatAPI服务
*/
fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean {
this.apiKey = apiKey
if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl
}
if (model.isNotEmpty()) {
this.model = model
}
Log.d("ChatApiService", "原始 baseUrl: $baseUrl")
// 处理 baseUrl:移除末尾的 /chat/completions(如果存在)
// 因为 openai-kotlin 会自动拼接 /chat/completions
var processedBaseUrl = if (this.baseUrl.endsWith("/chat/completions")) {
this.baseUrl.removeSuffix("/chat/completions")
} else {
this.baseUrl.removeSuffix("/")
}
// 确保以 / 结尾
if (!processedBaseUrl.endsWith("/")) {
processedBaseUrl += "/"
}
Log.d("ChatApiService", "处理后 baseUrl: $processedBaseUrl")
return try {
// 创建OpenAI配置
val config = OpenAIConfig(
token = apiKey,
timeout = Timeout(socket = 60.seconds),
host = OpenAIHost(baseUrl = processedBaseUrl)
)
openAI = OpenAI(config)
// 初始化MCP客户端 (暂时留空,但保持接口一致)
if (mcpServer.isNotEmpty()) {
// MCP功能暂时留空,但记录服务器地址以便后续实现
// initializeMcpClient(mcpServer)
}
isInitialized = apiKey.isNotEmpty()
true
} catch (e: Exception) {
false
}
}
/**
* 创建用户消息
*/
fun createUserMessage(content: String): Map<String, Any> {
return mapOf(
"role" to "user",
"content" to content
)
}
/**
* 创建助手消息
*/
fun createAssistantMessage(content: String): Map<String, Any> {
return mapOf(
"role" to "assistant",
"content" to content
)
}
/**
* 创建系统消息
*/
fun createSystemMessage(content: String): Map<String, Any> {
return mapOf(
"role" to "system",
"content" to content
)
}
/**
* 发送消息(非流式输出)
*/
suspend fun sendMessage(messages: List<Map<String, Any>>): String {
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
throw ChatApiException("ChatAPI服务未初始化")
}
// 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model
// 转换消息格式
val chatMessages = convertToChatMessages(messages)
// 构建请求
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId(currentModel),
messages = chatMessages,
maxTokens = 2000,
temperature = 0.7
)
val result = openAI!!.chatCompletion(chatCompletionRequest)
// 检查是否有工具调用
val choice = result.choices.firstOrNull()
choice?.message?.toolCalls?.firstOrNull()?.let { toolCall ->
if (toolCall is ToolCall.Function) {
val functionCallJson = mapOf(
"name" to toolCall.function.name,
"arguments" to (toolCall.function.arguments ?: ""),
"id" to toolCall.id.toString()
)
return gson.toJson(functionCallJson)
}
}
// 返回普通消息内容
return choice?.message?.content ?: throw ChatApiException("无效的响应格式")
}
/**
* 发送消息(流式输出)- 兼容JSONArray格式
*/
fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) {
// 设置回调
setStreamCallback(callback)
// 转换JSONArray为List<Map<String, Any>>
val messageList = convertJsonArrayToList(messages)
// 调用原始方法
sendMessageStream(messageList)
}
/**
* 发送消息(流式输出)
*/
fun sendMessageStream(messages: List<Map<String, Any>>) {
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
streamCallback?.onError(ChatApiException("ChatAPI服务未初始化"))
return
}
// 重置状态
isCanceled = false
currentMessages = messages
toolCalls.clear()
// 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model
currentStreamJob = launch {
try {
// 转换消息格式
val chatMessages = convertToChatMessages(messages)
// 构建请求
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId(currentModel),
messages = chatMessages,
maxTokens = 2000,
temperature = 0.7
)
val chatsFlow = openAI!!.chatCompletions(chatCompletionRequest)
chatsFlow.collect { result ->
if (isCanceled) return@collect
val choice = result.choices.firstOrNull() ?: return@collect
val delta = choice.delta ?: return@collect
// 处理普通文本内容
delta.content?.let { content ->
streamCallback?.onToken(content)
}
// 收集工具调用信息
delta.toolCalls?.forEach { toolCall ->
val index = toolCall.index
// 创建或获取现有的工具调用信息
val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() }
// 更新ID
toolCall.id?.let { toolCallInfo.id = it.toString() }
// 更新函数信息
toolCall.function?.let { function ->
function.name?.let { toolCallInfo.name = it }
function.arguments?.let { toolCallInfo.arguments += it }
}
}
}
if (!isCanceled) {
// 处理工具调用或完成
val hasToolCalls = processToolCalls()
if (!hasToolCalls) {
streamCallback?.onComplete()
}
}
} catch (e: Exception) {
if (!isCanceled) {
streamCallback?.onError(ChatApiException("流式请求失败: ${e.message}"))
}
}
}
}
/**
* 处理工具调用
*/
private suspend fun processToolCalls(): Boolean {
val firstToolCall = toolCalls.values.firstOrNull { it.isValid() } ?: return false
// 创建函数调用字典
val functionCall = mapOf(
"name" to firstToolCall.name,
"arguments" to firstToolCall.arguments,
"id" to firstToolCall.id
)
// 通知上层工具调用事件
streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall))
// 在后台队列处理工具调用
launch {
try {
if (!isCanceled) {
// 这里暂时返回占位符结果,实际MCP功能留空
val result = mapOf("context" to "MCP功能暂未实现")
if (!isCanceled) {
// 处理结果
streamCallback?.onFunctionCallResult(
convertMapToJsonObject(functionCall),
convertMapToJsonObject(result)
)
// 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal(
messages = currentMessages,
functionCall = functionCall,
functionResult = gson.toJson(result)
)
}
}
} catch (e: Exception) {
if (!isCanceled) {
val errorMessage = "工具调用处理失败: ${e.message}"
sendFunctionCallResultInternal(
messages = currentMessages,
functionCall = functionCall,
functionResult = errorMessage
)
}
}
}
return true
}
/**
* 发送函数调用结果(内部方法)
*/
private suspend fun sendFunctionCallResultInternal(
messages: List<Map<String, Any>>,
functionCall: Map<String, Any>,
functionResult: String
) {
if (isCanceled) return
val fullMessages = messages.toMutableList()
// 添加函数调用消息
val callId = functionCall["id"] as? String ?: "call_${System.currentTimeMillis()}"
fullMessages.add(
mapOf(
"role" to "assistant",
"content" to "",
"tool_calls" to listOf(
mapOf(
"id" to callId,
"type" to "function",
"function" to mapOf(
"name" to (functionCall["name"] as? String ?: ""),
"arguments" to (functionCall["arguments"] as? String ?: "{}")
)
)
)
)
)
// 添加函数结果消息
fullMessages.add(
mapOf(
"role" to "tool",
"content" to functionResult,
"tool_call_id" to callId
)
)
// 重新发起对话,让AI基于工具调用结果生成回复
sendMessageStream(fullMessages)
}
/**
* 取消当前流式请求
*/
fun cancelCurrentStream(): Boolean {
isCanceled = true
currentStreamJob?.cancel()
currentStreamJob = null
return true
}
/**
* 取消所有操作并释放资源 (与 iOS 版本保持一致)
*/
fun cancelAll() {
cancelCurrentStream()
}
/**
* 注册函数
*/
fun registerFunction(name: String, description: String, parameters: Map<String, Any>): Boolean {
// 暂时返回true,实际功能留空,但保持与iOS版本接口一致
return true
}
/**
* 初始化MCP客户端
*/
fun initializeMcpClient(serverUrl: String): Boolean {
// MCP功能暂时留空,但保持与iOS版本接口一致
return false
}
/**
* MCP客户端是否已初始化
*/
fun isMcpInitialized(): Boolean {
// MCP功能暂时留空,但保持与iOS版本接口一致
return false
}
/**
* 关闭MCP客户端
*/
fun closeMcpClient() {
// MCP功能暂时留空,但保持与iOS版本接口一致
}
/**
* 处理MCP工具调用
*/
suspend fun handleMcpToolCall(functionCallJson: String): String {
// MCP功能暂时留空,但保持与iOS版本接口一致
return "MCP功能暂未实现"
}
// MARK: - 工具方法
/**
* 检查消息中是否包含图片
*/
private fun containsImage(messages: List<Map<String, Any>>): Boolean {
for (message in messages) {
val content = message["content"]
if (content is String && content.contains("image_url")) {
return true
}
if (content is List<*>) {
content.forEach { item ->
if (item is Map<*, *>) {
val type = item["type"] as? String
if (type == "image_url") {
return true
}
}
}
}
}
return false
}
/**
* 转换为OpenAI ChatMessage格式
*/
private fun convertToChatMessages(messages: List<Map<String, Any>>): List<ChatMessage> {
val chatMessages = mutableListOf<ChatMessage>()
for (message in messages) {
val role = message["role"] as? String ?: continue
val chatRole = when (role) {
"system" -> Role.System
"user" -> Role.User
"assistant" -> Role.Assistant
"tool" -> Role.Tool
else -> Role.User
}
// 特殊处理assistant角色的tool_calls消息
if (role == "assistant" && message.containsKey("tool_calls")) {
val content = message["content"] as? String ?: ""
// 解析tool_calls
val toolCallsList = mutableListOf<ToolCall>()
@Suppress("UNCHECKED_CAST")
val toolCallsArray = message["tool_calls"] as? List<Map<String, Any>>
toolCallsArray?.forEach { toolCallDict ->
val id = toolCallDict["id"] as? String ?: return@forEach
val type = toolCallDict["type"] as? String
if (type == "function") {
@Suppress("UNCHECKED_CAST")
val function = toolCallDict["function"] as? Map<String, Any> ?: return@forEach
val name = function["name"] as? String ?: return@forEach
val arguments = function["arguments"] as? String ?: ""
val functionCall = FunctionCall(
nameOrNull = name,
argumentsOrNull = arguments
)
val toolCall = ToolCall.Function(
id = ToolId(id),
function = functionCall
)
toolCallsList.add(toolCall)
}
}
val chatMessage = ChatMessage.Assistant(
content = if (content.isEmpty()) null else content,
toolCalls = if (toolCallsList.isEmpty()) null else toolCallsList
)
chatMessages.add(chatMessage)
continue
}
// 特殊处理tool角色消息
if (role == "tool") {
val content = message["content"] as? String ?: ""
val toolCallId = message["tool_call_id"] as? String ?: ""
val chatMessage = ChatMessage.Tool(
content = content,
toolCallId = ToolId(toolCallId)
)
chatMessages.add(chatMessage)
continue
}
// 处理普通消息的不同类型内容
val content = message["content"]
when (content) {
is String -> {
val chatMessage = when (chatRole) {
Role.System -> ChatMessage.System(content)
Role.User -> ChatMessage.User(content)
Role.Assistant -> ChatMessage.Assistant(content)
else -> ChatMessage.User(content)
}
chatMessages.add(chatMessage)
}
is List<*> -> {
// 对于多模态内容,提取文本部分(简化处理)
var textContent = ""
content.forEach { item ->
if (item is Map<*, *>) {
val type = item["type"] as? String
if (type == "text") {
val text = item["text"] as? String ?: ""
textContent += text
}
}
}
val finalContent = if (textContent.isEmpty()) "多媒体内容" else textContent
val chatMessage = when (chatRole) {
Role.System -> ChatMessage.System(finalContent)
Role.User -> ChatMessage.User(finalContent)
Role.Assistant -> ChatMessage.Assistant(finalContent)
else -> ChatMessage.User(finalContent)
}
chatMessages.add(chatMessage)
}
}
}
return chatMessages
}
/**
* 将文件转换为Base64字符串
*/
fun fileToBase64(filePath: String, maxSizeKB: Int = 20480): String? {
return try {
val file = File(filePath)
if (!file.exists() || !file.isFile) {
return null
}
// 读取文件
var bitmap = BitmapFactory.decodeFile(filePath) ?: return null
// 检查图片尺寸,限制最大为1024*1024
val maxDimension = 1024
if (bitmap.width > maxDimension || bitmap.height > maxDimension) {
// 计算缩放比例,保持纵横比
val ratio = min(maxDimension.toFloat() / bitmap.width, maxDimension.toFloat() / bitmap.height)
val newWidth = (bitmap.width * ratio).toInt()
val newHeight = (bitmap.height * ratio).toInt()
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
}
// 压缩图片
val outputStream = ByteArrayOutputStream()
var quality = 100
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
// 如果原始图片太大,继续优化文件大小
var fileSize = file.length() / 1024 // 转为KB
if (fileSize > maxSizeKB) {
val scale = sqrt(maxSizeKB.toDouble() / fileSize)
val newWidth = (bitmap.width * scale).toInt()
val newHeight = (bitmap.height * scale).toInt()
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
outputStream.reset()
quality = 85
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
}
// 转为Base64
Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
} catch (e: Exception) {
null
}
}
/**
* 创建带图片的用户消息
*/
fun createUserMessageWithImage(text: String, imageBase64: String): Map<String, Any> {
val contentArray = mutableListOf<Map<String, Any>>()
// 添加文本部分
if (text.isNotEmpty()) {
contentArray.add(
mapOf(
"type" to "text",
"text" to text
)
)
}
// 添加图片部分
contentArray.add(
mapOf(
"type" to "image_url",
"image_url" to mapOf(
"url" to "data:image/jpeg;base64,$imageBase64"
)
)
)
return mapOf(
"role" to "user",
"content" to contentArray
)
}
/**
* 从URL中提取主机信息 (保留向后兼容)
*/
private fun extractHost(url: String): String {
return try {
val javaUrl = java.net.URL(url)
javaUrl.host ?: "api.openai.com"
} catch (e: Exception) {
"api.openai.com"
}
}
/**
* 解析JSON参数 (与 iOS 版本保持一致)
*/
private fun parseJsonArguments(argumentsJson: String): Map<String, Any> {
return try {
if (argumentsJson.isBlank()) {
emptyMap()
} else {
val jsonObject = org.json.JSONObject(argumentsJson)
convertJsonObjectToMap(jsonObject)
}
} catch (e: Exception) {
emptyMap()
}
}
/**
* 转换JSONArray为List<Map<String, Any>>
*/
private fun convertJsonArrayToList(jsonArray: org.json.JSONArray): List<Map<String, Any>> {
val result = mutableListOf<Map<String, Any>>()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.optJSONObject(i) ?: continue
val map = convertJsonObjectToMap(jsonObject)
result.add(map)
}
return result
}
/**
* 转换JSONObject为Map<String, Any>
*/
private fun convertJsonObjectToMap(jsonObject: org.json.JSONObject): Map<String, Any> {
val map = mutableMapOf<String, Any>()
val keys = jsonObject.keys()
while (keys.hasNext()) {
val key = keys.next()
val value = jsonObject.get(key)
when (value) {
is org.json.JSONObject -> map[key] = convertJsonObjectToMap(value)
is org.json.JSONArray -> {
val list = mutableListOf<Any>()
for (i in 0 until value.length()) {
val item = value.get(i)
when (item) {
is org.json.JSONObject -> list.add(convertJsonObjectToMap(item))
else -> list.add(item)
}
}
map[key] = list
}
else -> map[key] = value
}
}
return map
}
/**
* 转换Map为JSONObject
*/
private fun convertMapToJsonObject(map: Map<String, Any>): org.json.JSONObject {
val jsonObject = org.json.JSONObject()
for ((key, value) in map) {
when (value) {
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
jsonObject.put(key, convertMapToJsonObject(value as Map<String, Any>))
}
is List<*> -> {
val jsonArray = org.json.JSONArray()
value.forEach { item ->
when (item) {
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
jsonArray.put(convertMapToJsonObject(item as Map<String, Any>))
}
else -> jsonArray.put(item)
}
}
jsonObject.put(key, jsonArray)
}
else -> jsonObject.put(key, value)
}
}
return jsonObject
}
}

42
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -340,17 +340,19 @@ public class SystemFunctionHandler: NSObject {
// 检查位置服务是否可用 // 检查位置服务是否可用
guard CLLocationManager.locationServicesEnabled() else { guard CLLocationManager.locationServicesEnabled() else {
continuation.resume(returning: Result(success: false, message: "位置服务未启用")) locationCompletion?(Result(success: false, message: "位置服务未启用。请前往 设置 > 隐私与安全 > 定位服务 启用定位服务"))
locationCompletion = nil
return return
} }
// 确保locationManager已初始化 // 确保locationManager已初始化
guard let locationManager = locationManager else { guard let locationManager = locationManager else {
continuation.resume(returning: Result(success: false, message: "位置管理器未初始化")) locationCompletion?(Result(success: false, message: "位置管理器未初始化"))
locationCompletion = nil
return return
} }
// 检查位置权限 - 兼容iOS 13.0+ // 检查位置权限
let authStatus: CLAuthorizationStatus let authStatus: CLAuthorizationStatus
if #available(iOS 14.0, *) { if #available(iOS 14.0, *) {
authStatus = locationManager.authorizationStatus authStatus = locationManager.authorizationStatus
@ -358,20 +360,27 @@ public class SystemFunctionHandler: NSObject {
authStatus = CLLocationManager.authorizationStatus() authStatus = CLLocationManager.authorizationStatus()
} }
print("当前位置权限状态: \(authStatus.rawValue)")
switch authStatus { switch authStatus {
case .notDetermined: case .notDetermined:
// 请求权限,权限结果会在 didChangeAuthorization 回调中处理 print("请求位置权限...")
// 请求权限
locationManager.requestWhenInUseAuthorization() locationManager.requestWhenInUseAuthorization()
// 不要在这里 return,让权限回调来处理后续逻辑 // 权限结果会在 didChangeAuthorization 回调中处理
case .denied, .restricted: case .denied:
continuation.resume(returning: Result(success: false, message: "位置权限被拒绝")) locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限"))
return locationCompletion = nil
case .restricted:
locationCompletion?(Result(success: false, message: "位置权限受限制,可能是由于家长控制或企业配置限制"))
locationCompletion = nil
case .authorizedWhenInUse, .authorizedAlways: case .authorizedWhenInUse, .authorizedAlways:
print("已有位置权限,开始获取位置...")
// 已有权限,直接请求位置 // 已有权限,直接请求位置
locationManager.requestLocation() locationManager.requestLocation()
@unknown default: @unknown default:
continuation.resume(returning: Result(success: false, message: "未知的位置权限状态")) locationCompletion?(Result(success: false, message: "未知的位置权限状态: \(authStatus.rawValue)"))
return locationCompletion = nil
} }
} }
} }
@ -404,10 +413,17 @@ extension SystemFunctionHandler: CLLocationManagerDelegate {
return return
} }
if status == .authorizedWhenInUse || status == .authorizedAlways { switch status {
case .authorizedWhenInUse, .authorizedAlways:
manager.requestLocation() manager.requestLocation()
} else if status == .denied || status == .restricted { case .denied, .restricted:
locationCompletion?(Result(success: false, message: "位置权限被拒绝")) locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限"))
locationCompletion = nil
case .notDetermined:
// 权限仍未确定,等待用户操作
break
@unknown default:
locationCompletion?(Result(success: false, message: "位置权限状态异常,请重试"))
locationCompletion = nil locationCompletion = nil
} }
} }

3
local_plugins/chat_api/pubspec.yaml

@ -34,6 +34,9 @@ flutter:
# adding or updating assets for this project. # adding or updating assets for this project.
plugin: plugin:
platforms: platforms:
android:
package: com.yunqiinnovation.chat_api
pluginClass: ChatApiPlugin
ios: ios:
pluginClass: ChatApiPlugin pluginClass: ChatApiPlugin
ffiPlugin: true ffiPlugin: true

33
local_plugins/jl_ota/.gitignore

@ -1,33 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
build/

33
local_plugins/jl_ota/.metadata

@ -1,33 +0,0 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: android
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: ios
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
local_plugins/jl_ota/CHANGELOG.md

@ -1,3 +0,0 @@
## 0.0.1
* TODO: Describe initial release.

1
local_plugins/jl_ota/LICENSE

@ -1 +0,0 @@
TODO: Add your license here.

15
local_plugins/jl_ota/README.md

@ -1,15 +0,0 @@
# jl_ota
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

4
local_plugins/jl_ota/analysis_options.yaml

@ -1,4 +0,0 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

9
local_plugins/jl_ota/android/.gitignore

@ -1,9 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

68
local_plugins/jl_ota/android/build.gradle

@ -1,68 +0,0 @@
group = "com.yunqiinnovation.jl_ota"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "1.8.22"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.7.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
namespace = "com.yunqiinnovation.jl_ota"
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
test.java.srcDirs += "src/test/kotlin"
}
defaultConfig {
minSdk = 21
}
dependencies {
// implementation project(':ble_service')
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
}
testOptions {
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}

BIN
local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.2-release.aar

Binary file not shown.

1
local_plugins/jl_ota/android/settings.gradle

@ -1 +0,0 @@
rootProject.name = 'jl_ota'

3
local_plugins/jl_ota/android/src/main/AndroidManifest.xml

@ -1,3 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.jl_ota">
</manifest>

76
local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt

@ -1,76 +0,0 @@
package com.yunqiinnovation.jl_ota
import io.flutter.embedding.engine.plugins.FlutterPlugin
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 android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.NonNull
//import com.yunqiinnovation.ble_service.BleService
/** JlOtaPlugin */
class JlOtaPlugin() : FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel: MethodChannel
private lateinit var context: Context
private lateinit var eventChannel: EventChannel
private var streamSink: EventChannel.EventSink? = null
companion object {
private const val TAG = "JlOtaPlugin"
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota"
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events"
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "jl_ota")
channel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME)
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
streamSink = events
}
override fun onCancel(arguments: Any?) {
streamSink = null
}
})
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
// BleService.otaManager?.configureOTA()
// BleService.otaManager?.startOTA()
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

38
local_plugins/jl_ota/ios/.gitignore

@ -1,38 +0,0 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

0
local_plugins/jl_ota/ios/Assets/.gitkeep

19
local_plugins/jl_ota/ios/Classes/JlOtaPlugin.swift

@ -1,19 +0,0 @@
import Flutter
import UIKit
public class JlOtaPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "jl_ota", binaryMessenger: registrar.messenger())
let instance = JlOtaPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
}

14
local_plugins/jl_ota/ios/Resources/PrivacyInfo.xcprivacy

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

29
local_plugins/jl_ota/ios/jl_ota.podspec

@ -1,29 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint jl_ota.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'jl_ota'
s.version = '0.0.1'
s.summary = 'A new Flutter project.'
s.description = <<-DESC
A new Flutter project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
# If your plugin requires a privacy manifest, for example if it uses any
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'jl_ota_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
end

8
local_plugins/jl_ota/lib/jl_ota.dart

@ -1,8 +0,0 @@
import 'jl_ota_platform_interface.dart';
class JlOta {
Future<String?> getPlatformVersion() {
return JlOtaPlatform.instance.getPlatformVersion();
}
}

17
local_plugins/jl_ota/lib/jl_ota_method_channel.dart

@ -1,17 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'jl_ota_platform_interface.dart';
/// An implementation of [JlOtaPlatform] that uses method channels.
class MethodChannelJlOta extends JlOtaPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('jl_ota');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}

29
local_plugins/jl_ota/lib/jl_ota_platform_interface.dart

@ -1,29 +0,0 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'jl_ota_method_channel.dart';
abstract class JlOtaPlatform extends PlatformInterface {
/// Constructs a JlOtaPlatform.
JlOtaPlatform() : super(token: _token);
static final Object _token = Object();
static JlOtaPlatform _instance = MethodChannelJlOta();
/// The default instance of [JlOtaPlatform] to use.
///
/// Defaults to [MethodChannelJlOta].
static JlOtaPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [JlOtaPlatform] when
/// they register themselves.
static set instance(JlOtaPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}

72
local_plugins/jl_ota/pubspec.yaml

@ -1,72 +0,0 @@
name: jl_ota
description: "A new Flutter project."
version: 0.0.1
homepage:
environment:
sdk: ^3.7.0
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.yunqiinnovation.jl_ota
pluginClass: JlOtaPlugin
ios:
pluginClass: JlOtaPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

BIN
local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

2
local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

@ -1,2 +0,0 @@
#Wed May 21 01:03:35 CST 2025
gradle.version=8.9

26
local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt

@ -1,26 +0,0 @@
package com.jieli.otasdk.model
import android.bluetooth.BluetoothDevice
/**
* @author zqjasonZhong
* @since 2022/9/9
* @email zhongzhuocheng@zh-jieli.com
* @desc 设备连接状态
*/
class DeviceConnection(val device: BluetoothDevice?, var state: Int) {
override fun hashCode(): Int {
return device.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other !is DeviceConnection) return false
return device == other.device
}
override fun toString(): String {
return "DeviceConnection(device=$device, state=$state)"
}
}

376
local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt

@ -20,129 +20,365 @@ import com.jieli.jl_bt_ota.util.CHexConver
import com.jieli.jl_bt_ota.util.JL_Log import com.jieli.jl_bt_ota.util.JL_Log
import com.jieli.otasdk.tool.bluetooth.BluetoothHelper import com.jieli.otasdk.tool.bluetooth.BluetoothHelper
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.jieli.otasdk.model.DeviceConnection
import android.util.Log import android.util.Log
import androidx.annotation.NonNull import androidx.annotation.NonNull
import com.jieli.otasdk.tool.config.ConfigHelper import com.jieli.otasdk.tool.config.ConfigHelper
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
class CustomUpdateCallback( import com.yunqiinnovation.ble_service.BleConst
import android.os.Handler
import android.os.Looper
val device: BluetoothDevice? import java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile
import io.flutter.plugin.common.EventChannel
class CustomUpdateCallback(
val device: BluetoothDevice?,
private val streamSink: EventChannel.EventSink?
) : IUpgradeCallback { ) : IUpgradeCallback {
companion object { companion object {
private const val TAG = "JlOtaPlugin" private const val TAG = "JlOtaPlugin"
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" private const val CHANNEL_NAME = "com.example.ota"
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" private const val EVENT_CHANNEL_NAME = "com.example.ota/events"
} }
override fun onStartOTA() { override fun onStartOTA() {
Log.d(TAG, "onStartOTA开始升级") Log.d(TAG, "onStartOTA开始升级")
// viewModel.otaStateMLD.value = OTAStart(device) if (streamSink != null) {
if (Looper.myLooper() == Looper.getMainLooper()) {
streamSink.success(
mapOf(
"event" to "onStartOTA"
)
)
}
}
} }
override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) { override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) {
Log.d(TAG, "onNeedReconnect需要重新链接") Log.d(TAG, "onNeedReconnect需要重新链接")
if (streamSink != null) {
if (Looper.myLooper() == Looper.getMainLooper()) {
streamSink.success(
mapOf(
"event" to "onNeedReconnect",
"addr" to addr,
"isNewReconnectWay" to isNewReconnectWay
)
)
}
}
} }
override fun onProgress(type: Int, progress: Float) { override fun onProgress(type: Int, progress: Float) {
Log.d(TAG, "onProgress正在进展") Log.d(TAG, "onProgress正在进展")
//viewModel.otaStateMLD.value = OTAWorking(device, type, progress) if (streamSink != null) {
if (Looper.myLooper() == Looper.getMainLooper()) {
streamSink.success(
mapOf(
"event" to "onProgress",
"type" to type,
"progress" to progress
)
)
} else {
// Handler(Looper.getMainLooper()).post {
// streamSink.success(
// mapOf(
// "event" to "onDecodeStream"
// //"data" to data
// )
// )
// }
}
}
} }
override fun onStopOTA() { override fun onStopOTA() {
Log.d(TAG, "onStartOTA停止升级") Log.d(TAG, "onStartOTA停止升级")
if (streamSink != null) {
if (Looper.myLooper() == Looper.getMainLooper()) {
streamSink.success(
mapOf(
"event" to "onStopOTA",
)
)
}
}
BleService.isEnterOta = false
} }
override fun onCancelOTA() { override fun onCancelOTA() {
Log.d(TAG, "onCancelOTA取消升级") Log.d(TAG, "onCancelOTA取消升级")
if (streamSink != null) {
if (Looper.myLooper() == Looper.getMainLooper()) {
streamSink.success(
mapOf(
"event" to "onCancelOTA",
)
)
}
}
} }
override fun onError(error: BaseError?) { override fun onError(error: BaseError?) {
val errorMessage = error?.message ?: "Unknown error" val errorMessage = error?.message ?: "Unknown error"
Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}") Log.d(TAG, "升级失败: ${error?.message ?: "未知错误"}")
if (streamSink != null) {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.d(TAG, "升级失败:error?.message ${error?.message}未知错误 error?.code${error?.code}")
streamSink.success(
mapOf(
"event" to "onError",
"code" to (error?.code ?: 255), // 确保值是正确的类型
"message" to (error?.message ?: "Unknown error") // 确保值是正确的类型
)
)
}
}
// 如果需要打印完整错误对象信息(假设BaseError有重写toString()) // 如果需要打印完整错误对象信息(假设BaseError有重写toString())
Log.d(TAG, "完整错误对象: ${error ?: "null"}") Log.d(TAG, "完整错误对象: ${error ?: "null"}")
} }
} }
/** OtaPlugin */ /** OtaPlugin */
class OtaPlugin: FlutterPlugin, MethodCallHandler { class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android /// The MethodChannel that will the communication between Flutter and native Android
/// ///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it /// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity /// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel private lateinit var channel: MethodChannel
private lateinit var context: Context private lateinit var context: Context
private lateinit var otaDevice: BluetoothDevice
private lateinit var bluetoothHelper: BluetoothHelper
private lateinit var otaManager: OTAManager
private lateinit var filePath: String//升级文件路径
private var isDisconnect = false//是否主动断开
private var streamSink: EventChannel.EventSink? = null
private lateinit var eventChannel: EventChannel
private lateinit var bluetoothHelper : BluetoothHelper // 是否已注册BLE回调
val otaConnectionMLD = MutableLiveData<DeviceConnection>() private var isRegisteredToBle = false
val mandatoryUpgradeMLD = MutableLiveData<BluetoothDevice>()
private lateinit var otaManager : OTAManager
private val bluetoothCallback = object : BtEventCallback() { // 添加主线程Handler用于确保在主线程回调
private val mainHandler = Handler(Looper.getMainLooper())
companion object {
private const val TAG = "OtaPlugin"
private const val CHANNEL_NAME = "com.example.ota"
private const val EVENT_CHANNEL_NAME = "com.example.ota/events"
}
// 在主线程上安全发送事件
private fun sendEventOnMainThread(event: Map<String, Any?>) {
if (streamSink == null) return
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接发送
streamSink?.success(event)
} else {
// 否则切换到主线程
mainHandler.post {
streamSink?.success(event)
}
}
}
// 在主线程上安全返回结果
private fun sendResultOnMainThread(result: Result, value: Any?) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.success(value)
} else {
// 否则切换到主线程
mainHandler.post {
result.success(value)
}
}
}
// 在主线程上安全返回错误
private fun sendErrorOnMainThread(
result: Result,
errorCode: String,
errorMessage: String,
errorDetails: Any?
) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.error(errorCode, errorMessage, errorDetails)
} else {
// 否则切换到主线程
mainHandler.post {
result.error(errorCode, errorMessage, errorDetails)
}
}
}
private val bluetoothCallback = object : BtEventCallback() {
override fun onConnection(device: BluetoothDevice?, status: Int) { override fun onConnection(device: BluetoothDevice?, status: Int) {
if ((status == StateCode.CONNECTION_FAILED || status == StateCode.CONNECTION_DISCONNECT) && bluetoothHelper.getConnectedDevice() != null) { if ((status == StateCode.CONNECTION_FAILED || status == StateCode.CONNECTION_DISCONNECT) && bluetoothHelper.getConnectedDevice() != null) {
bluetoothHelper.disconnectDevice(bluetoothHelper.getConnectedDevice()) bluetoothHelper.disconnectDevice(bluetoothHelper.getConnectedDevice())
}
otaDevice = device!!
// sendEventOnMainThread(
// mapOf(
// "event" to "NOCONNECTION"
// )
// )
// 更新连接状态的 LiveData,用于通知 UI 或其他逻辑
// otaConnectionMLD.value = DeviceConnection(device, status)
Log.d("bluetoothCallback", "onConnection,status${status}")
if (status == StateCode.CONNECTION_OK) {
Log.d("TAG", "startUpgrade")
sendEventOnMainThread(
mapOf(
"event" to "CONNECTION_SUCCESS"
)
)
Log.d("TAG", "startUpgrade,filePath=${filePath}")
otaManager.bluetoothOption.firmwareFilePath = filePath
otaManager.startOTA(
CustomUpdateCallback(
otaManager.bluetoothHelper.getConnectedDevice(), streamSink
)
)
} }
otaConnectionMLD.value = DeviceConnection(device, status)
} }
override fun onMandatoryUpgrade(device: BluetoothDevice?) { override fun onMandatoryUpgrade(device: BluetoothDevice?) {
mandatoryUpgradeMLD.postValue(device!!) // mandatoryUpgradeMLD.postValue(device!!)
Log.d("bluetoothCallback", "onMandatoryUpgrade")
} }
} }
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "ota") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "ota")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME)
Log.d("TAG", "onAttachedToEngine") eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
} override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
streamSink = events
}
override fun onMethodCall(call: MethodCall, result: Result) { override fun onCancel(arguments: Any?) {
if (call.method == "getPlatformVersion") { streamSink = null
Log.d("TAG", "Connection") }
// otaManager.configHelper })
// 确保先初始化 ConfigHelper // 初始化 otaManager 和 bluetoothHelper
ConfigHelper.initialize(context) ConfigHelper.initialize(context)
otaManager = OTAManager(context) otaManager = OTAManager(context)
bluetoothHelper = BluetoothHelper(context) bluetoothHelper = BluetoothHelper(context)
// 如果需要额外的初始化逻辑,请在这里添加 // 如果需要额外的初始化逻辑,请在这里添加
otaManager.registerBluetoothCallback(bluetoothCallback) otaManager.registerBluetoothCallback(bluetoothCallback)
BleService.addCallback(this)
// otaManager.registerBluetoothCallback(bluetoothCallback) isRegisteredToBle = true
result.success("Android ${android.os.Build.VERSION.RELEASE}") Log.i("TAG", "onAttachedToEngine")
}else if (call.method == "searchDevices") {
Log.d("TAG", "searchDevices") }
// otaManager.configHelper
BleService.disconnect() override fun onMethodCall(call: MethodCall, result: Result) {
// 确保先初始化 ConfigHelper if (call.method == "startUpgrading") {
otaManager.bluetoothHelper.startScan(BleService.bluetoothGatt?.device) Log.d("TAG", "startUpgrading")
// 接收传入的参数
// otaManager.registerBluetoothCallback(bluetoothCallback) val arguments = call.arguments as? Map<String, Any>
result.success("Android ${android.os.Build.VERSION.RELEASE}") filePath = (arguments?.get("filePath") as? String).toString()
} else if (call.method == "startUpgrade") {
Log.d("TAG", "startUpgrade") if (filePath != null) {
var filePath = "/storage/emulated/0/Download/JieLiOTA/upgrade/update - v0.1.0_添加杰理OTA,优化主从切换拾音丢失问题.ufw" Log.d("TAG", "Received filePath: $filePath")
otaManager.bluetoothOption.firmwareFilePath = filePath // 在这里使用 filePath 参数,例如传递给其他方法或类
otaManager.startOTA( } else {
CustomUpdateCallback( Log.d("TAG", "filePath 参数为空")
otaManager.bluetoothHelper.getConnectedDevice() }
) BleService.isEnterOta = true
)
result.success("Android ${android.os.Build.VERSION.RELEASE}") val bluetoothGatt = BleService.bluetoothGatt
} else { if (bluetoothGatt?.device != null) {
result.notImplemented() isDisconnect = true
} otaDevice = bluetoothGatt.device
} BleService.disconnect()
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { } else if(otaDevice != null)
channel.setMethodCallHandler(null) { isDisconnect = true
} BleService.disconnect()
}
else{
Log.e("TAG", "BluetoothGatt 或 device 为 null,无法执行操作")
sendEventOnMainThread(
mapOf(
"event" to "NOCONNECTION"
)
)
BleService.isEnterOta = false
}
} else if (call.method == "onOtaClose") {
BleService.isEnterOta = false
isDisconnect = false
otaManager.bluetoothHelper.stopScan()
otaManager.bluetoothHelper.disconnectDevice(otaDevice)
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
// 取消BLE回调注册
if (isRegisteredToBle) {
BleService.removeCallback(this)
isRegisteredToBle = false
}
isDisconnect = false
// 释放其他资源
streamSink = null
BleService.isEnterOta = false
Log.d(TAG, "OtaPlugin 已释放资源")
}
override fun onScanResult(devices: List<Map<String, Any>>) {
// 不处理扫描结果
}
override fun onConnectionStateChanged(state: Int) {
if (state == BleConst.STATE_DISCONNECTED && isDisconnect) {
Log.d("TAG", "Ble设备已断开,onConnectionStateChanged")
Handler(Looper.getMainLooper()).postDelayed({
otaManager.bluetoothHelper.startScan(otaDevice)
}, 2000) // 2000 毫秒 = 2 秒
}
}
override fun onAudioDataReceived(data: ByteArray) {
// 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别
*/
override fun onWakeupSignalReceived() {
}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
// 不处理设备信息
}
} }

43
local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt

@ -16,19 +16,41 @@ import com.jieli.otasdk.tool.config.ConfigHelper
import com.jieli.otasdk.tool.bluetooth.OtaConstant import com.jieli.otasdk.tool.bluetooth.OtaConstant
import java.util.* import java.util.*
import android.util.Log import android.util.Log
/** /**
* 用于RCSP的第三方SDK接入OTA流程 * 用于RCSP的第三方SDK接入OTA流程
*/ */
class OTAManager(context: Context) : BluetoothOTAManager(context) { class OTAManager(context: Context) : BluetoothOTAManager(context) {
val configHelper = ConfigHelper.getInstance() val configHelper = ConfigHelper.getInstance()
val bluetoothHelper = BluetoothHelper.getInstance(context) val bluetoothHelper = BluetoothHelper.getInstance(context)
/**
* 转换成OTA库的连接状态
*
* @param status 连接状态
* @return 库的连接状态
*/
fun changeConnectStatus(status: Int): Int {
var changeStatus = StateCode.CONNECTION_DISCONNECT
when (status) {
2 -> {
changeStatus = StateCode.CONNECTION_OK
}
1 -> {
changeStatus = StateCode.CONNECTION_CONNECTING
}
}
return changeStatus
}
val btEventCallback = object : OnBTEventCallback() { val btEventCallback = object : OnBTEventCallback() {
override fun onDeviceConnection(device: BluetoothDevice?, way: Int, status: Int) { override fun onDeviceConnection(device: BluetoothDevice?, way: Int, status: Int) {
super.onDeviceConnection(device, way, status) super.onDeviceConnection(device, way, status)
// val connectionState = AppUtil.changeConnectStatus(status) val connectionState = changeConnectStatus(status)
// Log.i( // Log.i(
// TAG, "onConnection >>> device : ${printDeviceInfo(device)}, way = $way" + // TAG, "onConnection >>> device : ${printDeviceInfo(device)}, way = $way" +
// " status :$status, change status : $connectionState" // " status :$status, change status : $connectionState"
@ -37,9 +59,9 @@ class OTAManager(context: Context) : BluetoothOTAManager(context) {
//注释掉 //注释掉
Log.i( Log.i(
TAG, "onConnection >>> device : ${device}, way = $way" + TAG, "onConnection >>> device : ${device}, way = $way" +
" status :$status, change status : 3" " status :$status, change status : $connectionState"
) )
onBtDeviceConnection(device, 1) onBtDeviceConnection(device, connectionState)
} }
override fun onReceiveData( override fun onReceiveData(
@ -70,7 +92,7 @@ class OTAManager(context: Context) : BluetoothOTAManager(context) {
} }
init { init {
// ConfigHelper.initialize(context) // ConfigHelper.initialize(context)
val bluetoothOption = BluetoothOTAConfigure() val bluetoothOption = BluetoothOTAConfigure()
//选择通讯方式 //选择通讯方式
bluetoothOption.priority = bluetoothOption.priority =
@ -79,9 +101,10 @@ class OTAManager(context: Context) : BluetoothOTAManager(context) {
//是否需要自定义回连方式(默认不需要,如需要自定义回连方式,需要客户自行实现) //是否需要自定义回连方式(默认不需要,如需要自定义回连方式,需要客户自行实现)
bluetoothOption.isUseReconnect = bluetoothOption.isUseReconnect =
(configHelper.isUseCustomReConnectWay() && configHelper.isHidDevice()) (configHelper.isUseCustomReConnectWay() && configHelper.isHidDevice())
Log.d( Log.d(
TAG, "是否启用设备认证流程(与固件工程师确认) >>> ${configHelper.isUseDeviceAuth()} >>> ${bluetoothOption.isUseReconnect} " TAG,
) "是否启用设备认证流程(与固件工程师确认) >>> ${configHelper.isUseDeviceAuth()} >>> ${bluetoothOption.isUseReconnect} "
)
//是否启用设备认证流程(与固件工程师确认) //是否启用设备认证流程(与固件工程师确认)
bluetoothOption.isUseAuthDevice = configHelper.isUseDeviceAuth() bluetoothOption.isUseAuthDevice = configHelper.isUseDeviceAuth()
//设置BLE的MTU //设置BLE的MTU

11
local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java

@ -88,7 +88,7 @@ public class BleManager {
private volatile boolean isBleScanning; private volatile boolean isBleScanning;
private NotifyCharacteristicRunnable mNotifyCharacteristicRunnable; private NotifyCharacteristicRunnable mNotifyCharacteristicRunnable;
private BluetoothDevice deviced;//需要连接手机设备信息 private BluetoothDevice deviced1;//需要连接手机设备信息
private long timeout = 15000;//扫描设备时间 private long timeout = 15000;//扫描设备时间
private int mRetryConnectCount = 0;//连接失败后尝试连接次数 private int mRetryConnectCount = 0;//连接失败后尝试连接次数
private long startConnectTime = 0; //开始连接时间戳 private long startConnectTime = 0; //开始连接时间戳
@ -311,7 +311,8 @@ public class BleManager {
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
public boolean startLeScan(BluetoothDevice deviced) { public boolean startLeScan(BluetoothDevice deviced) {
Log.i(TAG, ", timeout = " + timeout); deviced1=deviced;
Log.i(TAG, ", deviced = " + deviced);
if (!isBluetoothEnable() || !isLocationPermissionGranted(mContext)) { if (!isBluetoothEnable() || !isLocationPermissionGranted(mContext)) {
checkPermissionsAndRequest(mContext); checkPermissionsAndRequest(mContext);
return false; return false;
@ -632,7 +633,8 @@ public class BleManager {
&& !mDiscoveredBleDevices.contains(device)) { && !mDiscoveredBleDevices.contains(device)) {
Log.d(TAG, "notify device : " + printDeviceInfo(device)); Log.d(TAG, "notify device : " + printDeviceInfo(device));
Log.d(TAG, "device.getAddress : " + device.getAddress()); Log.d(TAG, "device.getAddress : " + device.getAddress());
if ("4D:9B:09:38:22:F5".equals(device.getAddress())) { Log.d(TAG, "deviced1.getAddress() : " + deviced1.getAddress());
if (deviced1 != null && deviced1.getAddress().equals(device.getAddress())) {
// 匹配成功 // 匹配成功
Log.i(TAG, "filterDevice找到了DeapSound设备"); Log.i(TAG, "filterDevice找到了DeapSound设备");
@ -644,9 +646,10 @@ public class BleManager {
} else { } else {
Log.e(TAG, "连接设备失败: ${device.name} (${device.address})"); Log.e(TAG, "连接设备失败: ${device.name} (${device.address})");
} }
}
mDiscoveredBleDevices.add(device); mDiscoveredBleDevices.add(device);
handleDiscoveryBle(device, new BleScanInfo().setRawData(scanRecord).setRssi(rssi).setEnableConnect(isBleEnableConnect)); handleDiscoveryBle(device, new BleScanInfo().setRawData(scanRecord).setRssi(rssi).setEnableConnect(isBleEnableConnect));
}
} }
} }

139
local_plugins/ota/lib/ota.dart

@ -1,19 +1,142 @@
import 'ota_platform_interface.dart'; import 'ota_platform_interface.dart';
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
class OtaEvent {
static final int NOT_STARTED = -1; // 未开始
static final int VALIDATION_FILE = 0; // 验证文件
static final int ONUPGRADE = 1; // 升级中
static final int START_UPGRADING = 2; // 开始升级
static final int UPGRADE_SUCCESS = 3; // 升级成功
static final int UPGRADE_FAILED = 4; // 升级失败
static final int CONNECTION_DISCONNECTION = 5; // 连接断开
static final int CONNECTION_SUCCESS = 6; // 连接成功
/// 事件类型
final String event;
/// 输出升级状态
final int? type;
/// 输出升级进度
final double? progress;
/// 错误码 (仅在发生错误时有效)
final int? code;
/// 错误信息 (仅在发生错误时有效)
final String? message;
/// 构造函数
OtaEvent({
required this.event,
this.type,
this.progress,
this.code,
this.message,
});
/// 从Map构造对象
factory OtaEvent.fromMap(Map<dynamic, dynamic> map) {
return OtaEvent(
event: map['event'],
type: map['type'],
progress: map['progress'],
code: map['code'],
message: map['message'],
);
}
}
class Ota { class Ota {
Future<String?> getPlatformVersion() { // 单例实例
return OtaPlatform.instance.getPlatformVersion(); static final Ota _instance = Ota._internal();
// 私有构造函数
Ota._internal() {
// 初始化时设置事件监听
// _setupEventListener();
} }
Future<String?> searchDevices() { // 公共访问点
return OtaPlatform.instance.searchDevices(); static Ota get instance => _instance;
static const EventChannel _eventChannel = EventChannel(
'com.example.ota/events',
);
// double progress = 0.0;
// late int type = OtaEvent.NOT_STARTED;
// bool isUpgrade = false;
/// 事件流
Stream<OtaEvent>? _eventStream;
// bool _isEventListenerSet = false; // 防止重复监听
/// 获取事件流
Stream<OtaEvent> get eventStream {
_eventStream ??= _eventChannel.receiveBroadcastStream().map((event) {
if (event is Map) {
return OtaEvent.fromMap(event);
}
throw PlatformException(code: 'INVALID_EVENT', message: '无效的事件数据');
});
return _eventStream!;
} }
Future<String?> connectDevice() { /// 释放资源
return OtaPlatform.instance.connectDevice(); void onClose() {
onOtaClose();
// 停止事件监听
_eventStream = null;
//_isEventListenerSet = false;
print("Ota 对象资源已释放");
}
// /// 内部设置事件监听(只执行一次)
// void _setupEventListener() {
// if (_isEventListenerSet) return;
// eventStream.listen((event) {
// if (event.event == 'onProgress' &&
// event.type != null &&
// event.progress != null) {
// print("flutter 收到数据${event.progress}type=${type}");
// progress = event.progress!;
// type = event.type!;
// }
// if (event.event == 'onStartOTA') {
// type = OtaEvent.START_UPGRADING;
// isUpgrade = true; // 开始升级
// }
// if (event.event == 'onStopOTA') {
// type = OtaEvent.UPGRADE_SUCCESS;
// isUpgrade = false;
// }
// if (event.event == 'onError' &&
// event.code != null &&
// event.message != null) {
// print("flutter onError${event.code}type=${event.message}");
// type = OtaEvent.UPGRADE_FAILED;
// isUpgrade = false;
// }
// if (event.event == 'NOCONNECTION') {
// type = OtaEvent.CONNECTION_DISCONNECTION;
// isUpgrade = false;
// }
// if (event.event == 'CONNECTION_SUCCESS') {
// type = OtaEvent.CONNECTION_SUCCESS;
// }
// });
// _isEventListenerSet = true;
// }
Future<String?> startUpgrading(String? filePath) {
return OtaPlatform.instance.startUpgrading(filePath);
} }
Future<String?> startUpgrade() { Future<String?> onOtaClose() {
return OtaPlatform.instance.startUpgrade(); return OtaPlatform.instance.onOtaClose();
} }
} }

20
local_plugins/ota/lib/ota_method_channel.dart

@ -10,25 +10,17 @@ class MethodChannelOta extends OtaPlatform {
final methodChannel = const MethodChannel('ota'); final methodChannel = const MethodChannel('ota');
@override @override
Future<String?> getPlatformVersion() async { Future<String?> startUpgrading(String? filePath) async {
final version = await methodChannel.invokeMethod<String>( final version = await methodChannel.invokeMethod<String>(
'getPlatformVersion', 'startUpgrading',
{'filePath': filePath}, // 传递参数
); );
return version; return version;
} }
Future<String?> searchDevices() async { @override
final version = await methodChannel.invokeMethod<String>('searchDevices'); Future<String?> onOtaClose() async {
return version; final version = await methodChannel.invokeMethod<String>('onOtaClose');
}
Future<String?> connectDevice() async {
final version = await methodChannel.invokeMethod<String>('connectDevice');
return version;
}
Future<String?> startUpgrade() async {
final version = await methodChannel.invokeMethod<String>('startUpgrade');
return version; return version;
} }
} }

12
local_plugins/ota/lib/ota_platform_interface.dart

@ -23,19 +23,11 @@ abstract class OtaPlatform extends PlatformInterface {
_instance = instance; _instance = instance;
} }
Future<String?> getPlatformVersion() { Future<String?> startUpgrading(String? filePath) {
throw UnimplementedError('platformVersion() has not been implemented.'); throw UnimplementedError('platformVersion() has not been implemented.');
} }
Future<String?> searchDevices() { Future<String?> onOtaClose() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
Future<String?> connectDevice() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
Future<String?> startUpgrade() {
throw UnimplementedError('platformVersion() has not been implemented.'); throw UnimplementedError('platformVersion() has not been implemented.');
} }
} }

1
local_plugins/ota/pubspec.yaml

@ -11,7 +11,6 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
plugin_platform_interface: ^2.0.2 plugin_platform_interface: ^2.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

2
pubspec.yaml

@ -79,8 +79,6 @@ dependencies:
path: local_plugins/spotify_sdk path: local_plugins/spotify_sdk
ota: ota:
path: local_plugins/ota path: local_plugins/ota
jl_ota:
path: local_plugins/jl_ota
ble_service: ble_service:
path: local_plugins/ble_service path: local_plugins/ble_service
classic_bluetooth: classic_bluetooth:

Loading…
Cancel
Save