From aa957bdc9516724359fe85196c8432905be9435b Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 12 Jun 2025 21:39:36 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E4=B8=8A=E4=BC=A0android=20=E7=9A=84mcp?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E5=92=8C=E5=BC=82=E5=B8=B8=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat_api/ChatApiService.kt | 32 ++++++++----------- .../chat_api/CustomSseClientTransport.kt | 4 ++- .../yunqiinnovation/chat_api/MCPSubClient.kt | 6 ++-- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 190e2a1f0..150321d7e 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -333,7 +333,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor chatsFlow.collect { result -> if (isCanceled) return@collect - val choice = result.choices?.firstOrNull() + val choice = result.choices.firstOrNull() if (choice == null) { Log.e("ChatApiService", "choice为null") return@collect @@ -352,33 +352,35 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 收集工具调用信息 delta.toolCalls?.forEach { toolCall -> + val id = toolCall.id ?: run { + Log.e("ChatApiService", "toolCall.id 为空,跳过本次处理") + return@forEach + } + Log.e("ChatApiService", "AI发起工具调用: ${toolCall.id} ${toolCall.index} ${toolCall.function?.name} ${toolCall.function?.arguments}") val index = toolCall.index // 创建或获取现有的工具调用信息 val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() } - // 安全处理工具调用ID try { - toolCall.id?.let { id -> - toolCallInfo.id = id.toString() + toolCall.id?.let { i -> + toolCallInfo.id = i.toString() } } catch (e: Exception) { Log.e("ChatApiService", "处理工具调用ID异常: ${e.message}") } - // 安全处理函数信息 try { toolCall.function?.let { function -> try { - function.name?.let { name -> + function.name.let { name -> toolCallInfo.name = name } } catch (e: Exception) { - Log.e("ChatApiService", "处理工具调用函数名称异常: ${e.message}") + Log.e("ChatApiService", "处理工具调用函数名称异常! function:${toolCall.function} err:${e.message}") } - try { - function.arguments?.let { args -> + function.arguments.let { args -> toolCallInfo.arguments += args } } catch (e: Exception) { @@ -417,11 +419,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor Log.e("ChatApiService", "查找有效工具调用异常: ${e.message}") null } - if (firstToolCall == null) { return false } - // 创建函数调用字典 val functionCall = try { mapOf( @@ -433,10 +433,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor Log.e("ChatApiService", "创建函数调用字典异常: ${e.message}") return false } - // 通知上层工具调用事件 streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall)) - // 在后台队列处理工具调用 launch { try { @@ -444,14 +442,13 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 通过MCP客户端处理工具调用 val functionName = firstToolCall.name val argumentsJson = firstToolCall.arguments - val result = if (_mcpClient?.hasToolWithName(functionName) == true) { // 解析参数 val arguments = _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap() // 调用MCP工具 val toolResult = _mcpClient?.callTool(functionName, arguments) - + Log.e("ChatApiService", "AI调用工具结果: $functionName}:$arguments} toolResult:${toolResult}") // 处理结果 if (toolResult != null) { if (toolResult["isError"] == true) { @@ -478,14 +475,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 工具不存在 mapOf("context" to "Tool not found: $functionName") } - if (!isCanceled) { // 处理结果 streamCallback?.onFunctionCallResult( convertMapToJsonObject(functionCall), convertMapToJsonObject(result) ) - // 将结果发送回OpenAI继续对话 sendFunctionCallResultInternal( messages = currentMessages, @@ -855,12 +850,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor nameOrNull = name, argumentsOrNull = arguments ) - + val toolCall = ToolCall.Function( id = ToolId(id), function = functionCall ) - toolCallsList.add(toolCall) } } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt index 14777d02a..f4bf2de9b 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt @@ -107,7 +107,9 @@ class CustomSseClientTransport( "open" -> { // SSE连接已打开 } - + "ping" -> { + // 心跳 + } "endpoint" -> { try { val eventData = event.data ?: "" diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt index 3290c2297..a332689e2 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt @@ -77,11 +77,11 @@ class MCPSubClient( } // 连接到服务器 - client?.connect(transport) + client.connect(transport) // 获取可用工具列表 try { - val toolsResult = client?.listTools() + val toolsResult = client.listTools() if (toolsResult != null) { availableTools.clear() val filtered = toolsResult.tools.filter { tool -> @@ -101,7 +101,7 @@ class MCPSubClient( retryCount = 0 currentReconnectDelay = initialReconnectDelay // 启动心跳检测 - startHeartbeat() +// startHeartbeat() true } catch (e: Exception) { From 438f8d1e2d394972b768f79037a8fad5127309f5 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 13 Jun 2025 12:06:16 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BF=AE=E6=94=B9ota=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/ota_upgrade_controller.dart | 7 +- .../ota/ios/Classes/BleManager/JLBleManager.h | 3 + .../ota/ios/Classes/BleManager/JLBleManager.m | 89 ++++++++++++++++--- local_plugins/ota/ios/Classes/OtaPlugin.swift | 35 ++++++-- local_plugins/ota/lib/ota.dart | 4 + local_plugins/ota/lib/ota_method_channel.dart | 5 ++ .../ota/lib/ota_platform_interface.dart | 4 + 7 files changed, 126 insertions(+), 21 deletions(-) diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index 63536be30..25e6188ce 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -58,6 +58,7 @@ class OtaUpgradeController extends GetxController { final RxDouble progress = 0.0.obs; // 控制器自己的可观察属性 final RxInt type = OtaEvent.NOT_STARTED.obs; final RxBool isUpgrade = false.obs; + @override void onInit() { super.onInit(); @@ -156,7 +157,7 @@ class OtaUpgradeController extends GetxController { final firstPart = firmwareParts.sublist(0, 3).join('.'); final secondPart = firmwareParts.sublist(3).join('.'); print("firstPart${firstPart},secondPart=${secondPart}"); -// 判断两部分是否一致 + // 判断两部分是否一致 if (firstPart != secondPart) { throw "固件版本格式错误:${deviceInfo.firmwareVersion}"; } @@ -164,6 +165,9 @@ class OtaUpgradeController extends GetxController { // 正常流程 currentVersion.value = firstPart; print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); + + jlota.init(); + // API请求 final response = await Api.getequipmentinfo({ "id": deviceInfo.firmwareProductId, @@ -402,6 +406,7 @@ class OtaUpgradeController extends GetxController { if (event.event == 'onError') { type.value = OtaEvent.UPGRADE_FAILED; isUpgrade.value = false; + Get.snackbar('升级失败', '请检查设备连接或升级包'); } if (event.event == 'NOCONNECTION') { type.value = OtaEvent.CONNECTION_DISCONNECTION; diff --git a/local_plugins/ota/ios/Classes/BleManager/JLBleManager.h b/local_plugins/ota/ios/Classes/BleManager/JLBleManager.h index f768de83a..b72e4247f 100644 --- a/local_plugins/ota/ios/Classes/BleManager/JLBleManager.h +++ b/local_plugins/ota/ios/Classes/BleManager/JLBleManager.h @@ -15,6 +15,7 @@ #import #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -63,6 +64,8 @@ extern NSString *kFLT_BLE_DISCONNECTED; //断开连接 @property (strong, nonatomic) JLBleEntity *__nullable currentEntity; +@property (nonatomic, copy, nullable) FlutterEventSink flutterEventSink; + /** * 单例 */ diff --git a/local_plugins/ota/ios/Classes/BleManager/JLBleManager.m b/local_plugins/ota/ios/Classes/BleManager/JLBleManager.m index 1fd281682..a7087124e 100644 --- a/local_plugins/ota/ios/Classes/BleManager/JLBleManager.m +++ b/local_plugins/ota/ios/Classes/BleManager/JLBleManager.m @@ -206,8 +206,17 @@ NSString *FLT_BLE_RCSP_R = @"AE02"; //命令“读”通道 if (ble_name.length == 0) return; kJLLog(JLLOG_DEBUG, @"发现 ----> NAME:%@ RSSI:%@ AD:%@", ble_name,RSSI,ble_AD); - - [self addPeripheral:peripheral RSSI:RSSI Name:ble_name Info:ble_AD]; + + if (ble_AD.length >= 29) { + const uint8_t *bytes = ble_AD.bytes; + NSMutableString *macStr = [NSMutableString string]; + for (int i = 7; i <= 12; i++) { + uint8_t macByte = bytes[i] ^ 0x5A; + [macStr appendFormat:@"%02X", macByte]; + if (i < 12) [macStr appendString:@":"]; + } + [self addPeripheral:peripheral Info:macStr]; + } [[NSNotificationCenter defaultCenter] postNotificationName:kFLT_BLE_FOUND object:_blePeripheralArr userInfo:nil]; @@ -217,11 +226,11 @@ NSString *FLT_BLE_RCSP_R = @"AE02"; //命令“读”通道 } } -- (void)addPeripheral:(CBPeripheral*)peripheral RSSI:(NSNumber *)rssi Name:(NSString*)name Info:(NSData*)blead{ -// if ([name isEqualToString:@"DeapSound"]) { -// [self stopScanBLE]; -// [self connectBLE:peripheral]; -// } +- (void)addPeripheral:(CBPeripheral*)peripheral Info:(NSString*)macStr { + if (self.lastAddress && [macStr isEqualToString:self.lastAddress]) { + [self stopScanBLE]; + [self connectBLE:peripheral]; + } } #pragma mark 设备连接回调 @@ -349,13 +358,7 @@ NSString *FLT_BLE_RCSP_R = @"AE02"; //命令“读”通道 if (needForcedUpgrade) { kJLLog(JLLOG_DEBUG, @"设备需要强制升级,请到升级界面选择ota升级文件进行升级!"); } - NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; - NSString *otaFilePath = [documentsPath stringByAppendingPathComponent:@"update030.ufw"]; - if ([[NSFileManager defaultManager] fileExistsAtPath:otaFilePath]) { - [self otaFuncWithFilePath:otaFilePath]; - } else { - kJLLog(JLLOG_DEBUG, @"OTA文件不存在: %@", otaFilePath); - } + [self otaFuncWithFilePath:self.lastFilePath]; }]; }); } @@ -396,6 +399,15 @@ NSString *FLT_BLE_RCSP_R = @"AE02"; //命令“读”通道 */ - (void)otaFuncWithFilePath:(NSString *)otaFilePath { kJLLog(JLLOG_DEBUG, @"current otaFilePath ---> %@", otaFilePath); + if (self.flutterEventSink) { + self.flutterEventSink(@{ + @"event": @"onStartOTA", + @"type": @0, + @"progress": @0.0, + @"code": @8, + @"message": @"开始升级" + }); + } self.selectedOtaFilePath = otaFilePath; NSData *otaData = [[NSData alloc] initWithContentsOfFile:otaFilePath]; @@ -425,6 +437,55 @@ NSString *FLT_BLE_RCSP_R = @"AE02"; //命令“读”通道 -(void)otaUpgradeResult:(JL_OTAResult)result Progress:(float)progress{ //TODO: 设备升级过程回调,包括进度状态 kJLLog(JLLOG_DEBUG, @"otaUpgradeResult ---> %u", result); + if (result == JL_OTAResultPreparing) { + if (self.flutterEventSink) { + self.flutterEventSink(@{ + @"event": @"onProgress", + @"type": @0, + @"progress": @(progress * 100), + @"code": @(result), + @"message": @"开始升级" + }); + } + } else if (result == JL_OTAResultUpgrading) { + if (self.flutterEventSink) { + self.flutterEventSink(@{ + @"event": @"onProgress", + @"type": @1, + @"progress": @(progress * 100), + @"code": @(result), + @"message": @"开始升级" + }); + } + } else if (result == JL_OTAResultPrepared) { + + } else if (result == JL_OTAResultReconnect) { + + } else if (result == JL_OTAResultReconnectWithMacAddr) { + + } else if (result == JL_OTAResultSuccess) { + if (self.flutterEventSink) { + self.flutterEventSink(@{ + @"event": @"onStopOTA", + @"type": @0, + @"progress": @0.0, + @"code": @(result), + @"message": @"升级成功" + }); + } + } else if (result == JL_OTAResultReboot) { + + } else { + if (self.flutterEventSink) { + self.flutterEventSink(@{ + @"event": @"onError", + @"type": @0, + @"progress": @0.0, + @"code": @(result), + @"message": @"升级失败" + }); + } + } } -(void)otaDataSend:(NSData *)data{ diff --git a/local_plugins/ota/ios/Classes/OtaPlugin.swift b/local_plugins/ota/ios/Classes/OtaPlugin.swift index 7286ad364..1b6514e57 100644 --- a/local_plugins/ota/ios/Classes/OtaPlugin.swift +++ b/local_plugins/ota/ios/Classes/OtaPlugin.swift @@ -1,18 +1,24 @@ import Flutter -public class OtaPlugin: NSObject, FlutterPlugin { - let userManager = JLBleManager.sharedInstance() +public class OtaPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { + var userManager: JLBleManager? + var eventSink: FlutterEventSink? public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "ota", binaryMessenger: registrar.messenger()) + let eventChannel = FlutterEventChannel(name: "com.example.ota/events", binaryMessenger: registrar.messenger()) let instance = OtaPlugin() registrar.addMethodCallDelegate(instance, channel: channel) + eventChannel.setStreamHandler(instance) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? [String: Any] switch call.method { + case "init": + userManager = JLBleManager.sharedInstance() + result("") case "startUpgrading": let address = arguments?["address"] as? String let filePath = arguments?["filePath"] as? String @@ -26,14 +32,31 @@ public class OtaPlugin: NSObject, FlutterPlugin { } } + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + eventSink = events + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + eventSink = nil + userManager?.flutterEventSink = nil + return nil + } + private func startUpgrading(address: String?, filePath: String?) { - if userManager.mBleManagerState == CBManagerState.poweredOn { - userManager.startScanBLE(address, filePath: filePath) + if userManager?.mBleManagerState == CBManagerState.poweredOn { + userManager?.flutterEventSink = eventSink + userManager?.startScanBLE(address, filePath: filePath) DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { - self.userManager.stopScanBLE() + self.userManager?.stopScanBLE() } } } - private func onOtaClose() {} + private func onOtaClose() { + userManager?.stopScanBLE() + userManager?.otaFuncCancel{code in} + userManager?.disconnectBLE() + userManager = nil + } } diff --git a/local_plugins/ota/lib/ota.dart b/local_plugins/ota/lib/ota.dart index 2919d9107..79c2948f9 100644 --- a/local_plugins/ota/lib/ota.dart +++ b/local_plugins/ota/lib/ota.dart @@ -132,6 +132,10 @@ class Ota { // _isEventListenerSet = true; // } + Future init() { + return OtaPlatform.instance.init(); + } + Future startUpgrading({String? address, String? filePath}) { return OtaPlatform.instance.startUpgrading( address: address, diff --git a/local_plugins/ota/lib/ota_method_channel.dart b/local_plugins/ota/lib/ota_method_channel.dart index cc3890635..83203825c 100644 --- a/local_plugins/ota/lib/ota_method_channel.dart +++ b/local_plugins/ota/lib/ota_method_channel.dart @@ -9,6 +9,11 @@ class MethodChannelOta extends OtaPlatform { @visibleForTesting final methodChannel = const MethodChannel('ota'); + @override + Future init() async { + await methodChannel.invokeMethod('init'); + } + @override Future startUpgrading({String? address, String? filePath}) async { await methodChannel.invokeMethod('startUpgrading', { diff --git a/local_plugins/ota/lib/ota_platform_interface.dart b/local_plugins/ota/lib/ota_platform_interface.dart index 026414e86..a1a271b13 100644 --- a/local_plugins/ota/lib/ota_platform_interface.dart +++ b/local_plugins/ota/lib/ota_platform_interface.dart @@ -23,6 +23,10 @@ abstract class OtaPlatform extends PlatformInterface { _instance = instance; } + Future init() { + throw UnimplementedError('init() has not been implemented.'); + } + Future startUpgrading({String? address, String? filePath}) { throw UnimplementedError('startUpgrading() has not been implemented.'); } From f09bc3d6a994eefbd6188b8531f414afe1eccf98 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 13 Jun 2025 19:24:27 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=B8=8A=E4=BC=A0ai=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .metadata | 12 +-- android/app/src/debug/AndroidManifest.xml | 7 ++ .../deep_voice/MainActivity.kt | 5 ++ android/app/src/profile/AndroidManifest.xml | 7 ++ lib/data/services/location_manager.dart | 53 +++++++++++ .../agent_service/android/build.gradle.kts | 1 + .../agent_service/AgentService.kt | 15 +++- .../yunqiinnovation/agent_service/utils.kt | 13 ++- .../amap_service/android/build.gradle | 2 +- .../amap_service/NavigationServiceHelper.kt | 3 + .../chat_api/ChatApiService.kt | 11 +-- .../yunqiinnovation/chat_api/MCPSubClient.kt | 7 +- .../chat_api/SystemFunctionHandler.kt | 20 ++--- local_plugins/location_service/.gitignore | 33 +++++++ local_plugins/location_service/.metadata | 33 +++++++ local_plugins/location_service/CHANGELOG.md | 3 + local_plugins/location_service/LICENSE | 1 + local_plugins/location_service/README.md | 15 ++++ .../location_service/analysis_options.yaml | 4 + .../location_service/android/.gitignore | 9 ++ .../location_service/android/build.gradle | 70 +++++++++++++++ .../location_service/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 7 ++ .../location_service/LocationService.kt | 87 +++++++++++++++++++ .../location_service/LocationServicePlugin.kt | 32 +++++++ local_plugins/location_service/ios/.gitignore | 38 ++++++++ .../ios/location_service.podspec | 29 +++++++ .../ios/location_service/Package.swift | 32 +++++++ .../LocationServicePlugin.swift | 19 ++++ .../location_service/PrivacyInfo.xcprivacy | 14 +++ .../lib/location_service.dart | 49 +++++++++++ local_plugins/location_service/pubspec.yaml | 72 +++++++++++++++ pubspec.yaml | 2 + 33 files changed, 673 insertions(+), 33 deletions(-) create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/com/yunqiinnovation/deep_voice/MainActivity.kt create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 lib/data/services/location_manager.dart create mode 100644 local_plugins/location_service/.gitignore create mode 100644 local_plugins/location_service/.metadata create mode 100644 local_plugins/location_service/CHANGELOG.md create mode 100644 local_plugins/location_service/LICENSE create mode 100644 local_plugins/location_service/README.md create mode 100644 local_plugins/location_service/analysis_options.yaml create mode 100644 local_plugins/location_service/android/.gitignore create mode 100644 local_plugins/location_service/android/build.gradle create mode 100644 local_plugins/location_service/android/settings.gradle create mode 100644 local_plugins/location_service/android/src/main/AndroidManifest.xml create mode 100644 local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt create mode 100644 local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt create mode 100644 local_plugins/location_service/ios/.gitignore create mode 100644 local_plugins/location_service/ios/location_service.podspec create mode 100644 local_plugins/location_service/ios/location_service/Package.swift create mode 100644 local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift create mode 100644 local_plugins/location_service/ios/location_service/Sources/location_service/PrivacyInfo.xcprivacy create mode 100644 local_plugins/location_service/lib/location_service.dart create mode 100644 local_plugins/location_service/pubspec.yaml diff --git a/.metadata b/.metadata index 5c982c8d4..0d012fee6 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "ea121f8859e4b13e47a8f845e4586164519588bc" + revision: "5c1433509f997e9059d230942bdf83f1b88a179e" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: ea121f8859e4b13e47a8f845e4586164519588bc - base_revision: ea121f8859e4b13e47a8f845e4586164519588bc - - platform: ios - create_revision: ea121f8859e4b13e47a8f845e4586164519588bc - base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + - platform: android + create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e # User provided section diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..399f6981d --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deep_voice/MainActivity.kt new file mode 100644 index 000000000..ac31828f0 --- /dev/null +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deep_voice/MainActivity.kt @@ -0,0 +1,5 @@ +package com.yunqiinnovation.deep_voice + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..399f6981d --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/lib/data/services/location_manager.dart b/lib/data/services/location_manager.dart new file mode 100644 index 000000000..bf9839d83 --- /dev/null +++ b/lib/data/services/location_manager.dart @@ -0,0 +1,53 @@ +import 'package:deep_voice/core/utils/logger.dart'; +import 'package:get/get.dart'; +import 'package:location_service/location_service.dart'; +import 'package:music_service/music_service.dart'; +import 'package:permission_handler/permission_handler.dart'; +import '../../core/utils/permission_util.dart'; +import 'package:flutter/material.dart'; + +//音乐播放管理 +class LocationManager extends GetxService { + static LocationManager get to => Get.find(); + + // 音乐服务实例 + final LocationService _musicService = LocationService(); + + /// 初始化 + @override + void onInit() { + super.onInit(); + requestNotificationPermission(); + Logger.info('初始化权限请求'); + } + + Future requestNotificationPermission() async { + final granted = await PermissionUtil.instance.requestPermission( + // context: context, + permissionType: Permission.notification, + permissionName: '通知', + explanationText: '需要通知权限来显示音乐播放状态。', + permanentDenialText: '您已永久拒绝通知权限,请前往设置开启权限。', + settingsButtonText: '去设置', + cancelButtonText: '取消', + ); + + if (!granted) { + Logger.warning('通知权限未被授予'); + return; + } + + Logger.info('通知权限已授予'); + } + + /// 释放资源 + @override + void onClose() { + super.onClose(); + } + + //定位 + Future> location() async { + return await _musicService.location(); + } +} diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index 1fd600fb7..9d8dea1bb 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -49,4 +49,5 @@ dependencies { add("compileOnly", project(":amap_service")) add("compileOnly", project(":speech")) add("compileOnly", project(":chat_api")) + add("compileOnly", project(":location_service")) } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index c3854b568..ac3f5d9eb 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -9,6 +9,7 @@ import com.yunqiinnovation.chat_api.StreamCallback import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.amap_service.NavigationServiceHelper +import com.yunqiinnovation.location_service.LocationService import android.util.Log import kotlinx.coroutines.* import java.util.Collections @@ -125,8 +126,8 @@ object AgentService : CoroutineScope { this.chatApiService = ChatApiService(context) this.chatStorageHelper = ChatStorageHelper(context) this.audioPlayer = AudioPlayer(context) - - + //初始化定位服务 + LocationService.initialize(appContext) // 设置系统提示词 val customSystemPrompt = config["systemPrompt"] as? String if (!customSystemPrompt.isNullOrBlank()) { @@ -621,8 +622,14 @@ object AgentService : CoroutineScope { // 先添加系统提示词 if (systemPrompt.isNotEmpty()) { - messagesWithSystemPrompt.put(createSystemMessage(systemPrompt)) - // Log.d(TAG, "系统提示词: ${systemPrompt}") + val location = LocationService.getCachedLocation()?: mapOf( + "user_location" to "定位未开启", + "user_lat" to "定位未开启", + "user_lng" to "定位未开启", + ) + val _systemPrompt = Utils.fillTemplate(systemPrompt,location) + messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) + Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") } // 再添加历史消息 diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt index 2b5457952..f9b3df85a 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt @@ -141,4 +141,15 @@ object Utils { false } } -} \ No newline at end of file + + /** + * 替换字符串 变量 + * */ + fun fillTemplate(tpl: String, data: Map): String { + var result = tpl + data.forEach { (k, v) -> + result = result.replace("{$k}", v) + } + return result + } +} \ No newline at end of file diff --git a/local_plugins/amap_service/android/build.gradle b/local_plugins/amap_service/android/build.gradle index 3f126ab46..9bb3ba7fb 100644 --- a/local_plugins/amap_service/android/build.gradle +++ b/local_plugins/amap_service/android/build.gradle @@ -48,7 +48,7 @@ android { } dependencies { - implementation 'com.amap.api:navi-3dmap:latest.integration' + implementation('com.amap.api:navi-3dmap:latest.integration') testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.mockito:mockito-core:5.0.0") } diff --git a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationServiceHelper.kt b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationServiceHelper.kt index 500e45bf7..5b7eced04 100644 --- a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationServiceHelper.kt +++ b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationServiceHelper.kt @@ -105,4 +105,7 @@ object NavigationServiceHelper { } } + + + } \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 150321d7e..bad3aa1a2 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -308,7 +308,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor Log.e("ChatApiService", "消息列表为空") throw IllegalArgumentException("消息列表不能为空") } - +// Log.e("ChatApiService", "AI解析工具列表: ${tools}") + val chatsFlow = try { val chatCompletionRequest = ChatCompletionRequest( model = ModelId(currentModel), @@ -352,13 +353,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 收集工具调用信息 delta.toolCalls?.forEach { toolCall -> - val id = toolCall.id ?: run { - Log.e("ChatApiService", "toolCall.id 为空,跳过本次处理") - return@forEach - } - Log.e("ChatApiService", "AI发起工具调用: ${toolCall.id} ${toolCall.index} ${toolCall.function?.name} ${toolCall.function?.arguments}") val index = toolCall.index - // 创建或获取现有的工具调用信息 val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() } // 安全处理工具调用ID @@ -386,6 +381,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } catch (e: Exception) { Log.e("ChatApiService", "处理工具调用参数异常: ${e.message}") } + Log.e("ChatApiService", "AI发起工具调用:${toolCallInfo.name} ${toolCallInfo.arguments}") } } catch (e: Exception) { Log.e("ChatApiService", "处理工具调用函数信息异常: ${e.message}") @@ -692,6 +688,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor description = description, parameters = parameters ) + Log.e("ChatApiService", "AI携带工具:${name}:${parameters}") tools.add(tool) } catch (e: Exception) { diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt index a332689e2..b7b3adbe2 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt @@ -101,7 +101,7 @@ class MCPSubClient( retryCount = 0 currentReconnectDelay = initialReconnectDelay // 启动心跳检测 -// startHeartbeat() + startHeartbeat() true } catch (e: Exception) { @@ -129,7 +129,7 @@ class MCPSubClient( "properties" to emptyMap(), "required" to emptyList() ) - + Log.w(TAG, "解析工具数据: ${tool.name} ${parametersMap}") val toolMap = mapOf( "type" to "function", "function" to mapOf( @@ -229,7 +229,7 @@ class MCPSubClient( val required = mutableListOf() // 处理properties - inputSchema.properties?.let { propsJsonObject -> + inputSchema.properties.let { propsJsonObject -> for ((key, value) in propsJsonObject) { when (value) { is JsonPrimitive -> { @@ -260,7 +260,6 @@ class MCPSubClient( "properties" to properties, "required" to required ) - return result } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index 93a019bcb..058cb0876 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt @@ -127,16 +127,16 @@ class SystemFunctionHandler(private val context: Context? = null) { ) // 注册获取当前位置函数 - client.registerLocalFunction( - name = "get_current_location", - description = "获取当前地理位置", - parameters = mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - handler = GetCurrentLocationHandler(context) - ) +// client.registerLocalFunction( +// name = "get_current_location", +// description = "获取当前地理位置", +// parameters = mapOf( +// "type" to "object", +// "properties" to emptyMap(), +// "required" to emptyList() +// ), +// handler = GetCurrentLocationHandler(context) +// ) // 注册媒体播放功能 client.registerLocalFunction( diff --git a/local_plugins/location_service/.gitignore b/local_plugins/location_service/.gitignore new file mode 100644 index 000000000..e7d347d9d --- /dev/null +++ b/local_plugins/location_service/.gitignore @@ -0,0 +1,33 @@ +# 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/ diff --git a/local_plugins/location_service/.metadata b/local_plugins/location_service/.metadata new file mode 100644 index 000000000..709f51d7e --- /dev/null +++ b/local_plugins/location_service/.metadata @@ -0,0 +1,33 @@ +# 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: "5c1433509f997e9059d230942bdf83f1b88a179e" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + - platform: android + create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + - platform: ios + create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + + # 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' diff --git a/local_plugins/location_service/CHANGELOG.md b/local_plugins/location_service/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/local_plugins/location_service/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/local_plugins/location_service/LICENSE b/local_plugins/location_service/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/local_plugins/location_service/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/local_plugins/location_service/README.md b/local_plugins/location_service/README.md new file mode 100644 index 000000000..ba86601cc --- /dev/null +++ b/local_plugins/location_service/README.md @@ -0,0 +1,15 @@ +# location_service + +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. + diff --git a/local_plugins/location_service/analysis_options.yaml b/local_plugins/location_service/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/local_plugins/location_service/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/local_plugins/location_service/android/.gitignore b/local_plugins/location_service/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/local_plugins/location_service/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/local_plugins/location_service/android/build.gradle b/local_plugins/location_service/android/build.gradle new file mode 100644 index 000000000..63c9f82d9 --- /dev/null +++ b/local_plugins/location_service/android/build.gradle @@ -0,0 +1,70 @@ +group = "com.yunqiinnovation.location_service" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.1.0" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.7.3") + 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.location_service" + + 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 应该写在这里(android {} 外面) + dependencies { + implementation('com.amap.api:navi-3dmap:latest.integration') + + 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 + } + } + } +} + diff --git a/local_plugins/location_service/android/settings.gradle b/local_plugins/location_service/android/settings.gradle new file mode 100644 index 000000000..e165e2838 --- /dev/null +++ b/local_plugins/location_service/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'location_service' diff --git a/local_plugins/location_service/android/src/main/AndroidManifest.xml b/local_plugins/location_service/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..3cbeb31e4 --- /dev/null +++ b/local_plugins/location_service/android/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt new file mode 100644 index 000000000..e36eaa569 --- /dev/null +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt @@ -0,0 +1,87 @@ +package com.yunqiinnovation.location_service + +import android.content.Context +import android.location.Location +import android.util.Log +import com.amap.api.location.* +import io.flutter.plugin.common.MethodChannel + +object LocationService { + private const val TAG = "LocationService" + + private var locationClient: AMapLocationClient? = null + private var currentLocation: AMapLocation? = null + private var initialized = false + + fun initialize(context: Context) { + if (initialized) return + Log.e(TAG, "liwei--------- 初始化高德定位服务 1") + + try { + AMapLocationClient.updatePrivacyShow(context, true, true) + AMapLocationClient.updatePrivacyAgree(context, true) + Log.e(TAG, "liwei--------- 初始化高德定位服务 2") + locationClient = AMapLocationClient(context.applicationContext) + val option = AMapLocationClientOption().apply { + locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy + isOnceLocation = false + interval = 5000L + } + Log.e(TAG, "liwei--------- 初始化高德定位服务 3") + locationClient?.setLocationOption(option) + Log.e(TAG, "liwei--------- 初始化高德定位服务 4") + locationClient?.setLocationListener { location -> + if (location != null && location.errorCode == 0) { + currentLocation = location + Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}") + } else { + Log.w(TAG, "liwei---------定位失败: ${location?.errorCode} - ${location?.errorInfo}") + } + } + Log.e(TAG, "liwei--------- 初始化高德定位服务 5") + locationClient?.startLocation() + Log.e(TAG, "liwei--------- 初始化高德定位服务 6") + initialized = true + } catch (e: Exception) { + Log.e(TAG, "高德定位初始化失败: ${e.message}", e) + } + } + + fun getCachedLocation(): Map? { +// // 定位成功,获取经纬度 +// val latitude = location.latitude +// val longitude = location.longitude +// +// // 获取结构化地址 +// val province = location.province // 省 +// val city = location.city // 市 +// val district = location.district // 区县 +// val street = location.street // 街道 +// val streetNum = location.streetNum // 门牌号 +// val adCode = location.adCode // 行政区划代码 +// val address = location.address // 详细地址拼接字符串 + return currentLocation?.let { + mapOf( + "user_location" to it.address, + "user_lat" to it.latitude.toString(), + "user_lng" to it.longitude.toString(), + ) + } + } + fun getCurrentLocation(result: MethodChannel.Result) { + val cached = getCachedLocation() + if (cached != null) { + result.success(cached) + } else { + result.error("NO_LOCATION", "当前位置还未获取", null) + } + } + + fun stop() { + locationClient?.stopLocation() + locationClient?.onDestroy() + locationClient = null + currentLocation = null + initialized = false + } +} diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt new file mode 100644 index 000000000..b34b48f24 --- /dev/null +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt @@ -0,0 +1,32 @@ +package com.yunqiinnovation.location_service + +import android.content.Context +import android.util.Log +import io.flutter.embedding.engine.plugins.FlutterPlugin +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 + +/** LocationServicePlugin */ +class LocationServicePlugin: FlutterPlugin, MethodCallHandler { + private val TAG = "LocationServicePlugin" + private lateinit var context: Context + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.location_service") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + Log.d(TAG, "Method call: ${call.method}") + when (call.method) { + "location" -> LocationService.getCurrentLocation(result) + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {} +} diff --git a/local_plugins/location_service/ios/.gitignore b/local_plugins/location_service/ios/.gitignore new file mode 100644 index 000000000..034771fc9 --- /dev/null +++ b/local_plugins/location_service/ios/.gitignore @@ -0,0 +1,38 @@ +.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 diff --git a/local_plugins/location_service/ios/location_service.podspec b/local_plugins/location_service/ios/location_service.podspec new file mode 100644 index 000000000..b5409ce22 --- /dev/null +++ b/local_plugins/location_service/ios/location_service.podspec @@ -0,0 +1,29 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint location_service.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'location_service' + 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 = 'location_service/Sources/location_service/**/*' + 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 = {'location_service_privacy' => ['location_service/Sources/location_service/PrivacyInfo.xcprivacy']} +end diff --git a/local_plugins/location_service/ios/location_service/Package.swift b/local_plugins/location_service/ios/location_service/Package.swift new file mode 100644 index 000000000..0b0dd0fbd --- /dev/null +++ b/local_plugins/location_service/ios/location_service/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "location_service", + platforms: [ + .iOS("12.0") + ], + products: [ + .library(name: "location-service", targets: ["location_service"]) + ], + dependencies: [], + targets: [ + .target( + name: "location_service", + dependencies: [], + resources: [ + // 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 these lines. For more information, see + // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + // .process("PrivacyInfo.xcprivacy"), + + // If you have other resources that need to be bundled with your plugin, refer to + // the following instructions to add them: + // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package + ] + ) + ] +) diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift new file mode 100644 index 000000000..1b6f10f28 --- /dev/null +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift @@ -0,0 +1,19 @@ +import Flutter +import UIKit + +public class LocationServicePlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "location_service", binaryMessenger: registrar.messenger()) + let instance = LocationServicePlugin() + 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) + } + } +} diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/PrivacyInfo.xcprivacy b/local_plugins/location_service/ios/location_service/Sources/location_service/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..a34b7e2e6 --- /dev/null +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/local_plugins/location_service/lib/location_service.dart b/local_plugins/location_service/lib/location_service.dart new file mode 100644 index 000000000..c662819c6 --- /dev/null +++ b/local_plugins/location_service/lib/location_service.dart @@ -0,0 +1,49 @@ +import 'package:flutter/services.dart'; + +class LocationService { + static final LocationService _instance = LocationService._internal(); + + /// 获取单例 + factory LocationService() => _instance; + + // 方法通道 + final MethodChannel _methodChannel = MethodChannel( + 'com.yunqiinnovation.location_service', + ); + + // 事件通道 - 简化为只使用数据和状态通道 + late final EventChannel _dataEventChannel; + late final EventChannel _statusEventChannel; + + // 事件流 + late final Stream _dataStream; + late final Stream _statusStream; + + // 获取事件流 + Stream get onDataReceived => _dataStream; + + // 状态流直接传递原始数据 + Stream get onStatusUpdated => _statusStream; + + // 内部构造函数 + LocationService._internal() { + _dataEventChannel = const EventChannel( + 'com.yunqiinnovation.music_service/data_events', + ); + _statusEventChannel = const EventChannel( + 'com.yunqiinnovation.music_service/status_events', + ); + + // 初始化事件流 + _dataStream = _dataEventChannel.receiveBroadcastStream(); + _statusStream = _statusEventChannel.receiveBroadcastStream(); + } + + Future> location() async { + final result = await _methodChannel.invokeMethod('location'); + return { + 'latitude': result?['longitude'] ?? 0.0, + 'longitude': result?['longitude'] ?? 0.0, + }; + } +} diff --git a/local_plugins/location_service/pubspec.yaml b/local_plugins/location_service/pubspec.yaml new file mode 100644 index 000000000..b575ce3b7 --- /dev/null +++ b/local_plugins/location_service/pubspec.yaml @@ -0,0 +1,72 @@ +name: location_service +description: "A new Flutter project." +version: 0.0.1 +homepage: + +environment: + sdk: ^3.8.1 + 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.location_service + pluginClass: LocationServicePlugin + ios: + pluginClass: LocationServicePlugin + + # 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 diff --git a/pubspec.yaml b/pubspec.yaml index fe2bd0ce1..6844d9e38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,6 +91,8 @@ dependencies: path: local_plugins/music_service amap_service: path: local_plugins/amap_service + location_service: + path: local_plugins/location_service realtime: path: local_plugins/realtime From 2074bca27a406e5033dc93f53dd928be69055c69 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 13 Jun 2025 20:12:53 +0800 Subject: [PATCH 4/4] =?UTF-8?q?ota=E5=8D=87=E7=BA=A7=E6=96=AD=E5=BC=80ble?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/ota_upgrade_controller.dart | 18 +++++- .../Sources/ble_service/BleService.swift | 39 ++++++------- .../ble_service/SwiftBleServicePlugin.swift | 11 +++- .../ble_service/lib/ble_service.dart | 56 +++++++++++++------ 4 files changed, 84 insertions(+), 40 deletions(-) diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index 25e6188ce..32e94afa3 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -1,3 +1,5 @@ +import 'package:ble_service/ble_service.dart'; +import 'package:classic_bluetooth/classic_bluetooth.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:path/path.dart'; @@ -30,6 +32,8 @@ class OtaUpgradeController extends GetxController { // 当前设备固件版本号 final RxString currentVersion = ''.obs; + String _address = ''; + // 更新包地址,用于下载更新文件 String updatepackageaddress = ''; @@ -50,6 +54,7 @@ class OtaUpgradeController extends GetxController { // 蓝牙管理服务,用于获取设备信息和通信 final _bleManager = Get.find(); + final BleService _bleService = BleService(); final maxRetryAttempts = 3; // 最大重试次数 int currentRetryCount = 0; @@ -68,6 +73,7 @@ class OtaUpgradeController extends GetxController { @override void onClose() { jlota.onOtaClose(); + _bleService.setEnterOta(false); isUpgrade.value = false; super.onClose(); } @@ -166,6 +172,16 @@ class OtaUpgradeController extends GetxController { currentVersion.value = firstPart; print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); + final connectedDevices = await ClassicBluetooth.getConnectedDevices(); + if (connectedDevices.isNotEmpty) { + _address = connectedDevices.first.address; + } + + await _bleService.setEnterOta(true); + await _bleService.disconnect(); + + await Future.delayed(const Duration(seconds: 2)); + jlota.init(); // API请求 @@ -372,7 +388,7 @@ class OtaUpgradeController extends GetxController { // 开始升级 void startUpgrading(String path) async { - jlota.startUpgrading(address: 'E9:09:05:E0:18:7A', filePath: path); + jlota.startUpgrading(address: _address, filePath: path); return; // isUpgrade.value = false; } diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index 0c7398b7c..7b1ba6dfe 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -43,6 +43,10 @@ public class BleService: NSObject { private var audioCharacteristic: CBCharacteristic? private var isScanning = false + + // 是否是ota模式 + var isEnterOta = false + private var callbacks = [Callback]() private var connectionState = BleConst.STATE_DISCONNECTED @@ -212,6 +216,9 @@ public class BleService: NSObject { /// 统一的连接方法,支持恢复和重连 private func connectPeripheral(_ peripheral: CBPeripheral) { + if isEnterOta { + return + } guard let manager = centralManager else { return } @@ -592,19 +599,14 @@ public class BleService: NSObject { ] if !responseData.isEmpty { - if let versionStr = String(data: responseData, encoding: .utf8) { - resultMap["version"] = versionStr - os_log("接收到目标版本号: %@", log: logger, type: .info, versionStr) - } else { - // 如果无法解析为字符串,以点分隔的数字表示版本 - let versionNums = responseData - .map { $0 } - .filter { $0 != 0xFF } // 过滤掉无效字节 - .map { String($0) } - .joined(separator: ".") - resultMap["version"] = versionNums - os_log("接收到目标版本号(数字格式): %@", log: logger, type: .info, versionNums) - } + // 如果无法解析为字符串,以点分隔的数字表示版本 + let versionNums = responseData + .map { $0 } + .filter { $0 != 0xFF } // 过滤掉无效字节 + .map { String($0) } + .joined(separator: ".") + resultMap["version"] = versionNums + os_log("接收到目标版本号(数字格式): %@", log: logger, type: .info, versionNums) } notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) @@ -616,13 +618,8 @@ public class BleService: NSObject { "rawData": responseData ] - if responseData.count >= 2 { - let productId = UInt16(responseData[0]) << 8 | UInt16(responseData[1]) - resultMap["productId"] = productId - os_log("接收到产品ID: %d", log: logger, type: .info, productId) - } else if !responseData.isEmpty { - // 单字节产品ID - let productId = UInt16(responseData[0]) + if !responseData.isEmpty { + let productId = responseData.map { String($0 & 0xFF) }.joined(separator: ".") resultMap["productId"] = productId os_log("接收到产品ID: %d", log: logger, type: .info, productId) } @@ -1263,4 +1260,4 @@ extension BleService: CBPeripheralDelegate { os_log("✅ 描述符写入成功: %{public}@", log: logger, type: .debug, descriptor.uuid.uuidString) } } -} \ No newline at end of file +} diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift index 79d161eb2..cff8e6565 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift @@ -150,6 +150,15 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { case "isBleConnected": result(BleService.shared.isConnected()) + + case "setEnterOta": + let arguments = call.arguments as? [String: Any] + BleService.shared.isEnterOta = (arguments?["enterOta"] as? Bool) ?? false + result("") + + case "disconnect": + BleService.shared.disconnect() + result("") case "registerBackgroundScan", "unregisterBackgroundScan": // iOS上禁用,直接返回成功以避免Flutter端报错 @@ -380,4 +389,4 @@ class BleStatusStreamHandler: NSObject, FlutterStreamHandler { plugin?.statusEventSink = nil return nil } -} \ No newline at end of file +} diff --git a/local_plugins/ble_service/lib/ble_service.dart b/local_plugins/ble_service/lib/ble_service.dart index 232aba03b..ad11aa617 100644 --- a/local_plugins/ble_service/lib/ble_service.dart +++ b/local_plugins/ble_service/lib/ble_service.dart @@ -183,34 +183,35 @@ class BleService { return false; } } - + //======================================================================== // 以下是Companion Device相关功能 //======================================================================== - + /// 检查设备是否支持Companion Device Manager - /// + /// /// 返回设备是否支持Companion Device Manager API /// 仅在Android 8.0及以上版本可用 Future isCompanionSupported() async { try { - final result = await _methodChannel.invokeMethod('isCompanionSupported'); + final result = + await _methodChannel.invokeMethod('isCompanionSupported'); return result ?? false; } catch (e) { print('检查Companion设备支持失败: $e'); return false; } } - + /// 启动设备关联流程 - /// + /// /// 弹出系统设备选择对话框,用户选择后将建立持久化关联 /// 关联成功后,应用将获得后台扫描和启动前台服务的特权 - /// + /// /// [deviceNamePattern] 设备名称正则表达式,例如"MyHeadset.*" /// [serviceUuid] 可选的服务UUID /// [macAddress] 可选的MAC地址 - /// + /// /// 返回是否成功启动关联流程 Future startDeviceAssociation({ String? deviceNamePattern, @@ -232,13 +233,14 @@ class BleService { return false; } } - + /// 获取已关联的设备MAC地址 - /// + /// /// 返回已关联的设备MAC地址,如果没有则返回null Future getPairedMacAddress() async { try { - final result = await _methodChannel.invokeMethod('getPairedMacAddress'); + final result = + await _methodChannel.invokeMethod('getPairedMacAddress'); return result; } catch (e) { print('获取配对MAC地址失败: $e'); @@ -246,9 +248,8 @@ class BleService { } } - /// 检查BLE连接状态 - /// + /// /// 返回BLE是否已连接 Future isBleConnected() async { try { @@ -260,11 +261,31 @@ class BleService { } } + /// 设置是否是ota模式 + Future setEnterOta(bool enterOta) async { + try { + await _methodChannel.invokeMethod('setEnterOta', { + 'enterOta': enterOta, + }); + } catch (e) { + print('设置是否是ota模式失败: $e'); + } + } + + /// 断开连接 + Future disconnect() async { + try { + await _methodChannel.invokeMethod('disconnect'); + } catch (e) { + print('断开连接失败: $e'); + } + } + /// 注册BLE后台扫描 /// /// 注册后,即使应用被杀死,系统也会在检测到设备广播时唤醒应用 /// 这是确保设备可以在应用后台时被自动连接的关键功能 - /// + /// /// [macAddress] 可选的设备MAC地址,如果不提供则使用已关联的设备 Future registerBackgroundScan({String? macAddress}) async { try { @@ -290,13 +311,14 @@ class BleService { return false; } } - + /// 清除所有设备关联 - /// + /// /// 用于重置设备关联状态,通常在用户主动解除关联或调试时使用 Future clearAssociations() async { try { - final result = await _methodChannel.invokeMethod('clearAssociations'); + final result = + await _methodChannel.invokeMethod('clearAssociations'); return result ?? false; } catch (e) { print('清除设备关联失败: $e');