From dde2b8ddb91ce7e0203090216ec1a7295f23c0c6 Mon Sep 17 00:00:00 2001
From: fdp <1286779656@qq.com>
Date: Thu, 10 Jul 2025 16:10:25 +0800
Subject: [PATCH 01/20] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dios=E7=BC=96=E8=AF=91?=
=?UTF-8?q?=E6=8A=A5=E9=94=99=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ios/Podfile.lock | 22 ++++++----
ios/Runner.xcodeproj/project.pbxproj | 16 +++----
ios/Runner/Info.plist | 2 +-
lib/data/services/network/nw_api.dart | 2 +-
.../Sources/music_service/MusicService.swift | 1 +
.../realtime/RealtimeAudioManager.swift | 43 ++++++++++++-------
.../speech/Sources/speech/ITtsService.swift | 8 +++-
pubspec.yaml | 2 -
8 files changed, 59 insertions(+), 37 deletions(-)
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 4ef33cfa7..d0bbc4669 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -11,8 +11,6 @@ PODS:
- Flutter
- classic_bluetooth (0.0.1):
- Flutter
- - device_info_plus (0.0.1):
- - Flutter
- FBAEMKit (18.0.0):
- FBSDKCoreKit_Basics (= 18.0.0)
- FBSDKCoreKit (18.0.0):
@@ -44,6 +42,8 @@ PODS:
- WechatOpenSDK-XCFramework (~> 2.0.4)
- haptic_feedback (0.5.1):
- Flutter
+ - keyboard_height_plugin (0.0.1):
+ - Flutter
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
@@ -85,16 +85,17 @@ PODS:
- SDWebImage/Core (~> 5.17)
- spotify_sdk (0.0.1):
- Flutter
- - tencentcloud_cos_sdk_plugin (1.2.4):
+ - tencentcloud_cos_sdk_plugin (1.2.3):
- Flutter
- QCloudCOSXML (= 6.4.7)
- WechatOpenSDK-XCFramework (2.0.4)
+ - wifi_iot (0.0.1):
+ - Flutter
DEPENDENCIES:
- amap_service (from `.symlinks/plugins/amap_service/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`)
- - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
@@ -103,12 +104,14 @@ DEPENDENCIES:
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- fluwx (from `.symlinks/plugins/fluwx/ios`)
- haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
+ - keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- location_service (from `.symlinks/plugins/location_service/ios`)
- ota (from `.symlinks/plugins/ota/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- spotify_sdk (from `.symlinks/plugins/spotify_sdk/ios`)
- tencentcloud_cos_sdk_plugin (from `.symlinks/plugins/tencentcloud_cos_sdk_plugin/ios`)
+ - wifi_iot (from `.symlinks/plugins/wifi_iot/ios`)
SPEC REPOS:
trunk:
@@ -135,8 +138,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audio_waveforms/ios"
classic_bluetooth:
:path: ".symlinks/plugins/classic_bluetooth/ios"
- device_info_plus:
- :path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_facebook_auth:
@@ -153,6 +154,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluwx/ios"
haptic_feedback:
:path: ".symlinks/plugins/haptic_feedback/ios"
+ keyboard_height_plugin:
+ :path: ".symlinks/plugins/keyboard_height_plugin/ios"
location_service:
:path: ".symlinks/plugins/location_service/ios"
ota:
@@ -165,6 +168,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/spotify_sdk/ios"
tencentcloud_cos_sdk_plugin:
:path: ".symlinks/plugins/tencentcloud_cos_sdk_plugin/ios"
+ wifi_iot:
+ :path: ".symlinks/plugins/wifi_iot/ios"
SPEC CHECKSUMS:
amap_service: 1c3e7e07f51cbbdff03719a4a079c60d015c1c0b
@@ -173,7 +178,6 @@ SPEC CHECKSUMS:
AMapNavi: fceb4715f9a3b5a25f330facebee2aa2bdba42dd
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d
- device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
FBAEMKit: e34530df538b8eb8aeb53c35867715ba6c63ef0c
FBSDKCoreKit: d3f479a69127acebb1c6aad91c1a33907bcf6c2f
FBSDKCoreKit_Basics: 017b6dc2a1862024815a8229e75661e627ac1e29
@@ -186,6 +190,7 @@ SPEC CHECKSUMS:
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0
+ keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
location_service: fab4ef23d473296a215c49e12c129e466574d94e
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
@@ -198,8 +203,9 @@ SPEC CHECKSUMS:
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca
- tencentcloud_cos_sdk_plugin: 7bed564dbe72df23e7b5cacb1e51b1a20cb1639c
+ tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
+ wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc
PODFILE CHECKSUM: 517ecd5ce5153c9609017e05f4e64ab72605e805
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index bad551a70..4df97502d 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -252,7 +252,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
D013B7972DFA83E400B2B69B /* XCRemoteSwiftPackageReference "flutterfire" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
@@ -526,7 +526,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 42C82SZUVX;
+ DEVELOPMENT_TEAM = 537SFY28XS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
@@ -534,7 +534,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
+ PRODUCT_BUNDLE_IDENTIFIER = com.chuangxiangyunqi.deapsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -716,7 +716,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 42C82SZUVX;
+ DEVELOPMENT_TEAM = 537SFY28XS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
@@ -724,7 +724,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
+ PRODUCT_BUNDLE_IDENTIFIER = com.chuangxiangyunqi.deapsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -741,7 +741,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 42C82SZUVX;
+ DEVELOPMENT_TEAM = 537SFY28XS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
@@ -749,7 +749,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
+ PRODUCT_BUNDLE_IDENTIFIER = com.chuangxiangyunqi.deapsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -793,7 +793,7 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 5be33da0e..bc3f4a793 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -39,7 +39,7 @@
- CFBundleVersion
+ CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
FacebookAdvertiserIDCollectionEnabled
diff --git a/lib/data/services/network/nw_api.dart b/lib/data/services/network/nw_api.dart
index 9a60f66f3..8feff1db5 100644
--- a/lib/data/services/network/nw_api.dart
+++ b/lib/data/services/network/nw_api.dart
@@ -1,5 +1,5 @@
class NWApi {
// static const baseApi = "https://www.deapsound.com";
- static const baseApi = "https://api.ideapsound.com";
+ static const baseApi = "https://api.deapsound.com";
// static const baseApi = "https://api.ideapsound.com";
}
diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
index 45b830e4a..fd0382cae 100644
--- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
+++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
@@ -4,6 +4,7 @@
//
// Created by 李伟 on 2025/6/4.
//
+import Flutter
import Foundation
import AVFoundation
import MediaPlayer
diff --git a/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift b/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift
index c2a9d424a..93a38fb84 100644
--- a/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift
+++ b/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift
@@ -324,21 +324,32 @@ class RealtimeAudioManager {
}
/// 计算音频缓冲区的RMS值
- private func calculateRMS(buffer: AVAudioPCMBuffer) -> Float {
- guard let channelData = buffer.int16ChannelData?[0],
- buffer.frameLength > 0 else {
- return 0.0
- }
-
- let frameCount = Int(buffer.frameLength)
- var rms: Float = 0.0
-
- // 使用Accelerate框架高效计算RMS
- vDSP_measqv(channelData, 1, &rms, vDSP_Length(frameCount))
-
- // 计算平方根并归一化
- rms = sqrt(rms / Float(frameCount)) / 32768.0
-
- return rms
+private func calculateRMS(buffer: AVAudioPCMBuffer) -> Float {
+ guard let channelData = buffer.int16ChannelData?[0],
+ buffer.frameLength > 0 else {
+ return 0.0
}
+
+ let frameCount = Int(buffer.frameLength)
+ let channelDataPointer = UnsafeMutablePointer(mutating: channelData)
+
+ // 创建浮点缓冲区用于转换
+ var floatData = [Float](repeating: 0.0, count: frameCount)
+
+ // 1. 将16位整型转换为浮点型
+ vDSP_vflt16(channelDataPointer, 1, &floatData, 1, vDSP_Length(frameCount))
+
+ // 2. 归一化到[-1.0, 1.0]范围
+ var normalizedData = [Float](repeating: 0.0, count: frameCount)
+ var divisor: Float = 32768.0
+ vDSP_vsdiv(floatData, 1, &divisor, &normalizedData, 1, vDSP_Length(frameCount))
+
+ // 3. 计算均方值
+ var meanSquare: Float = 0.0
+ vDSP_measqv(normalizedData, 1, &meanSquare, vDSP_Length(frameCount))
+
+ // 4. 计算RMS值
+ let rms = sqrt(meanSquare)
+ return rms
+}
}
\ No newline at end of file
diff --git a/local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift b/local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift
index 3f784bc20..72ada9274 100644
--- a/local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift
+++ b/local_plugins/speech/ios/speech/Sources/speech/ITtsService.swift
@@ -13,6 +13,12 @@ import Foundation
/** 合成取消 */
case synthesisCanceled
+ /** 播放开始 */
+ case playbackStarted
+
+ /** 播放完成 */
+ case playbackCompleted
+
/** 发生错误 */
case error
}
@@ -144,4 +150,4 @@ import Foundation
* @param listener 要移除的音频数据监听器
*/
func removeAudioDataListener(_ listener: AudioDataListener)
-}
\ No newline at end of file
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 32cdf99ff..437abdc1a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -99,8 +99,6 @@ dependencies:
path: local_plugins/amap_service
location_service:
path: local_plugins/location_service
- bytedance_speech:
- path: local_plugins/bytedance_speech
realtime:
path: local_plugins/realtime
jl_opus:
From f808732321e03510d0e7eb907834eec01b0edbb1 Mon Sep 17 00:00:00 2001
From: fdp <1286779656@qq.com>
Date: Thu, 10 Jul 2025 18:49:30 +0800
Subject: [PATCH 02/20] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=BE=AE=E4=BF=A1?=
=?UTF-8?q?=E7=99=BB=E5=BD=95=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=99=BB=E5=BD=95?=
=?UTF-8?q?=E7=95=8C=E9=9D=A2=E9=83=A8=E5=88=86=E8=8B=B9=E6=9E=9C=E6=89=8B?=
=?UTF-8?q?=E6=9C=BA=E6=98=BE=E7=A4=BA=E5=BC=82=E5=B8=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ios/Runner/Runner.entitlements | 8 ++++++--
lib/data/services/login_service.dart | 2 +-
lib/modules/login/controllers/login_controller.dart | 4 ++--
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index 032f72d37..0286ef500 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -2,11 +2,15 @@
+ com.apple.developer.associated-domains
+
+ applinks:deepsoud.com
+
+ com.apple.developer.homekit
+
com.apple.developer.networking.wifi-info
com.apple.external-accessory.wireless-configuration
- com.apple.developer.homekit
-
diff --git a/lib/data/services/login_service.dart b/lib/data/services/login_service.dart
index 2844ef0f0..acc39ad1b 100644
--- a/lib/data/services/login_service.dart
+++ b/lib/data/services/login_service.dart
@@ -8,7 +8,7 @@ class LoginService {
FlWX().register(
appSecret: '4cfde33bc22b6cc6b1f8b225dd97d255',
appId: 'wx5bbb7be9324f3e2d',
- universalLink: 'https://yunqiinnovation.com/app/',
+ universalLink: 'https://deepsoud.com/app/',
params: FlWXCallbackParams(onHttp: (url) async {
return '';
}));
diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart
index fc4150b4a..ce83f8a08 100644
--- a/lib/modules/login/controllers/login_controller.dart
+++ b/lib/modules/login/controllers/login_controller.dart
@@ -194,8 +194,8 @@ class LoginController extends GetxController {
List get verificationCode => _verificationCode;
// 文本控制器
- final contactController = TextEditingController();
- final verificationCodeController = TextEditingController();
+ TextEditingController contactController = TextEditingController();
+ TextEditingController verificationCodeController = TextEditingController();
@override
void onInit() {
From e17e5c72351aa6d444fa603c494536f95ed8489c Mon Sep 17 00:00:00 2001
From: fdp <1286779656@qq.com>
Date: Fri, 11 Jul 2025 10:30:22 +0800
Subject: [PATCH 03/20] 1
---
lib/modules/meeting/controllers/meeting_controller.dart | 1 -
.../Sources/agent_service/AgentServiceImpl.swift | 7 +++++--
.../Sources/agent_service/AgentServicePlugin.swift | 6 +++++-
3 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart
index 363383c13..dc5acfd37 100644
--- a/lib/modules/meeting/controllers/meeting_controller.dart
+++ b/lib/modules/meeting/controllers/meeting_controller.dart
@@ -319,7 +319,6 @@ class MeetingController extends GetxController {
} else {
FilePickerResult? result = await FilePicker.platform.pickFiles(
withData: true,
- type: FileType.audio,
);
if (result != null) {
PlatformFile file = result.files.first;
diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
index 0dc9119f6..251984c27 100644
--- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
+++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
@@ -290,8 +290,7 @@ class AgentServiceImpl: NSObject {
startIdleCheck()
}
}
-
- func startRecognition(useBle: Bool = false) -> Bool {
+ func startRecognition(useBle: Bool = false) -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
@@ -326,6 +325,10 @@ class AgentServiceImpl: NSObject {
return true
}
+ func recognizeCallback(useBle: Bool = false) -> Bool {
+
+ return true
+ }
func stopRecognition() -> Bool {
if !isRecognizing {
diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
index 7c3b415f7..e3ad6b106 100644
--- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
+++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
@@ -43,6 +43,10 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
case "startConversation":
impl.startRecognition()
result(true)
+
+ case "recognizeCallback":
+ impl.recognizeCallback()
+ result(true)
case "stopConversation":
impl.stopRecognition()
@@ -157,4 +161,4 @@ extension AgentServiceEventHandler: AgentServiceListener {
}
}
}
-}
\ No newline at end of file
+}
From 660a0611cf2e6786b86b0cefb10aa252440bd376 Mon Sep 17 00:00:00 2001
From: fdp <1286779656@qq.com>
Date: Fri, 11 Jul 2025 16:09:45 +0800
Subject: [PATCH 04/20] =?UTF-8?q?=E6=9C=89=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../agent_service/AgentServiceImpl.swift | 2 +-
.../Sources/azure_speech/AzureAsrHelper.swift | 274 ++++++++++++++++--
.../azure_speech/AzureSpeechPlugin.swift | 4 +-
.../Sources/tools/RecordFile.swift | 232 +++++++++++++++
4 files changed, 480 insertions(+), 32 deletions(-)
create mode 100644 local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift
diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
index 251984c27..a80a0d7cd 100644
--- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
+++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
@@ -347,7 +347,7 @@ class AgentServiceImpl: NSObject {
return false
}
- azureAsrHelper?.pushAudioData(data: audioData)
+ azureAsrHelper?.audioStream?.saveAudioDataTo(data)
return true
}
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
index b0f4bcb02..ce81a449d 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
@@ -31,7 +31,13 @@ public class AzureAsrHelper: NSObject {
private var isAutoDetectLanguage = false
private var subscriptionKey = ""
private var region = ""
+ // 音频处理
+ private let audioQueue = DispatchQueue(label: "audio.processing.queue")
+ private var audioEngine: AVAudioEngine?
+ private var audioBuffer: Data?
+ // 录音文件处理
+ private let recordFile = RecordFile.shared
// 音频源配置
public enum AudioSourceType {
/** 使用设备麦克风 */
@@ -45,6 +51,11 @@ public class AzureAsrHelper: NSObject {
// 音频处理
private var externalAudioStream: ExternalAudioPullStream?
+
+ private var audioStream: AudioStream?
+
+ // 当前回调
+ private weak var currentContinuousCallback: ContinuousRecognizeCallback?
/**
* 初始化Azure语音服务
@@ -106,28 +117,16 @@ public class AzureAsrHelper: NSObject {
}
// 创建识别器
- return setupRecognizer()
+ return true
} catch {
os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
return false
}
}
-
- /**
- * 向音频流写入音频数据
- * 仅当音频源设置为external时有效
- *
- * @param data 音频数据字节数组
- */
- public func pushAudioData(data: Data) {
- if audioSourceType != .external {
- return
- }
-
- // 使用拉流模式,将数据推入队列
- externalAudioStream?.pushAudio(data)
+
+ private fun isRecognizerValid(): Boolean {
+ return recognizer != null
}
-
/**
* 开始连续语音识别
*
@@ -136,7 +135,7 @@ public class AzureAsrHelper: NSObject {
* @return 是否成功开始识别
*/
public func startContinuousRecognition(
- callback: ContinuousRecognizeCallback,
+
audioSourceType: AudioSourceType = .microphone
) -> Bool {
guard speechConfig != nil else {
@@ -147,22 +146,24 @@ public class AzureAsrHelper: NSObject {
if _isContinuousRecognitionActive {
return true
}
-
+ if (!isRecognizerValid()) {
+ Log.w(tag, "识别器已失效,正在重新创建...")
+ if (!setupRecognizer()) {
+ Log.e(tag, "重新创建识别器失败")
+ return false
+ }
+ }
self.audioSourceType = audioSourceType
- // 重置识别器
- if !setupRecognizer() {
- callback.onError("重置识别器失败")
- return false
- }
+
do {
// 设置各种事件监听
- setupEventListeners(callback: callback)
+ //setupEventListeners(callback: callback)
// 启动音频处理
- startAudioProcessing()
-
+ // startAudioProcessing()
+ audioStream!!.startAudioRecord()
// 开始连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
@@ -201,7 +202,7 @@ public class AzureAsrHelper: NSObject {
}
if audioSourceType == .external {
- pushAudioData(data: Data())
+ //pushAudioData(data: Data())
}
// 停止连续识别
@@ -266,6 +267,38 @@ public class AzureAsrHelper: NSObject {
speechConfig = nil
}
}
+ // MARK: - 录音功能
+ /// 开启录音
+ func enableRecord(filePath: String) {
+ recordFile.stopRecord(isSave: false)
+ recordFile.creatingFiles(atPath: filePath)
+ }
+
+ /// 移动文件
+ func moveFile(sourcePath: String, destPath: String) -> Bool {
+ return recordFile.moveFile(from: sourcePath, to: destPath)
+ }
+
+ /// 重命名文件
+ func renameFile(at sourcePath: String, to newName: String) -> Bool {
+ return recordFile.renameFile(at: sourcePath, to: newName)
+ }
+
+ /// 暂停录音
+ func pauseRecord() {
+ recordFile.isPause = true
+ }
+
+ /// 停止录音
+ func stopRecord(isSave: Bool) {
+ recordFile.stopRecord(isSave: isSave)
+ }
+
+ /// 输入外部音频数据
+ func saveAudioDataTo(_ data: Data) {
+ guard audioSourceType == .external else { return }
+ writeQueue.offer(data.copyOf())
+ }
// MARK: - 私有方法
@@ -325,13 +358,32 @@ public class AzureAsrHelper: NSObject {
externalAudioStream = nil
}
}
+ /**
+ * 设置麦克风流 - 使用推流方式
+ */
+ private func setupMicrophoneStream() {
+ do {
+ // 创建麦克风流对象
+ audioStream = AudioStream()
+
+ // 创建音频配置
+ audioConfig = SPXAudioConfiguration(streamInput: audioStream!.pushAudioStream)
+ } catch {
+ os_log("设置麦克风流失败: %{public}@", log: log, type: .error, error.localizedDescription)
+ audioStream = nil
+ }
+ }
/**
* 设置事件监听器
*/
private func setupEventListeners(callback: ContinuousRecognizeCallback) {
guard let recognizer = recognizer else { return }
-
+ // 重置识别器
+ if !setupRecognizer() {
+ callback.onError("重置识别器失败")
+ return false
+ }
// 识别中事件
recognizer.addRecognizingEventHandler { [weak self] (sender, event) in
guard let self = self else { return }
@@ -432,7 +484,171 @@ public class AzureAsrHelper: NSObject {
}
}
-
+ /**
+ * 麦克风音频流处理
+ */
+ private class AudioStream: NSObject {
+ private var audioEngine: AVAudioEngine?
+ private var pushAudioStream: SPXPushAudioInputStream?
+ private var audioFile: AVAudioFile?
+ private var isRecording = false
+ private var recordFilePath: URL?
+ private let writeQueue = LinkedBlockingQueue()
+ override init() {
+ super.init()
+
+ // 创建推流对象
+ let format = SPXAudioStreamFormat(usingPCMWithSampleRate: 16000, bitsPerSample: 16, channels: 1)!
+ pushAudioStream = SPXPushAudioInputStream(audioFormat: format)
+ }
+
+ /**
+ * 开始音频捕获
+ */
+ func startAudioRecord() {
+ do {
+ // 配置音频会话
+ let session = AVAudioSession.sharedInstance()
+ try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth])
+ try session.setActive(true)
+
+ // 初始化音频引擎
+ audioEngine = AVAudioEngine()
+ guard let engine = audioEngine else { return }
+
+ // 获取输入节点
+ let inputNode = engine.inputNode
+ let inputFormat = inputNode.outputFormat(forBus: 0)
+
+ // 配置音频格式
+ let recordingFormat = AVAudioFormat(
+ commonFormat: .pcmFormatInt16,
+ sampleRate: 16000,
+ channels: 1,
+ interleaved: true
+ )!
+
+ // 安装Tap
+ inputNode.installTap(
+ onBus: 0,
+ bufferSize: 1024,
+ format: inputFormat
+ ) { [weak self] (buffer, time) in
+ self?.processAudioBuffer(buffer, format: recordingFormat)
+ }
+
+ // 启动引擎
+ try engine.start()
+ } catch {
+ os_log("音频捕获启动失败: %{public}@", type: .error, error.localizedDescription)
+ }
+ }
+
+ /**
+ * 停止音频捕获
+ */
+ func stopCapture() {
+ audioEngine?.stop()
+ audioEngine?.inputNode.removeTap(onBus: 0)
+ audioEngine = nil
+ stopRecord(isSave: true)
+ }
+
+ /**
+ * 处理音频缓冲区
+ */
+ private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, format: AVAudioFormat) {
+ guard let converter = AVAudioConverter(from: buffer.format, to: format) else { return }
+
+ // 设置音频配置
+ switch audioSourceType {
+ case .microphone:
+ // 使用默认麦克风输入配置
+ // 创建目标缓冲区
+ let targetFrameCapacity = AVAudioFrameCount(
+ (Double(buffer.frameCapacity) * format.sampleRate / buffer.format.sampleRate
+ )
+ guard let targetBuffer = AVAudioPCMBuffer(
+ pcmFormat: format,
+ frameCapacity: targetFrameCapacity
+ ) else { return }
+
+ // 转换音频格式
+ var error: NSError?
+ let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
+ outStatus.pointee = .haveData
+ return buffer
+ }
+
+ converter.convert(to: targetBuffer, error: &error, withInputFrom: inputBlock)
+
+ // 获取音频数据
+ guard let int16Data = targetBuffer.int16ChannelData else { return }
+ let data = Data(
+ bytes: int16Data[0],
+ count: Int(targetBuffer.frameLength) * MemoryLayout.size
+ )
+
+ case .external:
+ // 使用拉流方式处理外部音频
+ if (writeQueue.isNotEmpty()) {
+ let data = writeQueue.poll()
+ Log.d("tag", "写入数据: ${data?.size}")
+ bytesToWrite = data?.size ?: 0
+ }
+
+ }
+
+ // 推送到Azure流
+ pushAudioStream?.write(data)
+
+ // 保存到录音文件
+
+
+ // 触发音频回调
+ if let callback = AzureAsrHelper.shared?.currentContinuousCallback {
+ callback.onAudio(data)
+ }
+ }
+
+ /**
+ * 开启录音
+ */
+ func enableRecord(filePath: String) {
+ do {
+ recordFilePath = URL(fileURLWithPath: filePath)
+
+ let settings: [String: Any] = [
+ AVFormatIDKey: kAudioFormatLinearPCM,
+ AVSampleRateKey: 16000.0,
+ AVNumberOfChannelsKey: 1,
+ AVEncoderBitDepthHintKey: 16,
+ AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
+ ]
+
+ audioFile = try AVAudioFile(
+ forWriting: recordFilePath!,
+ settings: settings
+ )
+ isRecording = true
+ } catch {
+ os_log("录音文件创建失败: %{public}@", type: .error, error.localizedDescription)
+ }
+ }
+
+ /**
+ * 停止录音
+ */
+ func stopRecord(isSave: Bool) {
+ isRecording = false
+ audioFile = nil
+
+ if !isSave, let path = recordFilePath {
+ try? FileManager.default.removeItem(at: path)
+ }
+ recordFilePath = nil
+ }
+ }
/**
* 外部音频拉流
* 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
index 66e375647..7d2e55168 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
@@ -183,8 +183,8 @@ import os.log
result(FlutterError(code: "INVALID_ARGUMENTS", message: "音频数据不能为空", details: nil))
return
}
-
- azureAsrHelper.pushAudioData(data: audioBytes.data)
+ azureAsrHelper?.audioStream?.saveAudioDataTo(data: audioBytes.data)
+
result(true)
default:
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift
new file mode 100644
index 000000000..723e592d0
--- /dev/null
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift
@@ -0,0 +1,232 @@
+import Foundation
+
+class RecordFile {
+ static let shared = RecordFile()
+
+ private var currentAudioFile: URL?
+ private var fileHandle: FileHandle?
+ private let writeQueue = DispatchQueue(label: "audio.write.queue")
+ private let writeGroup = DispatchGroup()
+ private var isWriting = false
+ private var dataBuffer = Data()
+ private var totalBytesWritten = 0
+ private let sampleRate: UInt32 = 16000
+ private var lastSavedFile: URL?
+ var isPause = false
+ var fileName = ""
+
+ // 创建音频文件并初始化 WAV 头
+ func creatingFiles(atPath path: String) {
+ guard fileHandle == nil else { return }
+
+ let fileURL = URL(fileURLWithPath: path)
+ let directoryURL = fileURL.deletingLastPathComponent()
+
+ // 创建目录
+ do {
+ try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
+ } catch {
+ print("创建目录失败: \(error.localizedDescription)")
+ return
+ }
+
+ // 创建文件
+ FileManager.default.createFile(atPath: path, contents: nil)
+
+ do {
+ fileHandle = try FileHandle(forWritingTo: fileURL)
+ currentAudioFile = fileURL
+
+ // 写入初始 WAV 头
+ let header = generateWavHeader(dataLength: 0)
+ try fileHandle?.write(contentsOf: header)
+ totalBytesWritten = 0
+ } catch {
+ print("文件初始化失败: \(error.localizedDescription)")
+ fileHandle = nil
+ currentAudioFile = nil
+ }
+ }
+
+ // 异步保存音频数据
+ func saveAudioDataToWav(_ data: Data) {
+ guard let _ = fileHandle, let _ = currentAudioFile else { return }
+
+ writeQueue.async { [weak self] in
+ guard let self = self else { return }
+ self.dataBuffer.append(data)
+ self.totalBytesWritten += data.count
+
+ if !self.isWriting {
+ self.isWriting = true
+ self.writeGroup.enter()
+
+ defer {
+ self.isWriting = false
+ self.writeGroup.leave()
+ }
+
+ while !self.dataBuffer.isEmpty {
+ let chunk: Data
+ if self.dataBuffer.count > 8192 {
+ chunk = self.dataBuffer.prefix(8192)
+ self.dataBuffer.removeFirst(8192)
+ } else {
+ chunk = self.dataBuffer
+ self.dataBuffer.removeAll()
+ }
+
+ do {
+ try self.fileHandle?.seekToEnd()
+ self.fileHandle?.write(chunk)
+ } catch {
+ print("写入数据失败: \(error.localizedDescription)")
+ }
+ }
+ }
+ }
+ }
+
+ // 生成 WAV 文件头
+ private func generateWavHeader(dataLength: Int) -> Data {
+ let totalLength = 36 + dataLength
+ let byteRate = sampleRate * 2
+
+ var header = Data()
+ header.append("RIFF".data(using: .ascii)!)
+ header.append(Data(bytes: [
+ UInt8(totalLength & 0xFF),
+ UInt8((totalLength >> 8) & 0xFF),
+ UInt8((totalLength >> 16) & 0xFF),
+ UInt8((totalLength >> 24) & 0xFF)
+ ]))
+ header.append("WAVE".data(using: .ascii)!)
+ header.append("fmt ".data(using: .ascii)!)
+ header.append(Data(bytes: [16, 0, 0, 0])) // PCM 头长度
+ header.append(Data(bytes: [1, 0])) // PCM 格式
+ header.append(Data(bytes: [1, 0])) // 单声道
+ header.append(Data(bytes: [
+ UInt8(sampleRate & 0xFF),
+ UInt8((sampleRate >> 8) & 0xFF),
+ UInt8((sampleRate >> 16) & 0xFF),
+ UInt8((sampleRate >> 24) & 0xFF)
+ ]))
+ header.append(Data(bytes: [
+ UInt8(byteRate & 0xFF),
+ UInt8((byteRate >> 8) & 0xFF),
+ UInt8((byteRate >> 16) & 0xFF),
+ UInt8((byteRate >> 24) & 0xFF)
+ ]))
+ header.append(Data(bytes: [2, 0])) // 块对齐
+ header.append(Data(bytes: [16, 0])) // 样本位数
+ header.append("data".data(using: .ascii)!)
+ header.append(Data(bytes: [
+ UInt8(dataLength & 0xFF),
+ UInt8((dataLength >> 8) & 0xFF),
+ UInt8((dataLength >> 16) & 0xFF),
+ UInt8((dataLength >> 24) & 0xFF)
+ ]))
+
+ return header
+ }
+
+ // 重命名文件
+ func renameFile(at sourcePath: String, to newName: String) -> Bool {
+ let sourceURL = URL(fileURLWithPath: sourcePath)
+
+ guard FileManager.default.fileExists(atPath: sourcePath) else {
+ print("重命名失败: 文件不存在 \(sourcePath)")
+ return false
+ }
+
+ if let current = currentAudioFile, current == sourceURL {
+ print("重命名失败: 不能重命名正在录制的文件")
+ return false
+ }
+
+ let newFileURL = sourceURL
+ .deletingLastPathComponent()
+ .appendingPathComponent(newName)
+ .appendingPathExtension("wav")
+
+ do {
+ try FileManager.default.moveItem(at: sourceURL, to: newFileURL)
+ if lastSavedFile == sourceURL {
+ lastSavedFile = newFileURL
+ }
+ return true
+ } catch {
+ print("重命名异常: \(error.localizedDescription)")
+ return false
+ }
+ }
+
+ // 移动文件
+ func moveFile(from sourcePath: String, to destPath: String) -> Bool {
+ let sourceURL = URL(fileURLWithPath: sourcePath)
+ let destURL = URL(fileURLWithPath: destPath)
+
+ guard FileManager.default.fileExists(atPath: sourcePath) else {
+ print("移动失败: 源文件不存在 \(sourcePath)")
+ return false
+ }
+
+ let destDir = destURL.deletingLastPathComponent()
+ do {
+ try FileManager.default.createDirectory(at: destDir, withIntermediateDirectories: true)
+ } catch {
+ print("创建目录失败: \(error.localizedDescription)")
+ return false
+ }
+
+ do {
+ try FileManager.default.moveItem(at: sourceURL, to: destURL)
+ if lastSavedFile == sourceURL {
+ lastSavedFile = destURL
+ }
+ return true
+ } catch {
+ print("移动文件异常: \(error.localizedDescription)")
+ return false
+ }
+ }
+
+ // 关闭文件
+ func closeFile(isSave: Bool) -> Bool {
+ defer {
+ fileHandle = nil
+ currentAudioFile = nil
+ dataBuffer.removeAll()
+ totalBytesWritten = 0
+ }
+
+ guard let fileURL = currentAudioFile, let handle = fileHandle else {
+ return false
+ }
+
+ // 等待写入完成
+ writeGroup.wait()
+
+ do {
+ try handle.close()
+
+ if !isSave || totalBytesWritten == 0 {
+ try FileManager.default.removeItem(at: fileURL)
+ return false
+ }
+
+ // 更新 WAV 头
+ let header = generateWavHeader(dataLength: totalBytesWritten)
+ let updateHandle = try FileHandle(forUpdating: fileURL)
+ try updateHandle.seek(toOffset: 0)
+ updateHandle.write(header)
+ try updateHandle.close()
+
+ lastSavedFile = fileURL
+ return true
+ } catch {
+ print("关闭文件失败: \(error.localizedDescription)")
+ return false
+ }
+ }
+}
\ No newline at end of file
From 39faee590c25407ec6cb88e609303be74bb06586 Mon Sep 17 00:00:00 2001
From: fdp <1286779656@qq.com>
Date: Fri, 11 Jul 2025 19:24:29 +0800
Subject: [PATCH 05/20] add
---
.../agent_service/AgentServiceImpl.swift | 2 +-
.../azure_speech/AzureAsrHelper.kt | 3 +-
.../Sources/azure_speech/AzureAsrHelper.swift | 274 ++----------------
.../azure_speech/AzureSpeechPlugin.swift | 4 +-
.../{tools => azure_speech}/RecordFile.swift | 0
5 files changed, 34 insertions(+), 249 deletions(-)
rename local_plugins/azure_speech/ios/azure_speech/Sources/{tools => azure_speech}/RecordFile.swift (100%)
diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
index a80a0d7cd..251984c27 100644
--- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
+++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
@@ -347,7 +347,7 @@ class AgentServiceImpl: NSObject {
return false
}
- azureAsrHelper?.audioStream?.saveAudioDataTo(data)
+ azureAsrHelper?.pushAudioData(data: audioData)
return true
}
diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
index d5f98efca..3adf31b59 100644
--- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
+++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
@@ -171,7 +171,8 @@ class AzureAsrHelper(private val context: Context) {
}
setupMicrophoneStream()
-
+ recognizer?.close()
+ recognizer = null
//创建识别器
recognizer = if (isAutoDetectLanguage) {
val autoDetectConfig =
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
index ce81a449d..b0f4bcb02 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
@@ -31,13 +31,7 @@ public class AzureAsrHelper: NSObject {
private var isAutoDetectLanguage = false
private var subscriptionKey = ""
private var region = ""
- // 音频处理
- private let audioQueue = DispatchQueue(label: "audio.processing.queue")
- private var audioEngine: AVAudioEngine?
- private var audioBuffer: Data?
- // 录音文件处理
- private let recordFile = RecordFile.shared
// 音频源配置
public enum AudioSourceType {
/** 使用设备麦克风 */
@@ -51,11 +45,6 @@ public class AzureAsrHelper: NSObject {
// 音频处理
private var externalAudioStream: ExternalAudioPullStream?
-
- private var audioStream: AudioStream?
-
- // 当前回调
- private weak var currentContinuousCallback: ContinuousRecognizeCallback?
/**
* 初始化Azure语音服务
@@ -117,16 +106,28 @@ public class AzureAsrHelper: NSObject {
}
// 创建识别器
- return true
+ return setupRecognizer()
} catch {
os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
return false
}
}
-
- private fun isRecognizerValid(): Boolean {
- return recognizer != null
+
+ /**
+ * 向音频流写入音频数据
+ * 仅当音频源设置为external时有效
+ *
+ * @param data 音频数据字节数组
+ */
+ public func pushAudioData(data: Data) {
+ if audioSourceType != .external {
+ return
+ }
+
+ // 使用拉流模式,将数据推入队列
+ externalAudioStream?.pushAudio(data)
}
+
/**
* 开始连续语音识别
*
@@ -135,7 +136,7 @@ public class AzureAsrHelper: NSObject {
* @return 是否成功开始识别
*/
public func startContinuousRecognition(
-
+ callback: ContinuousRecognizeCallback,
audioSourceType: AudioSourceType = .microphone
) -> Bool {
guard speechConfig != nil else {
@@ -146,24 +147,22 @@ public class AzureAsrHelper: NSObject {
if _isContinuousRecognitionActive {
return true
}
- if (!isRecognizerValid()) {
- Log.w(tag, "识别器已失效,正在重新创建...")
- if (!setupRecognizer()) {
- Log.e(tag, "重新创建识别器失败")
- return false
- }
- }
+
self.audioSourceType = audioSourceType
-
+ // 重置识别器
+ if !setupRecognizer() {
+ callback.onError("重置识别器失败")
+ return false
+ }
do {
// 设置各种事件监听
- //setupEventListeners(callback: callback)
+ setupEventListeners(callback: callback)
// 启动音频处理
- // startAudioProcessing()
- audioStream!!.startAudioRecord()
+ startAudioProcessing()
+
// 开始连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
@@ -202,7 +201,7 @@ public class AzureAsrHelper: NSObject {
}
if audioSourceType == .external {
- //pushAudioData(data: Data())
+ pushAudioData(data: Data())
}
// 停止连续识别
@@ -267,38 +266,6 @@ public class AzureAsrHelper: NSObject {
speechConfig = nil
}
}
- // MARK: - 录音功能
- /// 开启录音
- func enableRecord(filePath: String) {
- recordFile.stopRecord(isSave: false)
- recordFile.creatingFiles(atPath: filePath)
- }
-
- /// 移动文件
- func moveFile(sourcePath: String, destPath: String) -> Bool {
- return recordFile.moveFile(from: sourcePath, to: destPath)
- }
-
- /// 重命名文件
- func renameFile(at sourcePath: String, to newName: String) -> Bool {
- return recordFile.renameFile(at: sourcePath, to: newName)
- }
-
- /// 暂停录音
- func pauseRecord() {
- recordFile.isPause = true
- }
-
- /// 停止录音
- func stopRecord(isSave: Bool) {
- recordFile.stopRecord(isSave: isSave)
- }
-
- /// 输入外部音频数据
- func saveAudioDataTo(_ data: Data) {
- guard audioSourceType == .external else { return }
- writeQueue.offer(data.copyOf())
- }
// MARK: - 私有方法
@@ -358,32 +325,13 @@ public class AzureAsrHelper: NSObject {
externalAudioStream = nil
}
}
- /**
- * 设置麦克风流 - 使用推流方式
- */
- private func setupMicrophoneStream() {
- do {
- // 创建麦克风流对象
- audioStream = AudioStream()
-
- // 创建音频配置
- audioConfig = SPXAudioConfiguration(streamInput: audioStream!.pushAudioStream)
- } catch {
- os_log("设置麦克风流失败: %{public}@", log: log, type: .error, error.localizedDescription)
- audioStream = nil
- }
- }
/**
* 设置事件监听器
*/
private func setupEventListeners(callback: ContinuousRecognizeCallback) {
guard let recognizer = recognizer else { return }
- // 重置识别器
- if !setupRecognizer() {
- callback.onError("重置识别器失败")
- return false
- }
+
// 识别中事件
recognizer.addRecognizingEventHandler { [weak self] (sender, event) in
guard let self = self else { return }
@@ -484,171 +432,7 @@ public class AzureAsrHelper: NSObject {
}
}
- /**
- * 麦克风音频流处理
- */
- private class AudioStream: NSObject {
- private var audioEngine: AVAudioEngine?
- private var pushAudioStream: SPXPushAudioInputStream?
- private var audioFile: AVAudioFile?
- private var isRecording = false
- private var recordFilePath: URL?
- private let writeQueue = LinkedBlockingQueue()
- override init() {
- super.init()
-
- // 创建推流对象
- let format = SPXAudioStreamFormat(usingPCMWithSampleRate: 16000, bitsPerSample: 16, channels: 1)!
- pushAudioStream = SPXPushAudioInputStream(audioFormat: format)
- }
-
- /**
- * 开始音频捕获
- */
- func startAudioRecord() {
- do {
- // 配置音频会话
- let session = AVAudioSession.sharedInstance()
- try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth])
- try session.setActive(true)
-
- // 初始化音频引擎
- audioEngine = AVAudioEngine()
- guard let engine = audioEngine else { return }
-
- // 获取输入节点
- let inputNode = engine.inputNode
- let inputFormat = inputNode.outputFormat(forBus: 0)
-
- // 配置音频格式
- let recordingFormat = AVAudioFormat(
- commonFormat: .pcmFormatInt16,
- sampleRate: 16000,
- channels: 1,
- interleaved: true
- )!
-
- // 安装Tap
- inputNode.installTap(
- onBus: 0,
- bufferSize: 1024,
- format: inputFormat
- ) { [weak self] (buffer, time) in
- self?.processAudioBuffer(buffer, format: recordingFormat)
- }
-
- // 启动引擎
- try engine.start()
- } catch {
- os_log("音频捕获启动失败: %{public}@", type: .error, error.localizedDescription)
- }
- }
-
- /**
- * 停止音频捕获
- */
- func stopCapture() {
- audioEngine?.stop()
- audioEngine?.inputNode.removeTap(onBus: 0)
- audioEngine = nil
- stopRecord(isSave: true)
- }
-
- /**
- * 处理音频缓冲区
- */
- private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, format: AVAudioFormat) {
- guard let converter = AVAudioConverter(from: buffer.format, to: format) else { return }
-
- // 设置音频配置
- switch audioSourceType {
- case .microphone:
- // 使用默认麦克风输入配置
- // 创建目标缓冲区
- let targetFrameCapacity = AVAudioFrameCount(
- (Double(buffer.frameCapacity) * format.sampleRate / buffer.format.sampleRate
- )
- guard let targetBuffer = AVAudioPCMBuffer(
- pcmFormat: format,
- frameCapacity: targetFrameCapacity
- ) else { return }
-
- // 转换音频格式
- var error: NSError?
- let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
- outStatus.pointee = .haveData
- return buffer
- }
-
- converter.convert(to: targetBuffer, error: &error, withInputFrom: inputBlock)
-
- // 获取音频数据
- guard let int16Data = targetBuffer.int16ChannelData else { return }
- let data = Data(
- bytes: int16Data[0],
- count: Int(targetBuffer.frameLength) * MemoryLayout.size
- )
-
- case .external:
- // 使用拉流方式处理外部音频
- if (writeQueue.isNotEmpty()) {
- let data = writeQueue.poll()
- Log.d("tag", "写入数据: ${data?.size}")
- bytesToWrite = data?.size ?: 0
- }
-
- }
-
- // 推送到Azure流
- pushAudioStream?.write(data)
-
- // 保存到录音文件
-
-
- // 触发音频回调
- if let callback = AzureAsrHelper.shared?.currentContinuousCallback {
- callback.onAudio(data)
- }
- }
-
- /**
- * 开启录音
- */
- func enableRecord(filePath: String) {
- do {
- recordFilePath = URL(fileURLWithPath: filePath)
-
- let settings: [String: Any] = [
- AVFormatIDKey: kAudioFormatLinearPCM,
- AVSampleRateKey: 16000.0,
- AVNumberOfChannelsKey: 1,
- AVEncoderBitDepthHintKey: 16,
- AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
- ]
-
- audioFile = try AVAudioFile(
- forWriting: recordFilePath!,
- settings: settings
- )
- isRecording = true
- } catch {
- os_log("录音文件创建失败: %{public}@", type: .error, error.localizedDescription)
- }
- }
-
- /**
- * 停止录音
- */
- func stopRecord(isSave: Bool) {
- isRecording = false
- audioFile = nil
-
- if !isSave, let path = recordFilePath {
- try? FileManager.default.removeItem(at: path)
- }
- recordFilePath = nil
- }
- }
+
/**
* 外部音频拉流
* 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
index 7d2e55168..66e375647 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
@@ -183,8 +183,8 @@ import os.log
result(FlutterError(code: "INVALID_ARGUMENTS", message: "音频数据不能为空", details: nil))
return
}
- azureAsrHelper?.audioStream?.saveAudioDataTo(data: audioBytes.data)
-
+
+ azureAsrHelper.pushAudioData(data: audioBytes.data)
result(true)
default:
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/RecordFile.swift
similarity index 100%
rename from local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift
rename to local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/RecordFile.swift
From 85f37766a7bc3f95afeb8c343af5ed1b0133a021 Mon Sep 17 00:00:00 2001
From: liwei1dao
Date: Mon, 14 Jul 2025 11:57:16 +0800
Subject: [PATCH 06/20] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E8=8B=B9=E6=9E=9C?=
=?UTF-8?q?=E7=89=88=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ios/Podfile.lock | 4 +-
ios/Runner.xcodeproj/project.pbxproj | 4 +-
ios/Runner/Info.plist | 25 +-
lib/data/services/ble_manager.dart | 10 +
.../controllers/pairing_controller.dart | 46 +-
.../Sources/ble_service/BleScanManager.swift | 586 ++++++++++++++++++
.../ble_service/SwiftBleServicePlugin.swift | 26 +-
.../ble_service/lib/ble_service.dart | 11 +
.../ota/ios/Classes/BleManager/JLBleManager.m | 1 +
9 files changed, 689 insertions(+), 24 deletions(-)
create mode 100644 local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index d0bbc4669..c9050a312 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -85,7 +85,7 @@ PODS:
- SDWebImage/Core (~> 5.17)
- spotify_sdk (0.0.1):
- Flutter
- - tencentcloud_cos_sdk_plugin (1.2.3):
+ - tencentcloud_cos_sdk_plugin (1.2.4):
- Flutter
- QCloudCOSXML (= 6.4.7)
- WechatOpenSDK-XCFramework (2.0.4)
@@ -203,7 +203,7 @@ SPEC CHECKSUMS:
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca
- tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b
+ tencentcloud_cos_sdk_plugin: 7bed564dbe72df23e7b5cacb1e51b1a20cb1639c
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 4df97502d..02daa62e1 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -252,7 +252,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
D013B7972DFA83E400B2B69B /* XCRemoteSwiftPackageReference "flutterfire" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
@@ -793,7 +793,7 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index bc3f4a793..b2cdd80bb 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -85,10 +85,6 @@
语音助理耳机
- NSAccessorySetupKitSupports
-
- Bluetooth
-
NSAppTransportSecurity
NSAllowsArbitraryLoads
@@ -117,10 +113,25 @@
+
NSBluetoothAlwaysUsageDescription
需要蓝牙权限用于连接和管理音频设备
NSBluetoothPeripheralUsageDescription
需要蓝牙权限用于连接和管理音频设备
+
+
+ UIBackgroundModes
+
+ bluetooth-central
+ audio
+ location
+
+
+
+ NSAccessorySetupKitSupports
+
+ Bluetooth
+
NSCalendarsUsageDescription
需要日历权限用于安排和管理您的音频会议和录音任务
NSCameraUsageDescription
@@ -137,12 +148,6 @@
需要相册权限用于保存录音文件和导入音频文件
UIApplicationSupportsIndirectInputEvents
- UIBackgroundModes
-
- bluetooth-central
- audio
- location
-
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart
index 3cbc033c3..5d131da39 100644
--- a/lib/data/services/ble_manager.dart
+++ b/lib/data/services/ble_manager.dart
@@ -859,4 +859,14 @@ class BleManager extends GetxService {
}
return Future.value(Get.find());
}
+
+ /// 请求蓝牙权限
+ Future requestBluetoothPermission() async {
+ try {
+ return await _bleService.requestBluetoothPermission();
+ } catch (e) {
+ Logger.error('请求蓝牙权限失败: ${e.toString()}');
+ return false;
+ }
+ }
}
diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart
index 53b1690fb..bed5baf39 100644
--- a/lib/modules/pairing/controllers/pairing_controller.dart
+++ b/lib/modules/pairing/controllers/pairing_controller.dart
@@ -418,17 +418,45 @@ class PairingController extends GetxController {
/// 检查权限
Future _checkPermissions() async {
- // Android权限检查
if (Platform.isAndroid) {
- // 检查蓝牙相关权限
- final bluetoothScan = await Permission.bluetoothScan.request();
- final bluetoothConnect = await Permission.bluetoothConnect.request();
- final location = await Permission.locationWhenInUse.request();
-
- return bluetoothScan.isGranted &&
- bluetoothConnect.isGranted &&
- location.isGranted;
+ // Android 平台权限检查
+ Map statuses = await [
+ Permission.bluetoothScan,
+ Permission.bluetoothConnect,
+ Permission.locationWhenInUse,
+ ].request();
+
+ return statuses[Permission.bluetoothScan]!.isGranted &&
+ statuses[Permission.bluetoothConnect]!.isGranted &&
+ statuses[Permission.locationWhenInUse]!.isGranted;
+ } else if (Platform.isIOS) {
+ // iOS 平台权限检查
+ Map statuses = await [
+ Permission.locationWhenInUse,
+ Permission.bluetooth, // 添加蓝牙权限检查
+ ].request();
+
+ // 检查位置权限
+ bool locationGranted = statuses[Permission.locationWhenInUse]!.isGranted;
+
+ // 检查蓝牙权限
+ bool bluetoothGranted = statuses[Permission.bluetooth]!.isGranted;
+
+ // 如果蓝牙权限未确定,强制触发权限申请
+ if (!bluetoothGranted) {
+ try {
+ await _bleManager.requestBluetoothPermission();
+ // 重新检查蓝牙权限状态
+ final newBluetoothStatus = await Permission.bluetooth.status;
+ bluetoothGranted = newBluetoothStatus.isGranted;
+ } catch (e) {
+ Logger.e(_tag, '蓝牙权限申请失败: $e');
+ }
+ }
+
+ return locationGranted && bluetoothGranted;
}
+
return true;
}
diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift
new file mode 100644
index 000000000..1be2b0c46
--- /dev/null
+++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift
@@ -0,0 +1,586 @@
+import Foundation
+import CoreBluetooth
+import UIKit // 添加UIKit导入
+import os.log
+
+/// 简化的 BLE 扫描管理器
+/// 只提供扫描和获取设备列表功能
+@available(iOS 13.0, *)
+public class BleScanManager: NSObject {
+
+ // MARK: - 单例
+ public static let shared = BleScanManager()
+
+ // MARK: - 数据模型
+ public struct BleDeviceInfo {
+ public let identifier: UUID
+ public let name: String?
+ public let rssi: NSNumber
+ public let advertisementData: [String: Any]
+ public let discoveryMethod: DiscoveryMethod
+ public let lastSeen: Date
+
+ public enum DiscoveryMethod {
+ case traditional
+ case accessorySetupKit
+ }
+ }
+
+ // MARK: - 属性
+ private var centralManager: CBCentralManager!
+ private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleScanManager")
+
+ // 扫描状态
+ private var isScanning = false
+ private var scanTimeout: Timer?
+
+ // 设备列表
+ private var discoveredDevices: [UUID: BleDeviceInfo] = [:]
+
+
+ // MARK: - 初始化
+ private override init() {
+ super.init()
+ setupCentralManager()
+ }
+
+ // 在setupCentralManager后添加权限检查
+ private func setupCentralManager() {
+ let options: [String: Any] = [
+ CBCentralManagerOptionShowPowerAlertKey: true,
+ CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager"
+ ]
+
+ centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
+
+ // 添加权限检查和触发逻辑
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+ self?.checkAndTriggerPermission()
+ }
+ }
+
+ private func checkAndTriggerPermission() {
+ // 检查蓝牙权限状态,兼容不同iOS版本
+ let authorization: CBManagerAuthorization
+ if #available(iOS 13.1, *) {
+ authorization = CBManager.authorization
+ } else if #available(iOS 13.0, *) {
+ // iOS 13.0 使用实例方法,需要确保centralManager已初始化
+ authorization = centralManager?.authorization ?? .notDetermined
+ } else {
+ // iOS 13.0以下版本没有权限检查API,默认为未确定
+ authorization = .notDetermined
+ }
+
+ os_log("📊 蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue)
+ os_log("📊 CBCentralManager状态: %d", log: logger, type: .info, centralManager?.state.rawValue ?? -1)
+
+ // 只在权限未确定且蓝牙状态为关闭时触发扫描
+ if authorization == .notDetermined && centralManager?.state == .poweredOff {
+ os_log("📊 主动触发一次扫描来申请权限", log: logger, type: .info)
+ // 主动触发一次扫描来申请权限
+ centralManager?.scanForPeripherals(withServices: nil, options: nil)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
+ self?.centralManager?.stopScan()
+ }
+ }
+ }
+
+
+
+ // MARK: - 公共接口
+
+ /// 开始扫描 BLE 设备
+ /// - Parameter timeout: 扫描超时时间,默认 10 秒
+ /// - Returns: 是否成功开始扫描
+ public func startScan(timeout: TimeInterval = 30.0) -> Bool {
+ os_log("🔍 开始 BLE 扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout)
+
+ guard !isScanning else {
+ os_log("⚠️ 扫描已在进行中", log: logger, type: .info)
+ return false
+ }
+
+ // 清空之前的设备列表
+ discoveredDevices.removeAll()
+
+ // iOS 18 兼容性检查
+ if #available(iOS 18.0, *) {
+ // iOS 18 使用新的权限检查方式
+ return startScanForIOS18(timeout: timeout)
+ } else {
+ // iOS 17 及以下使用传统方式
+ return startScanLegacy(timeout: timeout)
+ }
+ }
+
+ @available(iOS 18.0, *)
+ private func startScanForIOS18(timeout: TimeInterval) -> Bool {
+ // 对于 iOS 18,直接检查 CBCentralManager 状态
+ guard let centralManager = centralManager else {
+ os_log("❌ CBCentralManager 未初始化", log: logger, type: .error)
+ return false
+ }
+
+ let currentState = centralManager.state
+ os_log("📊 iOS 18 蓝牙状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState))
+
+ switch currentState {
+ case .poweredOn:
+ return performScanWithPermission(timeout: timeout)
+ case .unauthorized:
+ os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error)
+ showPermissionAlert()
+ return false
+ case .unknown:
+ os_log("⚠️ 蓝牙状态未知,等待状态更新", log: logger, type: .info)
+ // 设置监听器等待状态更新
+ waitForBluetoothState(timeout: timeout)
+ return true
+ default:
+ os_log("❌ 蓝牙不可用: %{public}@", log: logger, type: .error, getBluetoothStateString(currentState))
+ return false
+ }
+ }
+
+ private func startScanLegacy(timeout: TimeInterval) -> Bool {
+ // 检查并申请蓝牙权限
+ let authorization: CBManagerAuthorization = CBManager.authorization
+ os_log("📊 当前蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue)
+
+ switch authorization {
+ case .allowedAlways:
+ return performScanWithPermission(timeout: timeout)
+ case .denied, .restricted:
+ os_log("❌ 蓝牙权限被拒绝或受限", log: logger, type: .error)
+ showPermissionAlert()
+ return false
+ case .notDetermined:
+ // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗
+ if centralManager == nil {
+ setupCentralManager()
+ }
+
+ // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
+ guard let self = self else { return }
+ if self.centralManager.state == .poweredOn {
+ // 开始一个短暂的扫描来触发权限申请
+ self.centralManager.scanForPeripherals(withServices: nil, options: nil)
+ // 立即停止扫描
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ self.centralManager.stopScan()
+ }
+ } else if self.centralManager.state == .unauthorized {
+ // 权限被拒绝,显示设置提示
+ self.showPermissionAlert()
+ }
+ }
+ return true
+ @unknown default:
+ os_log("⚠️ 未知的蓝牙权限状态", log: logger, type: .info)
+ return false
+ }
+ }
+
+ private func waitForBluetoothState(timeout: TimeInterval) {
+ var checkCount = 0
+ let maxChecks = 20
+
+ let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
+ guard let self = self else {
+ timer.invalidate()
+ return
+ }
+
+ checkCount += 1
+ let currentState = self.centralManager?.state ?? .unknown
+
+ os_log("🔍 检查蓝牙状态 (%d/%d): %{public}@", log: self.logger, type: .info, checkCount, maxChecks, self.getBluetoothStateString(currentState))
+
+ switch currentState {
+ case .poweredOn:
+ timer.invalidate()
+ _ = self.performScanWithPermission(timeout: timeout)
+ case .unauthorized:
+ timer.invalidate()
+ self.showPermissionAlert()
+ case .poweredOff, .unsupported:
+ timer.invalidate()
+ os_log("❌ 蓝牙不可用", log: self.logger, type: .error)
+ default:
+ if checkCount >= maxChecks {
+ timer.invalidate()
+ os_log("⏰ 等待蓝牙状态超时", log: self.logger, type: .error)
+ }
+ }
+ }
+ }
+
+ private func forceTriggerPermissionRequest(timeout: TimeInterval) {
+ // 完全清理旧的 centralManager
+ centralManager?.delegate = nil
+ centralManager = nil
+
+ // 强制重新初始化,确保触发权限申请
+ let options: [String: Any] = [
+ CBCentralManagerOptionShowPowerAlertKey: true,
+ CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager"
+ ]
+
+ centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
+
+ // 设置一个监听器来等待权限状态更新
+ waitForPermissionResponse(timeout: timeout)
+ }
+
+ private func waitForPermissionResponse(timeout: TimeInterval) {
+ // 设置一个定时器来检查权限状态变化
+ var checkCount = 0
+ let maxChecks = 20 // 最多检查20次,每次间隔0.5秒,总共10秒
+
+ let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
+ guard let self = self else {
+ timer.invalidate()
+ return
+ }
+
+ checkCount += 1
+ let currentAuth = CBManager.authorization
+
+ os_log("🔍 检查权限状态 (%d/%d): %d", log: self.logger, type: .info, checkCount, maxChecks, currentAuth.rawValue)
+
+ switch currentAuth {
+ case .allowedAlways:
+ timer.invalidate()
+ os_log("✅ 蓝牙权限已获得,开始扫描", log: self.logger, type: .info)
+ _ = self.performScanWithPermission(timeout: timeout)
+ case .denied, .restricted:
+ timer.invalidate()
+ os_log("❌ 蓝牙权限被拒绝", log: self.logger, type: .error)
+ self.showPermissionAlert()
+ case .notDetermined:
+ if checkCount >= maxChecks {
+ timer.invalidate()
+ os_log("⏰ 等待权限响应超时", log: self.logger, type: .error)
+ self.showPermissionAlert()
+ }
+ // 继续等待
+ @unknown default:
+ timer.invalidate()
+ os_log("⚠️ 未知权限状态", log: self.logger, type: .error)
+ }
+ }
+ }
+
+ private func reinitializeCentralManager() {
+ // 清理旧的 centralManager
+ centralManager?.delegate = nil
+ centralManager = nil
+
+ // 重新初始化
+ setupCentralManager()
+ }
+
+ /// 检查蓝牙权限状态
+ private func checkBluetoothPermissionStatus() -> (hasPermission: Bool, needsRequest: Bool) {
+ // 检查权限状态
+ let authorization: CBManagerAuthorization
+ if #available(iOS 13.1, *) {
+ authorization = CBManager.authorization
+ } else if #available(iOS 13.0, *) {
+ if centralManager == nil {
+ setupCentralManager()
+ }
+ authorization = centralManager?.authorization ?? .notDetermined
+ } else {
+ // iOS 13以下没有权限概念
+ return (true, false)
+ }
+
+ os_log("📊 蓝牙权限状态: %d, CBCentralManager状态: %d", log: logger, type: .info, authorization.rawValue, centralManager?.state.rawValue ?? -1)
+
+ switch authorization {
+ case .allowedAlways:
+ return (true, false)
+ case .denied, .restricted:
+ return (false, false)
+ case .notDetermined:
+ return (false, true)
+ @unknown default:
+ return (false, true)
+ }
+ }
+
+ private func performScanWithPermission(timeout: TimeInterval) -> Bool {
+ // 首先检查权限状态
+ let permissionStatus = checkBluetoothPermissionStatus()
+
+ if !permissionStatus.hasPermission {
+ if permissionStatus.needsRequest {
+ os_log("🔐 需要申请蓝牙权限", log: logger, type: .info)
+ _ = requestBluetoothPermission()
+ return false
+ } else {
+ os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error)
+ showPermissionAlert()
+ return false
+ }
+ }
+
+ // 权限已获得,检查蓝牙硬件状态
+ let currentState = centralManager.state
+ os_log("📊 蓝牙硬件状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState))
+
+ switch currentState {
+ case .poweredOn:
+ // 蓝牙已开启,可以扫描
+ break
+ case .unknown:
+ os_log("⚠️ 蓝牙状态未知,等待状态更新...", log: logger, type: .info)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+ _ = self?.performScanWithPermission(timeout: timeout)
+ }
+ return false
+ case .poweredOff:
+ // 区分是权限问题还是硬件关闭
+ if permissionStatus.hasPermission {
+ os_log("❌ 蓝牙硬件已关闭,请在设备设置中开启蓝牙", log: logger, type: .error)
+ } else {
+ os_log("❌ 可能是权限问题导致的蓝牙状态异常", log: logger, type: .error)
+ _ = requestBluetoothPermission()
+ }
+ return false
+ case .unauthorized:
+ os_log("❌ 应用未授权使用蓝牙", log: logger, type: .error)
+ showPermissionAlert()
+ return false
+ case .unsupported:
+ os_log("❌ 设备不支持蓝牙", log: logger, type: .error)
+ return false
+ default:
+ os_log("⚠️ 其他蓝牙状态: %d", log: logger, type: .error, currentState.rawValue)
+ return false
+ }
+
+ // 开始BLE扫描
+ centralManager.scanForPeripherals(withServices: nil, options: [
+ CBCentralManagerScanOptionAllowDuplicatesKey: true
+ ])
+
+ isScanning = true
+
+ // 设置扫描超时
+ scanTimeout = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
+ self?.stopScan()
+ }
+
+ os_log("✅ 开始BLE扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout)
+ return true
+ }
+
+ private func showPermissionAlert() {
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let window = windowScene.windows.first else {
+ os_log("❌ 无法获取当前窗口", log: logger, type: .error)
+ return
+ }
+
+ let alert = UIAlertController(
+ title: "需要蓝牙权限",
+ message: "请在设置中开启蓝牙权限以使用设备扫描功能",
+ preferredStyle: .alert
+ )
+
+ alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
+ if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
+ UIApplication.shared.open(settingsUrl)
+ }
+ })
+
+ alert.addAction(UIAlertAction(title: "取消", style: .cancel))
+
+ window.rootViewController?.present(alert, animated: true)
+ }
+
+ /// 停止扫描
+ public func stopScan() {
+ guard isScanning else { return }
+
+ os_log("🛑 停止 BLE 扫描", log: logger, type: .info)
+
+ centralManager.stopScan() // 直接使用,不需要可选绑定
+ isScanning = false
+
+ scanTimeout?.invalidate()
+ scanTimeout = nil
+ }
+
+ /// 获取已发现的设备列表
+ /// - Returns: 按最后发现时间排序的设备列表
+ public func getDiscoveredDevices() -> [BleDeviceInfo] {
+ return Array(discoveredDevices.values).sorted { $0.lastSeen > $1.lastSeen }
+ }
+
+ /// 清除设备列表
+ public func clearDeviceList() {
+ discoveredDevices.removeAll()
+ os_log("🗑️ 已清除设备列表", log: logger, type: .info)
+ }
+
+ /// 检查是否正在扫描
+ public func isScanningActive() -> Bool {
+ return isScanning
+ }
+
+ /// 获取蓝牙状态
+ public func getBluetoothState() -> CBManagerState {
+ return centralManager.state // 直接返回,不需要 ?? .unknown
+ }
+
+ // MARK: - 私有方法
+
+
+
+ private func addOrUpdateDevice(_ deviceInfo: BleDeviceInfo) {
+ let identifier = deviceInfo.identifier
+ discoveredDevices[identifier] = deviceInfo
+
+ os_log("📱 发现设备: %{public}@", log: logger, type: .debug, deviceInfo.name ?? "Unknown")
+ }
+
+ /// 强制触发蓝牙权限申请
+ public func requestBluetoothPermission() -> Bool {
+ os_log("🔐 强制触发蓝牙权限申请", log: logger, type: .info)
+
+ // 检查当前权限状态
+ let authorization: CBManagerAuthorization
+ if #available(iOS 13.1, *) {
+ authorization = CBManager.authorization
+ } else if #available(iOS 13.0, *) {
+ // 确保centralManager已初始化
+ if centralManager == nil {
+ setupCentralManager()
+ }
+ authorization = centralManager.authorization
+ } else {
+ // iOS 13以下没有权限概念,直接返回true
+ return true
+ }
+
+ os_log("📊 当前权限状态: %d", log: logger, type: .info, authorization.rawValue)
+
+ switch authorization {
+ case .notDetermined:
+ // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗
+ if centralManager == nil {
+ os_log("📊 初始化蓝牙管理对象", log: logger, type: .info, "")
+ setupCentralManager()
+ }
+
+ // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
+ guard let self = self else { return }
+
+ if self.centralManager.state == .poweredOn {
+ // 开始一个短暂的扫描来触发权限申请
+ self.centralManager.scanForPeripherals(withServices: nil, options: nil)
+ // 立即停止扫描
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ self.centralManager.stopScan()
+ }
+ } else if self.centralManager.state == .unauthorized {
+ // 权限被拒绝,显示设置提示
+ self.showPermissionAlert()
+ }
+ }
+ return true
+ case .allowedAlways:
+ os_log("✅ 权限已授予", log: logger, type: .info)
+ return true
+ case .denied, .restricted:
+ os_log("❌ 权限被拒绝,显示设置提示", log: logger, type: .error)
+ showPermissionAlert()
+ return false
+ @unknown default:
+ os_log("⚠️ 未知权限状态", log: logger, type: .info)
+ return false
+ }
+ }
+}
+
+// MARK: - CBCentralManagerDelegate
+@available(iOS 13.0, *)
+extension BleScanManager: CBCentralManagerDelegate {
+
+ public func centralManagerDidUpdateState(_ central: CBCentralManager) {
+ let stateString = getBluetoothStateString(central.state)
+ os_log("🔧 CBCentralManager 状态更新: %{public}@ (原始值: %d)", log: logger, type: .info, stateString, central.state.rawValue)
+
+ switch central.state {
+ case .poweredOn:
+ os_log("✅ 蓝牙已开启,可以开始扫描", log: logger, type: .info)
+ // 如果有待处理的扫描请求,现在可以执行
+ case .poweredOff:
+ os_log("❌ 蓝牙已关闭", log: logger, type: .error)
+ if isScanning {
+ stopScan()
+ }
+ case .unauthorized:
+ os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error)
+ if isScanning {
+ stopScan()
+ }
+ case .unsupported:
+ os_log("❌ 设备不支持蓝牙", log: logger, type: .error)
+ if isScanning {
+ stopScan()
+ }
+ case .unknown:
+ os_log("⚠️ 蓝牙状态未知,等待进一步更新", log: logger, type: .error)
+ case .resetting:
+ os_log("⚠️ 蓝牙正在重置", log: logger, type: .error)
+ if isScanning {
+ stopScan()
+ }
+ @unknown default:
+ os_log("⚠️ 未知蓝牙状态: %d", log: logger, type: .error, central.state.rawValue)
+ }
+ }
+
+ public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
+ advertisementData: [String : Any], rssi RSSI: NSNumber) {
+
+ let deviceInfo = BleDeviceInfo(
+ identifier: peripheral.identifier,
+ name: peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String,
+ rssi: RSSI,
+ advertisementData: advertisementData,
+ discoveryMethod: .traditional,
+ lastSeen: Date()
+ )
+
+ addOrUpdateDevice(deviceInfo)
+ }
+
+ public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
+ os_log("🔄 恢复 CBCentralManager 状态", log: logger, type: .info)
+
+ // 恢复扫描状态
+ if dict[CBCentralManagerRestoredStateScanServicesKey] != nil {
+ isScanning = true
+ }
+ }
+
+ private func getBluetoothStateString(_ state: CBManagerState) -> String {
+ switch state {
+ case .unknown: return "未知状态"
+ case .resetting: return "重置中"
+ case .unsupported: return "设备不支持蓝牙"
+ case .unauthorized: return "应用未授权使用蓝牙"
+ case .poweredOff: return "蓝牙已关闭"
+ case .poweredOn: return "蓝牙已开启"
+ @unknown default: return "未知状态"
+ }
+ }
+}
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 99e8340cc..fb7f92d49 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
@@ -230,7 +230,31 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
os_log("当前环境不支持AccessorySetupKit", log: logger, type: .info)
result(false)
#endif
+ case "startScan":
+ let arguments = call.arguments as? [String: Any]
+ let devicename = (arguments?["devicename"] as? String) ?? ""
+ // 修复:支持所有iOS版本
+ if #available(iOS 18.0, *) {
+ // iOS 18+ 使用新的BleScanManager
+ os_log("iOS 18+ 使用 BleScanManager 进行扫描", log: logger, type: .info)
+ let succ = BleScanManager.shared.startScan(timeout: 15.0)
+ result(succ)
+ } else {
+ // iOS 17及以下使用传统BleService
+ os_log("iOS 17及以下使用传统 BleService 进行扫描", log: logger, type: .info)
+ BleService.shared.startScan()
+ result(true)
+ }
+
+ case "getDeviceInfo":
+ let arguments = call.arguments as? [String: Any]
+ let address = (arguments?["address"] as? String) ?? ""
+ let info = BleScanManager.shared.getDiscoveredDevices()
+ result(info)
+ case "requestBluetoothPermission":
+ BleScanManager.shared.requestBluetoothPermission();
+ result(true)
default:
result(FlutterMethodNotImplemented)
}
@@ -394,4 +418,4 @@ class BleStatusStreamHandler: NSObject, FlutterStreamHandler {
plugin?.statusEventSink = nil
return nil
}
-}
+}
diff --git a/local_plugins/ble_service/lib/ble_service.dart b/local_plugins/ble_service/lib/ble_service.dart
index a33ea1e94..844c9c594 100644
--- a/local_plugins/ble_service/lib/ble_service.dart
+++ b/local_plugins/ble_service/lib/ble_service.dart
@@ -435,4 +435,15 @@ class BleService {
return false;
}
}
+
+ /// 请求蓝牙权限(仅iOS)
+ Future requestBluetoothPermission() async {
+ try {
+ final result = await _methodChannel.invokeMethod('requestBluetoothPermission');
+ return result ?? false;
+ } catch (e) {
+ print('请求蓝牙权限失败: $e');
+ return false;
+ }
+ }
}
diff --git a/local_plugins/ota/ios/Classes/BleManager/JLBleManager.m b/local_plugins/ota/ios/Classes/BleManager/JLBleManager.m
index 1147c552d..cb22ecfa8 100644
--- a/local_plugins/ota/ios/Classes/BleManager/JLBleManager.m
+++ b/local_plugins/ota/ios/Classes/BleManager/JLBleManager.m
@@ -218,6 +218,7 @@ NSString *FLT_BLE_RCSP_R = @"AE02"; //命令“读”通道
}
- (void)addPeripheral:(CBPeripheral*)peripheral Info:(NSString*)macStr {
+ kJLLog(JLLOG_DEBUG, @"BLE addPeripheral ---> Device %@", peripheral.name);
if (self.lastAddress && [macStr isEqualToString:self.lastAddress]) {
[self stopScanBLE];
[self connectBLE:peripheral];
From 74df1fdd811ead1e271673c0c0b1a5a1c091642a Mon Sep 17 00:00:00 2001
From: liwei1dao
Date: Tue, 15 Jul 2025 19:01:28 +0800
Subject: [PATCH 07/20] =?UTF-8?q?=E4=B8=8A=E4=BC=A0iOS=20=E4=BB=A3?=
=?UTF-8?q?=E7=A0=81=E4=BF=AE=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ios/Podfile.lock | 8 +-
ios/Runner.xcodeproj/project.pbxproj | 1 +
ios/Runner/Info.plist | 64 +-
lib/modules/ble/bindings/ble_binding.dart | 9 +
.../ble/controllers/ble_controller.dart | 281 ++++
lib/modules/ble/views/ble_view.dart | 355 +++++
lib/modules/login/views/login_view.dart | 14 +-
.../controllers/pairing_controller.dart | 54 +-
lib/routes/app_pages.dart | 8 +
lib/routes/app_routes.dart | 3 +
.../ble_service/BleAccessorySetup.swift | 506 ++++----
.../Sources/ble_service/BleScanManager.swift | 1144 ++++++++---------
.../Sources/ble_service/BleService.swift | 191 ++-
.../ble_service/SwiftBleServicePlugin.swift | 216 +---
.../user_portrait/ios/user_portrait.podspec | 29 -
.../ios/user_portrait/Package.swift | 24 +-
.../user_portrait/PrivacyInfo.xcprivacy | 14 -
.../user_portrait/UserPortraitPlugin.swift | 4 +-
local_plugins/user_portrait/pubspec.yaml | 46 +-
.../user_portrait_method_channel_test.dart | 27 -
.../test/user_portrait_test.dart | 29 -
pubspec.yaml | 1 -
22 files changed, 1683 insertions(+), 1345 deletions(-)
create mode 100644 lib/modules/ble/bindings/ble_binding.dart
create mode 100644 lib/modules/ble/controllers/ble_controller.dart
create mode 100644 lib/modules/ble/views/ble_view.dart
delete mode 100644 local_plugins/user_portrait/ios/user_portrait.podspec
delete mode 100644 local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy
delete mode 100644 local_plugins/user_portrait/test/user_portrait_method_channel_test.dart
delete mode 100644 local_plugins/user_portrait/test/user_portrait_test.dart
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index c9050a312..77f53258d 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -77,9 +77,9 @@ PODS:
- QCloudTrack/Beacon (6.4.7)
- record_darwin (1.0.0):
- Flutter
- - SDWebImage (5.21.0):
- - SDWebImage/Core (= 5.21.0)
- - SDWebImage/Core (5.21.0)
+ - SDWebImage (5.21.1):
+ - SDWebImage/Core (= 5.21.1)
+ - SDWebImage/Core (5.21.1)
- SDWebImageWebPCoder (0.14.6):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
@@ -200,7 +200,7 @@ SPEC CHECKSUMS:
QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298
QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
- SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
+ SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca
tencentcloud_cos_sdk_plugin: 7bed564dbe72df23e7b5cacb1e51b1a20cb1639c
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 02daa62e1..34f3271ab 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -549,6 +549,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 537SFY28XS;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index b2cdd80bb..6dd9a6874 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -72,66 +72,10 @@
LSRequiresIPhoneOS
- NSAccessorySetupBluetoothCompanyIdentifiers
-
- EFAB
-
- NSAccessorySetupDescriptions
-
-
- AccessoryMediaType
- Headphones
- LocalizedAccessoryName
- 语音助理耳机
-
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
- NSAllowsArbitraryLoadsInWebContent
-
- NSExceptionDomains
-
- mcp.amap.com
-
- NSExceptionAllowsInsecureHTTPLoads
-
- NSExceptionMinimumTLSVersion
- TLSv1.0
- NSIncludesSubdomains
-
-
- mcp.ideapsound.com
-
- NSExceptionAllowsInsecureHTTPLoads
-
- NSExceptionMinimumTLSVersion
- TLSv1.0
- NSIncludesSubdomains
-
-
-
-
-
NSBluetoothAlwaysUsageDescription
需要蓝牙权限用于连接和管理音频设备
NSBluetoothPeripheralUsageDescription
需要蓝牙权限用于连接和管理音频设备
-
-
- UIBackgroundModes
-
- bluetooth-central
- audio
- location
-
-
-
- NSAccessorySetupKitSupports
-
- Bluetooth
-
NSCalendarsUsageDescription
需要日历权限用于安排和管理您的音频会议和录音任务
NSCameraUsageDescription
@@ -148,6 +92,12 @@
需要相册权限用于保存录音文件和导入音频文件
UIApplicationSupportsIndirectInputEvents
+ UIBackgroundModes
+
+ bluetooth-central
+ audio
+ location
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
@@ -165,5 +115,7 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ UIViewControllerBasedStatusBarAppearance
+
diff --git a/lib/modules/ble/bindings/ble_binding.dart b/lib/modules/ble/bindings/ble_binding.dart
new file mode 100644
index 000000000..4fc2cc5c7
--- /dev/null
+++ b/lib/modules/ble/bindings/ble_binding.dart
@@ -0,0 +1,9 @@
+import 'package:get/get.dart';
+import '../controllers/ble_controller.dart';
+
+class BleBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.put(BleController());
+ }
+}
\ No newline at end of file
diff --git a/lib/modules/ble/controllers/ble_controller.dart b/lib/modules/ble/controllers/ble_controller.dart
new file mode 100644
index 000000000..91ce3f8dd
--- /dev/null
+++ b/lib/modules/ble/controllers/ble_controller.dart
@@ -0,0 +1,281 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp;
+import 'package:permission_handler/permission_handler.dart';
+import 'package:app_settings/app_settings.dart';
+
+class BleController extends GetxController {
+ // 扫描状态
+ final RxBool isScanning = false.obs;
+ final RxBool isBluetoothOn = false.obs;
+ final RxBool hasPermissions = false.obs;
+
+ // 发现的设备列表
+ final RxList scanResults = [].obs;
+
+ // 连接的设备
+ final Rx connectedDevice =
+ Rx(null);
+
+ // 错误信息
+ final RxString errorMessage = ''.obs;
+
+ @override
+ void onInit() {
+ super.onInit();
+ _initBluetooth();
+ }
+
+ @override
+ void onClose() {
+ stopScan();
+ super.onClose();
+ }
+
+ // 初始化蓝牙
+ Future _initBluetooth() async {
+ try {
+ // 先检查权限
+ await _checkPermissions();
+
+ // 检查蓝牙适配器状态
+ fbp.FlutterBluePlus.adapterState.listen((state) {
+ isBluetoothOn.value = state == fbp.BluetoothAdapterState.on;
+ if (state == fbp.BluetoothAdapterState.on) {
+ errorMessage.value = '';
+ } else {
+ errorMessage.value = '请开启蓝牙';
+ stopScan();
+ }
+ });
+
+ // 监听扫描结果
+ fbp.FlutterBluePlus.scanResults.listen((results) {
+ scanResults.value = results;
+ });
+
+ // 监听扫描状态
+ fbp.FlutterBluePlus.isScanning.listen((scanning) {
+ isScanning.value = scanning;
+ });
+ } catch (e) {
+ errorMessage.value = '蓝牙初始化失败: $e';
+ }
+ }
+
+ // 检查权限(不请求)
+ Future _checkPermissions() async {
+ try {
+ Map statuses = await [
+ Permission.bluetoothScan,
+ Permission.bluetoothConnect,
+ Permission.location,
+ ].request();
+
+ hasPermissions.value =
+ statuses.values.every((status) => status == PermissionStatus.granted);
+
+ if (!hasPermissions.value) {
+ errorMessage.value = '需要蓝牙和位置权限才能扫描设备';
+ }
+ } catch (e) {
+ print('检查权限失败: $e');
+ hasPermissions.value = false;
+ }
+ }
+
+ // 请求权限
+ Future _requestPermissions() async {
+ try {
+ Map statuses = await [
+ Permission.bluetoothScan,
+ Permission.bluetoothConnect,
+ Permission.location,
+ ].request();
+
+ bool allGranted =
+ statuses.values.every((status) => status == PermissionStatus.granted);
+ hasPermissions.value = allGranted;
+
+ if (!allGranted) {
+ // 检查是否有权限被永久拒绝
+ bool hasPermanentlyDenied = statuses.values
+ .any((status) => status == PermissionStatus.permanentlyDenied);
+
+ if (hasPermanentlyDenied) {
+ _showPermissionDialog();
+ } else {
+ errorMessage.value = '需要蓝牙和位置权限才能扫描设备';
+ }
+ }
+
+ return allGranted;
+ } catch (e) {
+ print('请求权限失败: $e');
+ errorMessage.value = '权限请求失败: $e';
+ return false;
+ }
+ }
+
+ // 显示权限对话框
+ void _showPermissionDialog() {
+ Get.dialog(
+ AlertDialog(
+ title: Text('需要权限'),
+ content: Text('应用需要蓝牙和位置权限才能扫描设备。请在设置中手动开启权限。'),
+ actions: [
+ TextButton(
+ onPressed: () => Get.back(),
+ child: Text('取消'),
+ ),
+ TextButton(
+ onPressed: () {
+ Get.back();
+ AppSettings.openAppSettings();
+ },
+ child: Text('去设置'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // 显示蓝牙开启对话框
+ void _showBluetoothDialog() {
+ Get.dialog(
+ AlertDialog(
+ title: Text('需要开启蓝牙'),
+ content: Text('请开启蓝牙以扫描和连接设备。'),
+ actions: [
+ TextButton(
+ onPressed: () => Get.back(),
+ child: Text('取消'),
+ ),
+ TextButton(
+ onPressed: () {
+ Get.back();
+ // 尝试开启蓝牙
+ fbp.FlutterBluePlus.turnOn();
+ },
+ child: Text('开启蓝牙'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // 开始扫描
+ Future startScan() async {
+ try {
+ errorMessage.value = '';
+
+ // 检查权限
+ if (!hasPermissions.value) {
+ if (!await _requestPermissions()) {
+ return;
+ }
+ }
+
+ // 检查蓝牙状态
+ if (!isBluetoothOn.value) {
+ _showBluetoothDialog();
+ return;
+ }
+
+ // 清空之前的结果
+ scanResults.clear();
+
+ // 开始扫描
+ await fbp.FlutterBluePlus.startScan(
+ timeout: const Duration(seconds: 30),
+ androidUsesFineLocation: true,
+ );
+ } catch (e) {
+ errorMessage.value = '扫描失败: $e';
+ }
+ }
+
+ // 停止扫描
+ Future stopScan() async {
+ try {
+ await fbp.FlutterBluePlus.stopScan();
+ } catch (e) {
+ print('停止扫描失败: $e');
+ }
+ }
+
+ // 连接设备
+ Future connectToDevice(fbp.BluetoothDevice device) async {
+ try {
+ errorMessage.value = '';
+
+ // 停止扫描
+ await stopScan();
+
+ // 连接设备
+ await device.connect(timeout: const Duration(seconds: 15));
+ connectedDevice.value = device;
+
+ Get.snackbar(
+ '连接成功',
+ '已连接到 ${device.platformName.isNotEmpty ? device.platformName : '未知设备'}',
+ backgroundColor: Colors.green,
+ colorText: Colors.white,
+ );
+ } catch (e) {
+ errorMessage.value = '连接失败: $e';
+ Get.snackbar(
+ '连接失败',
+ e.toString(),
+ backgroundColor: Colors.red,
+ colorText: Colors.white,
+ );
+ }
+ }
+
+ // 断开连接
+ Future disconnectDevice() async {
+ try {
+ if (connectedDevice.value != null) {
+ await connectedDevice.value!.disconnect();
+ connectedDevice.value = null;
+
+ Get.snackbar(
+ '已断开连接',
+ '设备已断开连接',
+ backgroundColor: Colors.orange,
+ colorText: Colors.white,
+ );
+ }
+ } catch (e) {
+ errorMessage.value = '断开连接失败: $e';
+ }
+ }
+
+ // 获取设备名称
+ String getDeviceName(fbp.ScanResult result) {
+ if (result.device.platformName.isNotEmpty) {
+ return result.device.platformName;
+ }
+ if (result.advertisementData.localName.isNotEmpty) {
+ return result.advertisementData.localName;
+ }
+ return '未知设备';
+ }
+
+ // 获取信号强度描述
+ String getSignalStrengthText(int rssi) {
+ if (rssi >= -50) return '信号强';
+ if (rssi >= -70) return '信号中等';
+ if (rssi >= -90) return '信号弱';
+ return '信号很弱';
+ }
+
+ // 获取信号强度图标
+ IconData getSignalStrengthIcon(int rssi) {
+ if (rssi >= -50) return Icons.signal_wifi_4_bar;
+ if (rssi >= -70) return Icons.signal_wifi_4_bar;
+ if (rssi >= -90) return Icons.signal_wifi_4_bar;
+ return Icons.signal_wifi_4_bar;
+ }
+}
diff --git a/lib/modules/ble/views/ble_view.dart b/lib/modules/ble/views/ble_view.dart
new file mode 100644
index 000000000..08398c95f
--- /dev/null
+++ b/lib/modules/ble/views/ble_view.dart
@@ -0,0 +1,355 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp;
+import '../controllers/ble_controller.dart';
+
+class BleView extends GetView {
+ const BleView({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
+
+ return Scaffold(
+ backgroundColor: isDarkMode ? Colors.black : Colors.white,
+ appBar: AppBar(
+ title: Text(
+ 'BLE设备扫描',
+ style: TextStyle(
+ color: isDarkMode ? Colors.white : Colors.black,
+ fontSize: 18.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
+ elevation: 0,
+ iconTheme: IconThemeData(
+ color: isDarkMode ? Colors.white : Colors.black,
+ ),
+ actions: [
+ Obx(() => IconButton(
+ onPressed: controller.isScanning.value
+ ? controller.stopScan
+ : controller.startScan,
+ icon: Icon(
+ controller.isScanning.value ? Icons.stop : Icons.refresh,
+ color: controller.isScanning.value
+ ? Colors.red
+ : (isDarkMode ? Colors.white : Colors.black),
+ ),
+ )),
+ ],
+ ),
+ body: SafeArea(
+ child: Column(
+ children: [
+ // 状态栏
+ _buildStatusBar(isDarkMode),
+
+ // 连接的设备信息
+ _buildConnectedDevice(isDarkMode),
+
+ // 设备列表
+ Expanded(
+ child: _buildDeviceList(isDarkMode),
+ ),
+ ],
+ ),
+ ),
+ floatingActionButton: Obx(() => FloatingActionButton(
+ onPressed: controller.isScanning.value
+ ? controller.stopScan
+ : controller.startScan,
+ backgroundColor:
+ controller.isScanning.value ? Colors.red : Colors.blue,
+ child: Icon(
+ controller.isScanning.value
+ ? Icons.stop
+ : Icons.bluetooth_searching,
+ color: Colors.white,
+ ),
+ )),
+ );
+ }
+
+ // 状态栏
+ Widget _buildStatusBar(bool isDarkMode) {
+ return Obx(() => Container(
+ width: double.infinity,
+ padding: EdgeInsets.all(16.w),
+ margin: EdgeInsets.all(16.w),
+ decoration: BoxDecoration(
+ color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
+ borderRadius: BorderRadius.circular(12.r),
+ ),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ Icon(
+ controller.isBluetoothOn.value
+ ? Icons.bluetooth
+ : Icons.bluetooth_disabled,
+ color: controller.isBluetoothOn.value
+ ? Colors.blue
+ : Colors.red,
+ size: 24.w,
+ ),
+ SizedBox(width: 8.w),
+ Text(
+ controller.isBluetoothOn.value ? '蓝牙已开启' : '蓝牙已关闭',
+ style: TextStyle(
+ color: isDarkMode ? Colors.white : Colors.black,
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ Spacer(),
+ if (controller.isScanning.value)
+ SizedBox(
+ width: 20.w,
+ height: 20.w,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(Colors.blue),
+ ),
+ ),
+ ],
+ ),
+ if (controller.errorMessage.value.isNotEmpty) ...[
+ SizedBox(height: 8.h),
+ Text(
+ controller.errorMessage.value,
+ style: TextStyle(
+ color: Colors.red,
+ fontSize: 14.sp,
+ ),
+ ),
+ ],
+ if (controller.isScanning.value) ...[
+ SizedBox(height: 8.h),
+ Text(
+ '正在扫描设备...',
+ style: TextStyle(
+ color: Colors.blue,
+ fontSize: 14.sp,
+ ),
+ ),
+ ],
+ ],
+ ),
+ ));
+ }
+
+ // 已连接设备信息
+ Widget _buildConnectedDevice(bool isDarkMode) {
+ return Obx(() {
+ if (controller.connectedDevice.value == null) {
+ return SizedBox.shrink();
+ }
+
+ final device = controller.connectedDevice.value!;
+ return Container(
+ width: double.infinity,
+ padding: EdgeInsets.all(16.w),
+ margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
+ decoration: BoxDecoration(
+ color: Colors.green.withOpacity(0.1),
+ border: Border.all(color: Colors.green, width: 1),
+ borderRadius: BorderRadius.circular(12.r),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.bluetooth_connected,
+ color: Colors.green,
+ size: 24.w,
+ ),
+ SizedBox(width: 12.w),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '已连接设备',
+ style: TextStyle(
+ color: Colors.green,
+ fontSize: 12.sp,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ Text(
+ device.platformName.isNotEmpty
+ ? device.platformName
+ : '未知设备',
+ style: TextStyle(
+ color: isDarkMode ? Colors.white : Colors.black,
+ fontSize: 16.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ Text(
+ device.remoteId.toString(),
+ style: TextStyle(
+ color: isDarkMode ? Colors.white70 : Colors.black54,
+ fontSize: 12.sp,
+ ),
+ ),
+ ],
+ ),
+ ),
+ IconButton(
+ onPressed: controller.disconnectDevice,
+ icon: Icon(
+ Icons.close,
+ color: Colors.red,
+ size: 20.w,
+ ),
+ ),
+ ],
+ ),
+ );
+ });
+ }
+
+ // 设备列表
+ Widget _buildDeviceList(bool isDarkMode) {
+ return Obx(() {
+ if (controller.scanResults.isEmpty) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.bluetooth_searching,
+ size: 64.w,
+ color: isDarkMode ? Colors.white54 : Colors.black54,
+ ),
+ SizedBox(height: 16.h),
+ Text(
+ controller.isScanning.value ? '正在搜索设备...' : '点击扫描按钮开始搜索BLE设备',
+ style: TextStyle(
+ color: isDarkMode ? Colors.white54 : Colors.black54,
+ fontSize: 16.sp,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ );
+ }
+
+ return ListView.builder(
+ padding: EdgeInsets.symmetric(horizontal: 16.w),
+ itemCount: controller.scanResults.length,
+ itemBuilder: (context, index) {
+ final result = controller.scanResults[index];
+ return _buildDeviceItem(result, isDarkMode);
+ },
+ );
+ });
+ }
+
+ // 设备项
+ Widget _buildDeviceItem(fbp.ScanResult result, bool isDarkMode) {
+ final deviceName = controller.getDeviceName(result);
+ final isConnected =
+ controller.connectedDevice.value?.remoteId == result.device.remoteId;
+
+ return Container(
+ margin: EdgeInsets.only(bottom: 12.h),
+ decoration: BoxDecoration(
+ color: isDarkMode ? Colors.grey[800] : Colors.white,
+ borderRadius: BorderRadius.circular(12.r),
+ border: Border.all(
+ color: isConnected
+ ? Colors.green
+ : (isDarkMode ? Colors.grey[700]! : Colors.grey[300]!),
+ width: isConnected ? 2 : 1,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.05),
+ blurRadius: 8,
+ offset: Offset(0, 2),
+ ),
+ ],
+ ),
+ child: ListTile(
+ contentPadding: EdgeInsets.all(16.w),
+ leading: Container(
+ width: 48.w,
+ height: 48.w,
+ decoration: BoxDecoration(
+ color: isConnected
+ ? Colors.green.withOpacity(0.1)
+ : Colors.blue.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(24.r),
+ ),
+ child: Icon(
+ isConnected ? Icons.bluetooth_connected : Icons.bluetooth,
+ color: isConnected ? Colors.green : Colors.blue,
+ size: 24.w,
+ ),
+ ),
+ title: Text(
+ deviceName,
+ style: TextStyle(
+ color: isDarkMode ? Colors.white : Colors.black,
+ fontSize: 16.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(height: 4.h),
+ Text(
+ result.device.remoteId.toString(),
+ style: TextStyle(
+ color: isDarkMode ? Colors.white70 : Colors.black54,
+ fontSize: 12.sp,
+ ),
+ ),
+ SizedBox(height: 4.h),
+ Row(
+ children: [
+ Icon(
+ controller.getSignalStrengthIcon(result.rssi),
+ size: 16.w,
+ color: isDarkMode ? Colors.white70 : Colors.black54,
+ ),
+ SizedBox(width: 4.w),
+ Text(
+ '${result.rssi} dBm (${controller.getSignalStrengthText(result.rssi)})',
+ style: TextStyle(
+ color: isDarkMode ? Colors.white70 : Colors.black54,
+ fontSize: 12.sp,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ trailing: isConnected
+ ? Icon(
+ Icons.check_circle,
+ color: Colors.green,
+ size: 24.w,
+ )
+ : IconButton(
+ onPressed: () => controller.connectToDevice(result.device),
+ icon: Icon(
+ Icons.connect_without_contact,
+ color: Colors.blue,
+ size: 24.w,
+ ),
+ ),
+ onTap: isConnected
+ ? null
+ : () => controller.connectToDevice(result.device),
+ ),
+ );
+ }
+}
diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart
index 543ba1827..9df8ffb0f 100644
--- a/lib/modules/login/views/login_view.dart
+++ b/lib/modules/login/views/login_view.dart
@@ -39,13 +39,13 @@ class LoginView extends GetView {
SizedBox(height: 32.h),
_buildWelcomeText(isDarkMode),
- // 输入框和继续按钮
- SizedBox(height: 24.h),
- _buildInputField(isDarkMode),
- SizedBox(height: 16.h),
- _buildVerificationInputField(isDarkMode),
- SizedBox(height: 16.h),
- _buildContinueButton(isDarkMode),
+ // // 输入框和继续按钮
+ // SizedBox(height: 24.h),
+ // _buildInputField(isDarkMode),
+ // SizedBox(height: 16.h),
+ // _buildVerificationInputField(isDarkMode),
+ // SizedBox(height: 16.h),
+ // _buildContinueButton(isDarkMode),
// 社交媒体登录
SizedBox(height: 32.h),
diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart
index ee27f518a..2c31301aa 100644
--- a/lib/modules/pairing/controllers/pairing_controller.dart
+++ b/lib/modules/pairing/controllers/pairing_controller.dart
@@ -1,6 +1,3 @@
-import 'dart:convert';
-import 'dart:typed_data';
-
import 'package:app_settings/app_settings.dart';
import '/data/models/appconfig.dart';
import '/data/models/user_Info.dart';
@@ -14,7 +11,6 @@ import '../../../core/utils/logger.dart';
import 'dart:io';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
-import '../../../core/utils/permission_util.dart';
/// 配对状态枚举 - 简化版本
enum PairingState {
@@ -418,46 +414,16 @@ class PairingController extends GetxController {
/// 检查权限
Future _checkPermissions() async {
- if (Platform.isAndroid) {
- // Android 平台权限检查
- Map statuses = await [
- Permission.bluetoothScan,
- Permission.bluetoothConnect,
- Permission.locationWhenInUse,
- ].request();
-
- return statuses[Permission.bluetoothScan]!.isGranted &&
- statuses[Permission.bluetoothConnect]!.isGranted &&
- statuses[Permission.locationWhenInUse]!.isGranted;
- } else if (Platform.isIOS) {
- // iOS 平台权限检查
- Map statuses = await [
- Permission.locationWhenInUse,
- Permission.bluetooth, // 添加蓝牙权限检查
- ].request();
-
- // 检查位置权限
- bool locationGranted = statuses[Permission.locationWhenInUse]!.isGranted;
-
- // 检查蓝牙权限
- bool bluetoothGranted = statuses[Permission.bluetooth]!.isGranted;
-
- // 如果蓝牙权限未确定,强制触发权限申请
- if (!bluetoothGranted) {
- try {
- await _bleManager.requestBluetoothPermission();
- // 重新检查蓝牙权限状态
- final newBluetoothStatus = await Permission.bluetooth.status;
- bluetoothGranted = newBluetoothStatus.isGranted;
- } catch (e) {
- Logger.e(_tag, '蓝牙权限申请失败: $e');
- }
- }
-
- return locationGranted && bluetoothGranted;
- }
-
- return true;
+ // Android 平台权限检查
+ Map statuses = await [
+ Permission.bluetoothScan,
+ Permission.bluetoothConnect,
+ Permission.locationWhenInUse,
+ ].request();
+
+ return statuses[Permission.bluetoothScan]!.isGranted &&
+ statuses[Permission.bluetoothConnect]!.isGranted &&
+ statuses[Permission.locationWhenInUse]!.isGranted;
}
/// 验证设备
diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart
index 88f7f07c6..48efd4fe9 100644
--- a/lib/routes/app_pages.dart
+++ b/lib/routes/app_pages.dart
@@ -1,3 +1,6 @@
+import 'package:deep_voice/modules/ble/bindings/ble_binding.dart';
+import 'package:deep_voice/modules/ble/views/ble_view.dart';
+
import '../../modules/devices/bindings/devices_binding.dart';
import '../../modules/devices/views/devices_view.dart';
import 'package:get/get.dart';
@@ -249,5 +252,10 @@ abstract class AppPages {
page: () => const DevicesView(),
binding: DevicesBinding(),
),
+ GetPage(
+ name: Routes.ble,
+ page: () => const BleView(),
+ binding: BleBinding(),
+ ),
];
}
diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart
index 7942bfd73..e955f025e 100644
--- a/lib/routes/app_routes.dart
+++ b/lib/routes/app_routes.dart
@@ -56,4 +56,7 @@ abstract class Routes {
// 设备管理
static const devices = '/devices';
+
+ // ble设备管理
+ static const ble = '/ble';
}
diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift
index 3cf85910e..07f6803d0 100644
--- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift
+++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift
@@ -1,306 +1,306 @@
-import Foundation
-import CoreBluetooth
-import os.log
-import AVFoundation
-import AccessorySetupKit
-import UIKit
+// import Foundation
+// import CoreBluetooth
+// import os.log
+// import AVFoundation
+// import AccessorySetupKit
+// import UIKit
-@available(iOS 18.0, *)
-class BleAccessorySetup: NSObject {
- // MARK: - 属性
- static let shared = BleAccessorySetup()
+// @available(iOS 18.0, *)
+// class BleAccessorySetup: NSObject {
+// // MARK: - 属性
+// static let shared = BleAccessorySetup()
- private var bleService: BleService?
- private var session: ASAccessorySession?
- private var isSessionActivated = false
- private var isInitializing = false
- private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup")
+// private var bleService: BleService?
+// private var session: ASAccessorySession?
+// private var isSessionActivated = false
+// private var isInitializing = false
+// private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup")
- // 杰理蓝牙设备的公司标识符
- private let deviceCompanyIdentifier: UInt16 = 0xEFAB
- private let deviceDisplayName = "语音助理耳机"
+// // 杰理蓝牙设备的公司标识符
+// private let deviceCompanyIdentifier: UInt16 = 0xEFAB
+// private let deviceDisplayName = "语音助理耳机"
- // 用于存储配对设备UUID的UserDefaults键
- private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid"
+// // 用于存储配对设备UUID的UserDefaults键
+// private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid"
- typealias DeviceDiscoveredCallback = (UUID) -> Void
- private var onDeviceDiscovered: DeviceDiscoveredCallback?
+// typealias DeviceDiscoveredCallback = (UUID) -> Void
+// private var onDeviceDiscovered: DeviceDiscoveredCallback?
- // MARK: - 初始化
- private override init() { super.init() }
+// // MARK: - 初始化
+// private override init() { super.init() }
- // MARK: - 公共方法
- func setBleService(_ service: Any) {
- self.bleService = service as? BleService
- }
+// // MARK: - 公共方法
+// func setBleService(_ service: Any) {
+// self.bleService = service as? BleService
+// }
- func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
- self.onDeviceDiscovered = callback
- }
+// func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
+// self.onDeviceDiscovered = callback
+// }
- // 处理发现的设备并连接
- func connectToAccessory(uuid: UUID) {
- guard let service = bleService else {
- return
- }
+// // 处理发现的设备并连接
+// func connectToAccessory(uuid: UUID) {
+// guard let service = bleService else {
+// return
+// }
- service.connectToAccessory(uuid: uuid)
- }
+// service.connectToAccessory(uuid: uuid)
+// }
- func addToReconnectQueue(uuid: UUID) {
- guard let service = bleService else {
- return
- }
+// func addToReconnectQueue(uuid: UUID) {
+// guard let service = bleService else {
+// return
+// }
- service.addToReconnectQueue(uuid: uuid)
- }
+// service.addToReconnectQueue(uuid: uuid)
+// }
- // 从视图控制器显示设备选择器 - 主要入口点
- func showDevicePickerFromViewController(_ viewController: UIViewController) {
- initializeSession()
- }
+// // 从视图控制器显示设备选择器 - 主要入口点
+// func showDevicePickerFromViewController(_ viewController: UIViewController) {
+// initializeSession()
+// }
- // 清除蓝牙状态(仅用于测试或重置,不会清除系统授权)
- func resetBluetoothState() {
- if let service = bleService {
- service.disconnect()
- }
- }
+// // 清除蓝牙状态(仅用于测试或重置,不会清除系统授权)
+// func resetBluetoothState() {
+// if let service = bleService {
+// service.disconnect()
+// }
+// }
- // MARK: - 会话管理
- func initializeSession() {
- if isInitializing {
- os_log("会话正在初始化中,跳过", log: logger, type: .info)
- return
- }
+// // MARK: - 会话管理
+// func initializeSession() {
+// if isInitializing {
+// os_log("会话正在初始化中,跳过", log: logger, type: .info)
+// return
+// }
- // 如果会话已激活,检查是否有配对设备
- if session != nil && isSessionActivated {
- os_log("会话已激活,检查配对设备", log: logger, type: .info)
- // 会话已激活,检查是否有配对设备
- if let accessories = session?.accessories, !accessories.isEmpty {
- os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count)
- // 有配对设备,尝试连接
- if let service = bleService {
- let _ = service.getCentralManager()
+// // 如果会话已激活,检查是否有配对设备
+// if session != nil && isSessionActivated {
+// os_log("会话已激活,检查配对设备", log: logger, type: .info)
+// // 会话已激活,检查是否有配对设备
+// if let accessories = session?.accessories, !accessories.isEmpty {
+// os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count)
+// // 有配对设备,尝试连接
+// if let service = bleService {
+// let _ = service.getCentralManager()
- for accessory in accessories {
- if let uuid = accessory.bluetoothIdentifier {
- onDeviceDiscovered?(uuid)
- connectToAccessory(uuid: uuid)
- }
- }
- }
- } else {
- os_log("没有已配对设备,显示配对框", log: logger, type: .info)
- // 没有配对设备,直接显示配对框
- showDevicePicker()
- }
- return
- }
+// for accessory in accessories {
+// if let uuid = accessory.bluetoothIdentifier {
+// onDeviceDiscovered?(uuid)
+// connectToAccessory(uuid: uuid)
+// }
+// }
+// }
+// } else {
+// os_log("没有已配对设备,显示配对框", log: logger, type: .info)
+// // 没有配对设备,直接显示配对框
+// showDevicePicker()
+// }
+// return
+// }
- os_log("开始初始化新会话", log: logger, type: .info)
- isInitializing = true
+// os_log("开始初始化新会话", log: logger, type: .info)
+// isInitializing = true
- do {
- session = try ASAccessorySession()
- session?.activate(on: .main) { [weak self] event in
- guard let self = self else { return }
- self.handleSessionEvent(event)
- }
- } catch {
- os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription)
- handleSessionInitError(error)
- }
- }
+// do {
+// session = try ASAccessorySession()
+// session?.activate(on: .main) { [weak self] event in
+// guard let self = self else { return }
+// self.handleSessionEvent(event)
+// }
+// } catch {
+// os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription)
+// handleSessionInitError(error)
+// }
+// }
- func resetSession() {
- session?.invalidate()
- session = nil
- isSessionActivated = false
- isInitializing = false
- }
+// func resetSession() {
+// session?.invalidate()
+// session = nil
+// isSessionActivated = false
+// isInitializing = false
+// }
- // MARK: - 设备选择器显示
- private func showDevicePicker() {
- guard let session = session, isSessionActivated else {
- os_log("无法显示配对框:会话未激活", log: logger, type: .error)
- return
- }
+// // MARK: - 设备选择器显示
+// private func showDevicePicker() {
+// guard let session = session, isSessionActivated else {
+// os_log("无法显示配对框:会话未激活", log: logger, type: .error)
+// return
+// }
- os_log("准备显示设备配对框", log: logger, type: .info)
+// os_log("准备显示设备配对框", log: logger, type: .info)
- let descriptor = ASDiscoveryDescriptor()
- descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier)
+// let descriptor = ASDiscoveryDescriptor()
+// descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier)
- let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
- let displayItem = ASPickerDisplayItem(
- name: deviceDisplayName,
- productImage: deviceImage,
- descriptor: descriptor
- )
+// let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
+// let displayItem = ASPickerDisplayItem(
+// name: deviceDisplayName,
+// productImage: deviceImage,
+// descriptor: descriptor
+// )
- DispatchQueue.main.async {
- os_log("正在显示设备配对框", log: self.logger, type: .info)
- session.showPicker(for: [displayItem]) { [weak self] error in
- guard let self = self else { return }
+// DispatchQueue.main.async {
+// os_log("正在显示设备配对框", log: self.logger, type: .info)
+// session.showPicker(for: [displayItem]) { [weak self] error in
+// guard let self = self else { return }
- if let error = error {
- let nsError = error as NSError
- os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
- } else {
- os_log("设备选择器完成", log: self.logger, type: .info)
- }
- }
- }
- }
+// if let error = error {
+// let nsError = error as NSError
+// os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
+// } else {
+// os_log("设备选择器完成", log: self.logger, type: .info)
+// }
+// }
+// }
+// }
- // MARK: - 私有辅助方法
- private func handleSessionEvent(_ event: ASAccessoryEvent) {
- switch event.eventType {
- case .activated:
- os_log("会话已激活", log: logger, type: .info)
- handleSessionActivated()
+// // MARK: - 私有辅助方法
+// private func handleSessionEvent(_ event: ASAccessoryEvent) {
+// switch event.eventType {
+// case .activated:
+// os_log("会话已激活", log: logger, type: .info)
+// handleSessionActivated()
- case .accessoryAdded:
- os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
- handleAccessoryAdded(event.accessory)
+// case .accessoryAdded:
+// os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
+// handleAccessoryAdded(event.accessory)
- case .accessoryRemoved:
- os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
- handleAccessoryRemoved(event.accessory)
+// case .accessoryRemoved:
+// os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
+// handleAccessoryRemoved(event.accessory)
- case .accessoryChanged:
- os_log("设备状态已改变", log: logger, type: .info)
- break
+// case .accessoryChanged:
+// os_log("设备状态已改变", log: logger, type: .info)
+// break
- case .pickerDidPresent:
- os_log("配对框已显示", log: logger, type: .info)
- break
+// case .pickerDidPresent:
+// os_log("配对框已显示", log: logger, type: .info)
+// break
- case .pickerDidDismiss:
- os_log("配对框已关闭", log: logger, type: .info)
- break
+// case .pickerDidDismiss:
+// os_log("配对框已关闭", log: logger, type: .info)
+// break
- case .pickerSetupFailed:
- os_log("选择器设置失败: %{public}@", log: logger, type: .error,
- event.error?.localizedDescription ?? "未知错误")
+// case .pickerSetupFailed:
+// os_log("选择器设置失败: %{public}@", log: logger, type: .error,
+// event.error?.localizedDescription ?? "未知错误")
- default:
- os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue)
- break
- }
- }
+// default:
+// os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue)
+// break
+// }
+// }
- private func handleSessionActivated() {
- isSessionActivated = true
- isInitializing = false
+// private func handleSessionActivated() {
+// isSessionActivated = true
+// isInitializing = false
- if let accessories = session?.accessories, !accessories.isEmpty {
- if let service = bleService {
- let _ = service.getCentralManager()
+// if let accessories = session?.accessories, !accessories.isEmpty {
+// if let service = bleService {
+// let _ = service.getCentralManager()
- for accessory in accessories {
- if let uuid = accessory.bluetoothIdentifier {
- onDeviceDiscovered?(uuid)
- connectToAccessory(uuid: uuid)
- }
- }
- }
- } else {
- showDevicePicker()
- }
- }
+// for accessory in accessories {
+// if let uuid = accessory.bluetoothIdentifier {
+// onDeviceDiscovered?(uuid)
+// connectToAccessory(uuid: uuid)
+// }
+// }
+// }
+// } else {
+// showDevicePicker()
+// }
+// }
- private func handleAccessoryAdded(_ accessory: ASAccessory?) {
- guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
- return
- }
+// private func handleAccessoryAdded(_ accessory: ASAccessory?) {
+// guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
+// return
+// }
- // 保存配对设备的UUID
- savePairedMacAddress(uuid)
+// // 保存配对设备的UUID
+// savePairedMacAddress(uuid)
- onDeviceDiscovered?(uuid)
- connectToAccessory(uuid: uuid)
- }
+// onDeviceDiscovered?(uuid)
+// connectToAccessory(uuid: uuid)
+// }
- private func handleAccessoryRemoved(_ accessory: ASAccessory?) {
- guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
- return
- }
+// private func handleAccessoryRemoved(_ accessory: ASAccessory?) {
+// guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
+// return
+// }
- if let service = bleService {
- service.disconnect()
- }
- }
+// if let service = bleService {
+// service.disconnect()
+// }
+// }
- private func handleSessionInitError(_ error: Error) {
- isInitializing = false
- session = nil
- }
+// private func handleSessionInitError(_ error: Error) {
+// isInitializing = false
+// session = nil
+// }
- // MARK: - 蓝牙状态检查
- func isBluetoothAvailable() -> Bool {
- do {
- let audioSession = AVAudioSession.sharedInstance()
- let outputs = audioSession.currentRoute.outputs
+// // MARK: - 蓝牙状态检查
+// func isBluetoothAvailable() -> Bool {
+// do {
+// let audioSession = AVAudioSession.sharedInstance()
+// let outputs = audioSession.currentRoute.outputs
- for output in outputs {
- if output.portType == .bluetoothA2DP ||
- output.portType == .bluetoothHFP ||
- output.portType == .bluetoothLE {
- return true
- }
- }
- } catch {
- os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription)
- }
+// for output in outputs {
+// if output.portType == .bluetoothA2DP ||
+// output.portType == .bluetoothHFP ||
+// output.portType == .bluetoothLE {
+// return true
+// }
+// }
+// } catch {
+// os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription)
+// }
- return true
- }
+// return true
+// }
- // MARK: - 清除配对关系
- func clearAssociations() {
- resetBluetoothState()
+// // MARK: - 清除配对关系
+// func clearAssociations() {
+// resetBluetoothState()
- // 清除保存的配对设备UUID
- clearPairedMacAddress()
+// // 清除保存的配对设备UUID
+// clearPairedMacAddress()
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- resetSession()
- initializeSession()
- }
- #endif
- }
+// #if canImport(AccessorySetupKit) && swift(>=5.9)
+// if #available(iOS 18.0, *) {
+// resetSession()
+// initializeSession()
+// }
+// #endif
+// }
- // MARK: - 配对设备UUID管理
+// // MARK: - 配对设备UUID管理
- /// 获取已配对设备的UUID字符串
- /// 返回存储的配对设备UUID,如果没有则返回nil
- func getPairedMacAddress() -> String? {
- let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey)
- if let uuidString = uuidString, !uuidString.isEmpty {
- os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
- return uuidString
- }
- os_log("未找到已配对设备UUID", log: logger, type: .info)
- return nil
- }
+// /// 获取已配对设备的UUID字符串
+// /// 返回存储的配对设备UUID,如果没有则返回nil
+// func getPairedMacAddress() -> String? {
+// let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey)
+// if let uuidString = uuidString, !uuidString.isEmpty {
+// os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
+// return uuidString
+// }
+// os_log("未找到已配对设备UUID", log: logger, type: .info)
+// return nil
+// }
- /// 保存配对设备的UUID
- /// - Parameter uuid: 要保存的设备UUID
- func savePairedMacAddress(_ uuid: UUID) {
- let uuidString = uuid.uuidString
- UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey)
- UserDefaults.standard.synchronize()
- os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
- }
+// /// 保存配对设备的UUID
+// /// - Parameter uuid: 要保存的设备UUID
+// func savePairedMacAddress(_ uuid: UUID) {
+// let uuidString = uuid.uuidString
+// UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey)
+// UserDefaults.standard.synchronize()
+// os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
+// }
- /// 清除已保存的配对设备UUID
- private func clearPairedMacAddress() {
- UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey)
- UserDefaults.standard.synchronize()
- os_log("已清除配对设备UUID", log: logger, type: .info)
- }
-}
\ No newline at end of file
+// /// 清除已保存的配对设备UUID
+// private func clearPairedMacAddress() {
+// UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey)
+// UserDefaults.standard.synchronize()
+// os_log("已清除配对设备UUID", log: logger, type: .info)
+// }
+// }
\ No newline at end of file
diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift
index 1be2b0c46..692984773 100644
--- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift
+++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift
@@ -1,586 +1,586 @@
-import Foundation
-import CoreBluetooth
-import UIKit // 添加UIKit导入
-import os.log
+// import Foundation
+// import CoreBluetooth
+// import UIKit // 添加UIKit导入
+// import os.log
-/// 简化的 BLE 扫描管理器
-/// 只提供扫描和获取设备列表功能
-@available(iOS 13.0, *)
-public class BleScanManager: NSObject {
-
- // MARK: - 单例
- public static let shared = BleScanManager()
-
- // MARK: - 数据模型
- public struct BleDeviceInfo {
- public let identifier: UUID
- public let name: String?
- public let rssi: NSNumber
- public let advertisementData: [String: Any]
- public let discoveryMethod: DiscoveryMethod
- public let lastSeen: Date
-
- public enum DiscoveryMethod {
- case traditional
- case accessorySetupKit
- }
- }
-
- // MARK: - 属性
- private var centralManager: CBCentralManager!
- private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleScanManager")
-
- // 扫描状态
- private var isScanning = false
- private var scanTimeout: Timer?
-
- // 设备列表
- private var discoveredDevices: [UUID: BleDeviceInfo] = [:]
-
-
- // MARK: - 初始化
- private override init() {
- super.init()
- setupCentralManager()
- }
-
- // 在setupCentralManager后添加权限检查
- private func setupCentralManager() {
- let options: [String: Any] = [
- CBCentralManagerOptionShowPowerAlertKey: true,
- CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager"
- ]
-
- centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
-
- // 添加权限检查和触发逻辑
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
- self?.checkAndTriggerPermission()
- }
- }
-
- private func checkAndTriggerPermission() {
- // 检查蓝牙权限状态,兼容不同iOS版本
- let authorization: CBManagerAuthorization
- if #available(iOS 13.1, *) {
- authorization = CBManager.authorization
- } else if #available(iOS 13.0, *) {
- // iOS 13.0 使用实例方法,需要确保centralManager已初始化
- authorization = centralManager?.authorization ?? .notDetermined
- } else {
- // iOS 13.0以下版本没有权限检查API,默认为未确定
- authorization = .notDetermined
- }
-
- os_log("📊 蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue)
- os_log("📊 CBCentralManager状态: %d", log: logger, type: .info, centralManager?.state.rawValue ?? -1)
-
- // 只在权限未确定且蓝牙状态为关闭时触发扫描
- if authorization == .notDetermined && centralManager?.state == .poweredOff {
- os_log("📊 主动触发一次扫描来申请权限", log: logger, type: .info)
- // 主动触发一次扫描来申请权限
- centralManager?.scanForPeripherals(withServices: nil, options: nil)
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
- self?.centralManager?.stopScan()
- }
- }
- }
-
-
-
- // MARK: - 公共接口
-
- /// 开始扫描 BLE 设备
- /// - Parameter timeout: 扫描超时时间,默认 10 秒
- /// - Returns: 是否成功开始扫描
- public func startScan(timeout: TimeInterval = 30.0) -> Bool {
- os_log("🔍 开始 BLE 扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout)
-
- guard !isScanning else {
- os_log("⚠️ 扫描已在进行中", log: logger, type: .info)
- return false
- }
-
- // 清空之前的设备列表
- discoveredDevices.removeAll()
-
- // iOS 18 兼容性检查
- if #available(iOS 18.0, *) {
- // iOS 18 使用新的权限检查方式
- return startScanForIOS18(timeout: timeout)
- } else {
- // iOS 17 及以下使用传统方式
- return startScanLegacy(timeout: timeout)
- }
- }
-
- @available(iOS 18.0, *)
- private func startScanForIOS18(timeout: TimeInterval) -> Bool {
- // 对于 iOS 18,直接检查 CBCentralManager 状态
- guard let centralManager = centralManager else {
- os_log("❌ CBCentralManager 未初始化", log: logger, type: .error)
- return false
- }
-
- let currentState = centralManager.state
- os_log("📊 iOS 18 蓝牙状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState))
-
- switch currentState {
- case .poweredOn:
- return performScanWithPermission(timeout: timeout)
- case .unauthorized:
- os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error)
- showPermissionAlert()
- return false
- case .unknown:
- os_log("⚠️ 蓝牙状态未知,等待状态更新", log: logger, type: .info)
- // 设置监听器等待状态更新
- waitForBluetoothState(timeout: timeout)
- return true
- default:
- os_log("❌ 蓝牙不可用: %{public}@", log: logger, type: .error, getBluetoothStateString(currentState))
- return false
- }
- }
-
- private func startScanLegacy(timeout: TimeInterval) -> Bool {
- // 检查并申请蓝牙权限
- let authorization: CBManagerAuthorization = CBManager.authorization
- os_log("📊 当前蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue)
-
- switch authorization {
- case .allowedAlways:
- return performScanWithPermission(timeout: timeout)
- case .denied, .restricted:
- os_log("❌ 蓝牙权限被拒绝或受限", log: logger, type: .error)
- showPermissionAlert()
- return false
- case .notDetermined:
- // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗
- if centralManager == nil {
- setupCentralManager()
- }
+// /// 简化的 BLE 扫描管理器
+// /// 只提供扫描和获取设备列表功能
+// @available(iOS 13.0, *)
+// public class BleScanManager: NSObject {
+
+// // MARK: - 单例
+// public static let shared = BleScanManager()
+
+// // MARK: - 数据模型
+// public struct BleDeviceInfo {
+// public let identifier: UUID
+// public let name: String?
+// public let rssi: NSNumber
+// public let advertisementData: [String: Any]
+// public let discoveryMethod: DiscoveryMethod
+// public let lastSeen: Date
+
+// public enum DiscoveryMethod {
+// case traditional
+// case accessorySetupKit
+// }
+// }
+
+// // MARK: - 属性
+// private var centralManager: CBCentralManager!
+// private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleScanManager")
+
+// // 扫描状态
+// private var isScanning = false
+// private var scanTimeout: Timer?
+
+// // 设备列表
+// private var discoveredDevices: [UUID: BleDeviceInfo] = [:]
+
+
+// // MARK: - 初始化
+// private override init() {
+// super.init()
+// setupCentralManager()
+// }
+
+// // 在setupCentralManager后添加权限检查
+// private func setupCentralManager() {
+// let options: [String: Any] = [
+// CBCentralManagerOptionShowPowerAlertKey: true,
+// CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager"
+// ]
+
+// centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
+
+// // 添加权限检查和触发逻辑
+// DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+// self?.checkAndTriggerPermission()
+// }
+// }
+
+// private func checkAndTriggerPermission() {
+// // 检查蓝牙权限状态,兼容不同iOS版本
+// let authorization: CBManagerAuthorization
+// if #available(iOS 13.1, *) {
+// authorization = CBManager.authorization
+// } else if #available(iOS 13.0, *) {
+// // iOS 13.0 使用实例方法,需要确保centralManager已初始化
+// authorization = centralManager?.authorization ?? .notDetermined
+// } else {
+// // iOS 13.0以下版本没有权限检查API,默认为未确定
+// authorization = .notDetermined
+// }
+
+// os_log("📊 蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue)
+// os_log("📊 CBCentralManager状态: %d", log: logger, type: .info, centralManager?.state.rawValue ?? -1)
+
+// // 只在权限未确定且蓝牙状态为关闭时触发扫描
+// if authorization == .notDetermined && centralManager?.state == .poweredOff {
+// os_log("📊 主动触发一次扫描来申请权限", log: logger, type: .info)
+// // 主动触发一次扫描来申请权限
+// centralManager?.scanForPeripherals(withServices: nil, options: nil)
+// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
+// self?.centralManager?.stopScan()
+// }
+// }
+// }
+
+
+
+// // MARK: - 公共接口
+
+// /// 开始扫描 BLE 设备
+// /// - Parameter timeout: 扫描超时时间,默认 10 秒
+// /// - Returns: 是否成功开始扫描
+// public func startScan(timeout: TimeInterval = 30.0) -> Bool {
+// os_log("🔍 开始 BLE 扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout)
+
+// guard !isScanning else {
+// os_log("⚠️ 扫描已在进行中", log: logger, type: .info)
+// return false
+// }
+
+// // 清空之前的设备列表
+// discoveredDevices.removeAll()
+
+// // iOS 18 兼容性检查
+// if #available(iOS 18.0, *) {
+// // iOS 18 使用新的权限检查方式
+// return startScanForIOS18(timeout: timeout)
+// } else {
+// // iOS 17 及以下使用传统方式
+// return startScanLegacy(timeout: timeout)
+// }
+// }
+
+// @available(iOS 18.0, *)
+// private func startScanForIOS18(timeout: TimeInterval) -> Bool {
+// // 对于 iOS 18,直接检查 CBCentralManager 状态
+// guard let centralManager = centralManager else {
+// os_log("❌ CBCentralManager 未初始化", log: logger, type: .error)
+// return false
+// }
+
+// let currentState = centralManager.state
+// os_log("📊 iOS 18 蓝牙状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState))
+
+// switch currentState {
+// case .poweredOn:
+// return performScanWithPermission(timeout: timeout)
+// case .unauthorized:
+// os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error)
+// showPermissionAlert()
+// return false
+// case .unknown:
+// os_log("⚠️ 蓝牙状态未知,等待状态更新", log: logger, type: .info)
+// // 设置监听器等待状态更新
+// waitForBluetoothState(timeout: timeout)
+// return true
+// default:
+// os_log("❌ 蓝牙不可用: %{public}@", log: logger, type: .error, getBluetoothStateString(currentState))
+// return false
+// }
+// }
+
+// private func startScanLegacy(timeout: TimeInterval) -> Bool {
+// // 检查并申请蓝牙权限
+// let authorization: CBManagerAuthorization = CBManager.authorization
+// os_log("📊 当前蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue)
+
+// switch authorization {
+// case .allowedAlways:
+// return performScanWithPermission(timeout: timeout)
+// case .denied, .restricted:
+// os_log("❌ 蓝牙权限被拒绝或受限", log: logger, type: .error)
+// showPermissionAlert()
+// return false
+// case .notDetermined:
+// // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗
+// if centralManager == nil {
+// setupCentralManager()
+// }
- // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
- guard let self = self else { return }
- if self.centralManager.state == .poweredOn {
- // 开始一个短暂的扫描来触发权限申请
- self.centralManager.scanForPeripherals(withServices: nil, options: nil)
- // 立即停止扫描
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- self.centralManager.stopScan()
- }
- } else if self.centralManager.state == .unauthorized {
- // 权限被拒绝,显示设置提示
- self.showPermissionAlert()
- }
- }
- return true
- @unknown default:
- os_log("⚠️ 未知的蓝牙权限状态", log: logger, type: .info)
- return false
- }
- }
-
- private func waitForBluetoothState(timeout: TimeInterval) {
- var checkCount = 0
- let maxChecks = 20
-
- let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
- guard let self = self else {
- timer.invalidate()
- return
- }
+// // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗
+// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
+// guard let self = self else { return }
+// if self.centralManager.state == .poweredOn {
+// // 开始一个短暂的扫描来触发权限申请
+// self.centralManager.scanForPeripherals(withServices: nil, options: nil)
+// // 立即停止扫描
+// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+// self.centralManager.stopScan()
+// }
+// } else if self.centralManager.state == .unauthorized {
+// // 权限被拒绝,显示设置提示
+// self.showPermissionAlert()
+// }
+// }
+// return true
+// @unknown default:
+// os_log("⚠️ 未知的蓝牙权限状态", log: logger, type: .info)
+// return false
+// }
+// }
+
+// private func waitForBluetoothState(timeout: TimeInterval) {
+// var checkCount = 0
+// let maxChecks = 20
+
+// let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
+// guard let self = self else {
+// timer.invalidate()
+// return
+// }
- checkCount += 1
- let currentState = self.centralManager?.state ?? .unknown
+// checkCount += 1
+// let currentState = self.centralManager?.state ?? .unknown
- os_log("🔍 检查蓝牙状态 (%d/%d): %{public}@", log: self.logger, type: .info, checkCount, maxChecks, self.getBluetoothStateString(currentState))
+// os_log("🔍 检查蓝牙状态 (%d/%d): %{public}@", log: self.logger, type: .info, checkCount, maxChecks, self.getBluetoothStateString(currentState))
- switch currentState {
- case .poweredOn:
- timer.invalidate()
- _ = self.performScanWithPermission(timeout: timeout)
- case .unauthorized:
- timer.invalidate()
- self.showPermissionAlert()
- case .poweredOff, .unsupported:
- timer.invalidate()
- os_log("❌ 蓝牙不可用", log: self.logger, type: .error)
- default:
- if checkCount >= maxChecks {
- timer.invalidate()
- os_log("⏰ 等待蓝牙状态超时", log: self.logger, type: .error)
- }
- }
- }
- }
-
- private func forceTriggerPermissionRequest(timeout: TimeInterval) {
- // 完全清理旧的 centralManager
- centralManager?.delegate = nil
- centralManager = nil
-
- // 强制重新初始化,确保触发权限申请
- let options: [String: Any] = [
- CBCentralManagerOptionShowPowerAlertKey: true,
- CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager"
- ]
-
- centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
-
- // 设置一个监听器来等待权限状态更新
- waitForPermissionResponse(timeout: timeout)
- }
-
- private func waitForPermissionResponse(timeout: TimeInterval) {
- // 设置一个定时器来检查权限状态变化
- var checkCount = 0
- let maxChecks = 20 // 最多检查20次,每次间隔0.5秒,总共10秒
-
- let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
- guard let self = self else {
- timer.invalidate()
- return
- }
+// switch currentState {
+// case .poweredOn:
+// timer.invalidate()
+// _ = self.performScanWithPermission(timeout: timeout)
+// case .unauthorized:
+// timer.invalidate()
+// self.showPermissionAlert()
+// case .poweredOff, .unsupported:
+// timer.invalidate()
+// os_log("❌ 蓝牙不可用", log: self.logger, type: .error)
+// default:
+// if checkCount >= maxChecks {
+// timer.invalidate()
+// os_log("⏰ 等待蓝牙状态超时", log: self.logger, type: .error)
+// }
+// }
+// }
+// }
+
+// private func forceTriggerPermissionRequest(timeout: TimeInterval) {
+// // 完全清理旧的 centralManager
+// centralManager?.delegate = nil
+// centralManager = nil
+
+// // 强制重新初始化,确保触发权限申请
+// let options: [String: Any] = [
+// CBCentralManagerOptionShowPowerAlertKey: true,
+// CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager"
+// ]
+
+// centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
+
+// // 设置一个监听器来等待权限状态更新
+// waitForPermissionResponse(timeout: timeout)
+// }
+
+// private func waitForPermissionResponse(timeout: TimeInterval) {
+// // 设置一个定时器来检查权限状态变化
+// var checkCount = 0
+// let maxChecks = 20 // 最多检查20次,每次间隔0.5秒,总共10秒
+
+// let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
+// guard let self = self else {
+// timer.invalidate()
+// return
+// }
- checkCount += 1
- let currentAuth = CBManager.authorization
+// checkCount += 1
+// let currentAuth = CBManager.authorization
- os_log("🔍 检查权限状态 (%d/%d): %d", log: self.logger, type: .info, checkCount, maxChecks, currentAuth.rawValue)
+// os_log("🔍 检查权限状态 (%d/%d): %d", log: self.logger, type: .info, checkCount, maxChecks, currentAuth.rawValue)
- switch currentAuth {
- case .allowedAlways:
- timer.invalidate()
- os_log("✅ 蓝牙权限已获得,开始扫描", log: self.logger, type: .info)
- _ = self.performScanWithPermission(timeout: timeout)
- case .denied, .restricted:
- timer.invalidate()
- os_log("❌ 蓝牙权限被拒绝", log: self.logger, type: .error)
- self.showPermissionAlert()
- case .notDetermined:
- if checkCount >= maxChecks {
- timer.invalidate()
- os_log("⏰ 等待权限响应超时", log: self.logger, type: .error)
- self.showPermissionAlert()
- }
- // 继续等待
- @unknown default:
- timer.invalidate()
- os_log("⚠️ 未知权限状态", log: self.logger, type: .error)
- }
- }
- }
-
- private func reinitializeCentralManager() {
- // 清理旧的 centralManager
- centralManager?.delegate = nil
- centralManager = nil
-
- // 重新初始化
- setupCentralManager()
- }
-
- /// 检查蓝牙权限状态
- private func checkBluetoothPermissionStatus() -> (hasPermission: Bool, needsRequest: Bool) {
- // 检查权限状态
- let authorization: CBManagerAuthorization
- if #available(iOS 13.1, *) {
- authorization = CBManager.authorization
- } else if #available(iOS 13.0, *) {
- if centralManager == nil {
- setupCentralManager()
- }
- authorization = centralManager?.authorization ?? .notDetermined
- } else {
- // iOS 13以下没有权限概念
- return (true, false)
- }
-
- os_log("📊 蓝牙权限状态: %d, CBCentralManager状态: %d", log: logger, type: .info, authorization.rawValue, centralManager?.state.rawValue ?? -1)
-
- switch authorization {
- case .allowedAlways:
- return (true, false)
- case .denied, .restricted:
- return (false, false)
- case .notDetermined:
- return (false, true)
- @unknown default:
- return (false, true)
- }
- }
-
- private func performScanWithPermission(timeout: TimeInterval) -> Bool {
- // 首先检查权限状态
- let permissionStatus = checkBluetoothPermissionStatus()
-
- if !permissionStatus.hasPermission {
- if permissionStatus.needsRequest {
- os_log("🔐 需要申请蓝牙权限", log: logger, type: .info)
- _ = requestBluetoothPermission()
- return false
- } else {
- os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error)
- showPermissionAlert()
- return false
- }
- }
-
- // 权限已获得,检查蓝牙硬件状态
- let currentState = centralManager.state
- os_log("📊 蓝牙硬件状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState))
-
- switch currentState {
- case .poweredOn:
- // 蓝牙已开启,可以扫描
- break
- case .unknown:
- os_log("⚠️ 蓝牙状态未知,等待状态更新...", log: logger, type: .info)
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
- _ = self?.performScanWithPermission(timeout: timeout)
- }
- return false
- case .poweredOff:
- // 区分是权限问题还是硬件关闭
- if permissionStatus.hasPermission {
- os_log("❌ 蓝牙硬件已关闭,请在设备设置中开启蓝牙", log: logger, type: .error)
- } else {
- os_log("❌ 可能是权限问题导致的蓝牙状态异常", log: logger, type: .error)
- _ = requestBluetoothPermission()
- }
- return false
- case .unauthorized:
- os_log("❌ 应用未授权使用蓝牙", log: logger, type: .error)
- showPermissionAlert()
- return false
- case .unsupported:
- os_log("❌ 设备不支持蓝牙", log: logger, type: .error)
- return false
- default:
- os_log("⚠️ 其他蓝牙状态: %d", log: logger, type: .error, currentState.rawValue)
- return false
- }
-
- // 开始BLE扫描
- centralManager.scanForPeripherals(withServices: nil, options: [
- CBCentralManagerScanOptionAllowDuplicatesKey: true
- ])
-
- isScanning = true
-
- // 设置扫描超时
- scanTimeout = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
- self?.stopScan()
- }
-
- os_log("✅ 开始BLE扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout)
- return true
- }
-
- private func showPermissionAlert() {
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
- let window = windowScene.windows.first else {
- os_log("❌ 无法获取当前窗口", log: logger, type: .error)
- return
- }
-
- let alert = UIAlertController(
- title: "需要蓝牙权限",
- message: "请在设置中开启蓝牙权限以使用设备扫描功能",
- preferredStyle: .alert
- )
-
- alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
- if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
- UIApplication.shared.open(settingsUrl)
- }
- })
-
- alert.addAction(UIAlertAction(title: "取消", style: .cancel))
-
- window.rootViewController?.present(alert, animated: true)
- }
-
- /// 停止扫描
- public func stopScan() {
- guard isScanning else { return }
-
- os_log("🛑 停止 BLE 扫描", log: logger, type: .info)
-
- centralManager.stopScan() // 直接使用,不需要可选绑定
- isScanning = false
-
- scanTimeout?.invalidate()
- scanTimeout = nil
- }
-
- /// 获取已发现的设备列表
- /// - Returns: 按最后发现时间排序的设备列表
- public func getDiscoveredDevices() -> [BleDeviceInfo] {
- return Array(discoveredDevices.values).sorted { $0.lastSeen > $1.lastSeen }
- }
-
- /// 清除设备列表
- public func clearDeviceList() {
- discoveredDevices.removeAll()
- os_log("🗑️ 已清除设备列表", log: logger, type: .info)
- }
-
- /// 检查是否正在扫描
- public func isScanningActive() -> Bool {
- return isScanning
- }
-
- /// 获取蓝牙状态
- public func getBluetoothState() -> CBManagerState {
- return centralManager.state // 直接返回,不需要 ?? .unknown
- }
-
- // MARK: - 私有方法
+// switch currentAuth {
+// case .allowedAlways:
+// timer.invalidate()
+// os_log("✅ 蓝牙权限已获得,开始扫描", log: self.logger, type: .info)
+// _ = self.performScanWithPermission(timeout: timeout)
+// case .denied, .restricted:
+// timer.invalidate()
+// os_log("❌ 蓝牙权限被拒绝", log: self.logger, type: .error)
+// self.showPermissionAlert()
+// case .notDetermined:
+// if checkCount >= maxChecks {
+// timer.invalidate()
+// os_log("⏰ 等待权限响应超时", log: self.logger, type: .error)
+// self.showPermissionAlert()
+// }
+// // 继续等待
+// @unknown default:
+// timer.invalidate()
+// os_log("⚠️ 未知权限状态", log: self.logger, type: .error)
+// }
+// }
+// }
+
+// private func reinitializeCentralManager() {
+// // 清理旧的 centralManager
+// centralManager?.delegate = nil
+// centralManager = nil
+
+// // 重新初始化
+// setupCentralManager()
+// }
+
+// /// 检查蓝牙权限状态
+// private func checkBluetoothPermissionStatus() -> (hasPermission: Bool, needsRequest: Bool) {
+// // 检查权限状态
+// let authorization: CBManagerAuthorization
+// if #available(iOS 13.1, *) {
+// authorization = CBManager.authorization
+// } else if #available(iOS 13.0, *) {
+// if centralManager == nil {
+// setupCentralManager()
+// }
+// authorization = centralManager?.authorization ?? .notDetermined
+// } else {
+// // iOS 13以下没有权限概念
+// return (true, false)
+// }
+
+// os_log("📊 蓝牙权限状态: %d, CBCentralManager状态: %d", log: logger, type: .info, authorization.rawValue, centralManager?.state.rawValue ?? -1)
+
+// switch authorization {
+// case .allowedAlways:
+// return (true, false)
+// case .denied, .restricted:
+// return (false, false)
+// case .notDetermined:
+// return (false, true)
+// @unknown default:
+// return (false, true)
+// }
+// }
+
+// private func performScanWithPermission(timeout: TimeInterval) -> Bool {
+// // 首先检查权限状态
+// let permissionStatus = checkBluetoothPermissionStatus()
+
+// if !permissionStatus.hasPermission {
+// if permissionStatus.needsRequest {
+// os_log("🔐 需要申请蓝牙权限", log: logger, type: .info)
+// _ = requestBluetoothPermission()
+// return false
+// } else {
+// os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error)
+// showPermissionAlert()
+// return false
+// }
+// }
+
+// // 权限已获得,检查蓝牙硬件状态
+// let currentState = centralManager.state
+// os_log("📊 蓝牙硬件状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState))
+
+// switch currentState {
+// case .poweredOn:
+// // 蓝牙已开启,可以扫描
+// break
+// case .unknown:
+// os_log("⚠️ 蓝牙状态未知,等待状态更新...", log: logger, type: .info)
+// DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+// _ = self?.performScanWithPermission(timeout: timeout)
+// }
+// return false
+// case .poweredOff:
+// // 区分是权限问题还是硬件关闭
+// if permissionStatus.hasPermission {
+// os_log("❌ 蓝牙硬件已关闭,请在设备设置中开启蓝牙", log: logger, type: .error)
+// } else {
+// os_log("❌ 可能是权限问题导致的蓝牙状态异常", log: logger, type: .error)
+// _ = requestBluetoothPermission()
+// }
+// return false
+// case .unauthorized:
+// os_log("❌ 应用未授权使用蓝牙", log: logger, type: .error)
+// showPermissionAlert()
+// return false
+// case .unsupported:
+// os_log("❌ 设备不支持蓝牙", log: logger, type: .error)
+// return false
+// default:
+// os_log("⚠️ 其他蓝牙状态: %d", log: logger, type: .error, currentState.rawValue)
+// return false
+// }
+
+// // 开始BLE扫描
+// centralManager.scanForPeripherals(withServices: nil, options: [
+// CBCentralManagerScanOptionAllowDuplicatesKey: true
+// ])
+
+// isScanning = true
+
+// // 设置扫描超时
+// scanTimeout = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
+// self?.stopScan()
+// }
+
+// os_log("✅ 开始BLE扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout)
+// return true
+// }
+
+// private func showPermissionAlert() {
+// guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+// let window = windowScene.windows.first else {
+// os_log("❌ 无法获取当前窗口", log: logger, type: .error)
+// return
+// }
+
+// let alert = UIAlertController(
+// title: "需要蓝牙权限",
+// message: "请在设置中开启蓝牙权限以使用设备扫描功能",
+// preferredStyle: .alert
+// )
+
+// alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
+// if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
+// UIApplication.shared.open(settingsUrl)
+// }
+// })
+
+// alert.addAction(UIAlertAction(title: "取消", style: .cancel))
+
+// window.rootViewController?.present(alert, animated: true)
+// }
+
+// /// 停止扫描
+// public func stopScan() {
+// guard isScanning else { return }
+
+// os_log("🛑 停止 BLE 扫描", log: logger, type: .info)
+
+// centralManager.stopScan() // 直接使用,不需要可选绑定
+// isScanning = false
+
+// scanTimeout?.invalidate()
+// scanTimeout = nil
+// }
+
+// /// 获取已发现的设备列表
+// /// - Returns: 按最后发现时间排序的设备列表
+// public func getDiscoveredDevices() -> [BleDeviceInfo] {
+// return Array(discoveredDevices.values).sorted { $0.lastSeen > $1.lastSeen }
+// }
+
+// /// 清除设备列表
+// public func clearDeviceList() {
+// discoveredDevices.removeAll()
+// os_log("🗑️ 已清除设备列表", log: logger, type: .info)
+// }
+
+// /// 检查是否正在扫描
+// public func isScanningActive() -> Bool {
+// return isScanning
+// }
+
+// /// 获取蓝牙状态
+// public func getBluetoothState() -> CBManagerState {
+// return centralManager.state // 直接返回,不需要 ?? .unknown
+// }
+
+// // MARK: - 私有方法
- private func addOrUpdateDevice(_ deviceInfo: BleDeviceInfo) {
- let identifier = deviceInfo.identifier
- discoveredDevices[identifier] = deviceInfo
-
- os_log("📱 发现设备: %{public}@", log: logger, type: .debug, deviceInfo.name ?? "Unknown")
- }
-
- /// 强制触发蓝牙权限申请
- public func requestBluetoothPermission() -> Bool {
- os_log("🔐 强制触发蓝牙权限申请", log: logger, type: .info)
-
- // 检查当前权限状态
- let authorization: CBManagerAuthorization
- if #available(iOS 13.1, *) {
- authorization = CBManager.authorization
- } else if #available(iOS 13.0, *) {
- // 确保centralManager已初始化
- if centralManager == nil {
- setupCentralManager()
- }
- authorization = centralManager.authorization
- } else {
- // iOS 13以下没有权限概念,直接返回true
- return true
- }
-
- os_log("📊 当前权限状态: %d", log: logger, type: .info, authorization.rawValue)
-
- switch authorization {
- case .notDetermined:
- // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗
- if centralManager == nil {
- os_log("📊 初始化蓝牙管理对象", log: logger, type: .info, "")
- setupCentralManager()
- }
+// private func addOrUpdateDevice(_ deviceInfo: BleDeviceInfo) {
+// let identifier = deviceInfo.identifier
+// discoveredDevices[identifier] = deviceInfo
+
+// os_log("📱 发现设备: %{public}@", log: logger, type: .debug, deviceInfo.name ?? "Unknown")
+// }
+
+// /// 强制触发蓝牙权限申请
+// public func requestBluetoothPermission() -> Bool {
+// os_log("🔐 强制触发蓝牙权限申请", log: logger, type: .info)
+
+// // 检查当前权限状态
+// let authorization: CBManagerAuthorization
+// if #available(iOS 13.1, *) {
+// authorization = CBManager.authorization
+// } else if #available(iOS 13.0, *) {
+// // 确保centralManager已初始化
+// if centralManager == nil {
+// setupCentralManager()
+// }
+// authorization = centralManager.authorization
+// } else {
+// // iOS 13以下没有权限概念,直接返回true
+// return true
+// }
+
+// os_log("📊 当前权限状态: %d", log: logger, type: .info, authorization.rawValue)
+
+// switch authorization {
+// case .notDetermined:
+// // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗
+// if centralManager == nil {
+// os_log("📊 初始化蓝牙管理对象", log: logger, type: .info, "")
+// setupCentralManager()
+// }
- // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
- guard let self = self else { return }
+// // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗
+// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
+// guard let self = self else { return }
- if self.centralManager.state == .poweredOn {
- // 开始一个短暂的扫描来触发权限申请
- self.centralManager.scanForPeripherals(withServices: nil, options: nil)
- // 立即停止扫描
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- self.centralManager.stopScan()
- }
- } else if self.centralManager.state == .unauthorized {
- // 权限被拒绝,显示设置提示
- self.showPermissionAlert()
- }
- }
- return true
- case .allowedAlways:
- os_log("✅ 权限已授予", log: logger, type: .info)
- return true
- case .denied, .restricted:
- os_log("❌ 权限被拒绝,显示设置提示", log: logger, type: .error)
- showPermissionAlert()
- return false
- @unknown default:
- os_log("⚠️ 未知权限状态", log: logger, type: .info)
- return false
- }
- }
-}
+// if self.centralManager.state == .poweredOn {
+// // 开始一个短暂的扫描来触发权限申请
+// self.centralManager.scanForPeripherals(withServices: nil, options: nil)
+// // 立即停止扫描
+// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+// self.centralManager.stopScan()
+// }
+// } else if self.centralManager.state == .unauthorized {
+// // 权限被拒绝,显示设置提示
+// self.showPermissionAlert()
+// }
+// }
+// return true
+// case .allowedAlways:
+// os_log("✅ 权限已授予", log: logger, type: .info)
+// return true
+// case .denied, .restricted:
+// os_log("❌ 权限被拒绝,显示设置提示", log: logger, type: .error)
+// showPermissionAlert()
+// return false
+// @unknown default:
+// os_log("⚠️ 未知权限状态", log: logger, type: .info)
+// return false
+// }
+// }
+// }
-// MARK: - CBCentralManagerDelegate
-@available(iOS 13.0, *)
-extension BleScanManager: CBCentralManagerDelegate {
-
- public func centralManagerDidUpdateState(_ central: CBCentralManager) {
- let stateString = getBluetoothStateString(central.state)
- os_log("🔧 CBCentralManager 状态更新: %{public}@ (原始值: %d)", log: logger, type: .info, stateString, central.state.rawValue)
-
- switch central.state {
- case .poweredOn:
- os_log("✅ 蓝牙已开启,可以开始扫描", log: logger, type: .info)
- // 如果有待处理的扫描请求,现在可以执行
- case .poweredOff:
- os_log("❌ 蓝牙已关闭", log: logger, type: .error)
- if isScanning {
- stopScan()
- }
- case .unauthorized:
- os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error)
- if isScanning {
- stopScan()
- }
- case .unsupported:
- os_log("❌ 设备不支持蓝牙", log: logger, type: .error)
- if isScanning {
- stopScan()
- }
- case .unknown:
- os_log("⚠️ 蓝牙状态未知,等待进一步更新", log: logger, type: .error)
- case .resetting:
- os_log("⚠️ 蓝牙正在重置", log: logger, type: .error)
- if isScanning {
- stopScan()
- }
- @unknown default:
- os_log("⚠️ 未知蓝牙状态: %d", log: logger, type: .error, central.state.rawValue)
- }
- }
-
- public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
- advertisementData: [String : Any], rssi RSSI: NSNumber) {
-
- let deviceInfo = BleDeviceInfo(
- identifier: peripheral.identifier,
- name: peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String,
- rssi: RSSI,
- advertisementData: advertisementData,
- discoveryMethod: .traditional,
- lastSeen: Date()
- )
-
- addOrUpdateDevice(deviceInfo)
- }
-
- public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
- os_log("🔄 恢复 CBCentralManager 状态", log: logger, type: .info)
-
- // 恢复扫描状态
- if dict[CBCentralManagerRestoredStateScanServicesKey] != nil {
- isScanning = true
- }
- }
-
- private func getBluetoothStateString(_ state: CBManagerState) -> String {
- switch state {
- case .unknown: return "未知状态"
- case .resetting: return "重置中"
- case .unsupported: return "设备不支持蓝牙"
- case .unauthorized: return "应用未授权使用蓝牙"
- case .poweredOff: return "蓝牙已关闭"
- case .poweredOn: return "蓝牙已开启"
- @unknown default: return "未知状态"
- }
- }
-}
+// // MARK: - CBCentralManagerDelegate
+// @available(iOS 13.0, *)
+// extension BleScanManager: CBCentralManagerDelegate {
+
+// public func centralManagerDidUpdateState(_ central: CBCentralManager) {
+// let stateString = getBluetoothStateString(central.state)
+// os_log("🔧 CBCentralManager 状态更新: %{public}@ (原始值: %d)", log: logger, type: .info, stateString, central.state.rawValue)
+
+// switch central.state {
+// case .poweredOn:
+// os_log("✅ 蓝牙已开启,可以开始扫描", log: logger, type: .info)
+// // 如果有待处理的扫描请求,现在可以执行
+// case .poweredOff:
+// os_log("❌ 蓝牙已关闭", log: logger, type: .error)
+// if isScanning {
+// stopScan()
+// }
+// case .unauthorized:
+// os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error)
+// if isScanning {
+// stopScan()
+// }
+// case .unsupported:
+// os_log("❌ 设备不支持蓝牙", log: logger, type: .error)
+// if isScanning {
+// stopScan()
+// }
+// case .unknown:
+// os_log("⚠️ 蓝牙状态未知,等待进一步更新", log: logger, type: .error)
+// case .resetting:
+// os_log("⚠️ 蓝牙正在重置", log: logger, type: .error)
+// if isScanning {
+// stopScan()
+// }
+// @unknown default:
+// os_log("⚠️ 未知蓝牙状态: %d", log: logger, type: .error, central.state.rawValue)
+// }
+// }
+
+// public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
+// advertisementData: [String : Any], rssi RSSI: NSNumber) {
+
+// let deviceInfo = BleDeviceInfo(
+// identifier: peripheral.identifier,
+// name: peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String,
+// rssi: RSSI,
+// advertisementData: advertisementData,
+// discoveryMethod: .traditional,
+// lastSeen: Date()
+// )
+
+// addOrUpdateDevice(deviceInfo)
+// }
+
+// public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
+// os_log("🔄 恢复 CBCentralManager 状态", log: logger, type: .info)
+
+// // 恢复扫描状态
+// if dict[CBCentralManagerRestoredStateScanServicesKey] != nil {
+// isScanning = true
+// }
+// }
+
+// private func getBluetoothStateString(_ state: CBManagerState) -> String {
+// switch state {
+// case .unknown: return "未知状态"
+// case .resetting: return "重置中"
+// case .unsupported: return "设备不支持蓝牙"
+// case .unauthorized: return "应用未授权使用蓝牙"
+// case .poweredOff: return "蓝牙已关闭"
+// case .poweredOn: return "蓝牙已开启"
+// @unknown default: return "未知状态"
+// }
+// }
+// }
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 93673ada8..ae25e7e3b 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
@@ -3,15 +3,16 @@ import CoreBluetooth
import os.log
import AVFoundation
-/// BLE服务类 - 专为AccessorySetupKit设备设计,支持自动重连和状态恢复
+/// BLE服务类 - 传统BLE扫描和连接实现
///
/// 核心特性:
-/// 1. 外设掉电→上电后自动重连(通过pendingReconnectUUIDs队列)
-/// 2. App被系统终止后,外设广播或特征通知时自动恢复连接
-/// 3. 使用CBCentralManagerOptionRestoreIdentifierKey实现系统级状态保留
-/// 4. 支持bluetooth-central后台模式,允许后台BLE操作
-/// 5. willRestoreState恢复系统保留的外设、扫描条件
-/// 6. 统一的重连队列管理,适配AccessorySetupKit工作流程
+/// 1. 传统BLE扫描和连接逻辑
+/// 2. 外设掉电→上电后自动重连(通过pendingReconnectUUIDs队列)
+/// 3. App被系统终止后,外设广播或特征通知时自动恢复连接
+/// 4. 使用CBCentralManagerOptionRestoreIdentifierKey实现系统级状态保留
+/// 5. 支持bluetooth-central后台模式,允许后台BLE操作
+/// 6. willRestoreState恢复系统保留的外设、扫描条件
+/// 7. 统一的重连队列管理
@available(iOS 13.0, *)
public class BleService: NSObject {
// MARK: - 单例实现
@@ -43,6 +44,13 @@ public class BleService: NSObject {
private var audioCharacteristic: CBCharacteristic?
private var isScanning = false
+ private var scanTimer: Timer?
+
+ // 新增:设备缓存和广播数据记录
+ private var discoveredDevices: [String: [String: Any]] = [:] // 以设备ID为key的设备信息缓存
+ private var deviceAdvertisementData: [String: [String: Any]] = [:] // 设备广播数据缓存
+ private var scanStartTime: Date?
+ private let maxCacheTime: TimeInterval = 300 // 5分钟缓存时间
// 是否是ota模式
var isEnterOta = false
@@ -50,8 +58,6 @@ public class BleService: NSObject {
private var callbacks = [Callback]()
private var connectionState = BleConst.STATE_DISCONNECTED
-
-
// 状态恢复相关
private var restoredPeripherals: [CBPeripheral] = []
@@ -66,22 +72,11 @@ public class BleService: NSObject {
// MARK: - 初始化
private override init() {
super.init()
-
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- // iOS 18+下不立即初始化,等待AccessorySetupKit
- } else {
- initializeCentralManager()
- }
- #else
initializeCentralManager()
- #endif
-
- // 初始化Opus音频处理器
initializeOpusProcessor()
}
- // 延迟初始化中央管理器
+ // 初始化中央管理器
private func initializeCentralManager() {
if centralManager != nil {
os_log("🔧 CBCentralManager已存在,跳过初始化", log: logger, type: .debug)
@@ -170,19 +165,13 @@ public class BleService: NSObject {
}
/// 开始扫描BLE设备
- public func startScan() {
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- // iOS 18+不使用传统扫描
- return
- }
- #endif
-
+ public func startScan(timeout: TimeInterval = 30.0) {
if centralManager == nil {
initializeCentralManager()
}
guard let manager = centralManager, manager.state == .poweredOn else {
+ os_log("❌ 蓝牙未就绪,无法开始扫描", log: logger, type: .error)
return
}
@@ -190,6 +179,9 @@ public class BleService: NSObject {
stopScan()
}
+ // 清空之前的扫描结果
+ discoveredDevices.removeAll()
+
isScanning = true
let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]
@@ -197,7 +189,14 @@ public class BleService: NSObject {
CBCentralManagerScanOptionAllowDuplicatesKey: false
])
- os_log("开始扫描BLE设备...", log: logger, type: .info)
+ os_log("🔍 开始扫描BLE设备,超时时间: %.1f秒", log: logger, type: .info, timeout)
+
+ // 设置扫描超时
+ scanTimer?.invalidate()
+ scanTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
+ self?.stopScan()
+ os_log("⏰ 扫描超时,自动停止", log: self?.logger ?? OSLog.default, type: .info)
+ }
}
/// 停止扫描
@@ -205,10 +204,37 @@ public class BleService: NSObject {
if isScanning && centralManager != nil {
centralManager?.stopScan()
isScanning = false
- os_log("停止BLE扫描", log: logger, type: .info)
+ scanTimer?.invalidate()
+ scanTimer = nil
+ os_log("⏹️ 停止BLE扫描", log: logger, type: .info)
}
}
+ /// 连接到指定UUID的设备
+ public func connectToDevice(uuid: String) -> Bool {
+ guard let deviceUUID = UUID(uuidString: uuid) else {
+ os_log("❌ 无效的设备UUID: %@", log: logger, type: .error, uuid)
+ return false
+ }
+
+ guard let manager = centralManager else {
+ os_log("❌ CBCentralManager未初始化", log: logger, type: .error)
+ return false
+ }
+
+ // 先尝试从已知外设中获取
+ let knownPeripherals = manager.retrievePeripherals(withIdentifiers: [deviceUUID])
+ if let peripheral = knownPeripherals.first {
+ connectPeripheral(peripheral)
+ return true
+ }
+
+ // 如果没有找到,添加到重连队列
+ addToReconnectQueue(uuid: deviceUUID)
+ os_log("📱 设备未找到,已添加到重连队列: %@", log: logger, type: .info, uuid)
+ return false
+ }
+
/// 连接到指定设备
public func connect(to peripheral: CBPeripheral) {
connectPeripheral(peripheral)
@@ -231,18 +257,18 @@ public class BleService: NSObject {
}
if peripheral.state == .connected {
- os_log("设备已连接,恢复会话: %@", log: logger, type: .info, peripheral.identifier.uuidString)
+ os_log("📱 设备已连接,恢复会话: %@", log: logger, type: .info, peripheral.identifier.uuidString)
resumeSession(for: peripheral)
return
}
if peripheral.state == .connecting {
- os_log("设备正在连接中: %@", log: logger, type: .info, peripheral.identifier.uuidString)
+ os_log("📱 设备正在连接中: %@", log: logger, type: .info, peripheral.identifier.uuidString)
return
}
if connectionState == BleConst.STATE_CONNECTED && peripheralDevice?.identifier != peripheral.identifier {
- os_log("断开当前设备,连接新设备", log: logger, type: .info)
+ os_log("📱 断开当前设备,连接新设备", log: logger, type: .info)
disconnect()
}
@@ -257,7 +283,7 @@ public class BleService: NSObject {
CBConnectPeripheralOptionNotifyOnNotificationKey: true
])
- os_log("开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString)
+ os_log("🔗 开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString)
}
/// 恢复已连接设备的会话
@@ -278,7 +304,7 @@ public class BleService: NSObject {
os_log("🔍 恢复会话时重新发现服务和特征", log: logger, type: .info)
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID])
- os_log("已恢复设备会话: %@", log: logger, type: .info, peripheral.identifier.uuidString)
+ os_log("✅ 已恢复设备会话: %@", log: logger, type: .info, peripheral.identifier.uuidString)
}
/// 断开当前连接
@@ -291,7 +317,7 @@ public class BleService: NSObject {
if centralManager != nil {
updateConnectionState(BleConst.STATE_DISCONNECTING)
centralManager?.cancelPeripheralConnection(peripheral)
- os_log("开始断开设备连接", log: logger, type: .info)
+ os_log("🔌 开始断开设备连接", log: logger, type: .info)
}
}
@@ -300,6 +326,11 @@ public class BleService: NSObject {
return connectionState == BleConst.STATE_CONNECTED
}
+ /// 获取扫描到的设备列表
+ public func getDiscoveredDevices() -> [[String: Any]] {
+ return Array(discoveredDevices.values)
+ }
+
// MARK: - 回调通知方法
/// 通知所有回调扫描结果
@@ -903,25 +934,7 @@ public class BleService: NSObject {
]
}
- // MARK: - AccessorySetupKit
-
- /// 在接收到AccessorySetupKit的accessoryAdded事件后调用
- func connectToAccessory(uuid: UUID) {
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- os_log("开始连接设备: %{public}@", log: logger, type: .info, uuid.uuidString)
-
- // 确保CBCentralManager已初始化
- if centralManager == nil {
- os_log("CBCentralManager未初始化,开始初始化", log: logger, type: .debug)
- initializeCentralManager()
- }
-
- // 添加到重连队列
- addToReconnectQueue(uuid: uuid)
- }
- #endif
- }
+
}
// MARK: - SwiftOpusAudioProcessor.AudioDataCallback
@@ -943,7 +956,7 @@ extension BleService: CBCentralManagerDelegate {
case .poweredOn:
os_log("🔧 蓝牙已开启", log: logger, type: .info)
- // 注:AccessorySetupKit应用不需要恢复传统BLE扫描
+ // 蓝牙就绪后可以开始扫描
// 恢复外设连接
if !restoredPeripherals.isEmpty {
@@ -1042,22 +1055,72 @@ extension BleService: CBCentralManagerDelegate {
// 原有方法
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
- // 将扫描结果转换为字典格式
+ let deviceId = peripheral.identifier.uuidString
+ let deviceName = peripheral.name ?? "Unknown"
+
+ // 构建设备基本信息
var deviceInfo: [String: Any] = [
- "name": peripheral.name ?? "Unknown",
- "id": peripheral.identifier.uuidString,
- "rssi": RSSI.intValue
+ "name": deviceName,
+ "id": deviceId,
+ "rssi": RSSI.intValue,
+ "discoveredAt": Date().timeIntervalSince1970
]
- // 添加广播数据
+ // 详细的广播数据记录
+ var advData: [String: Any] = [:]
+
+ // 制造商数据
if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data {
deviceInfo["manufacturerData"] = manufacturerData
+ advData["manufacturerData"] = manufacturerData.map { String(format: "%02x", $0) }.joined(separator: " ")
+ advData["manufacturerDataRaw"] = manufacturerData
}
+ // 服务UUID列表
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
- deviceInfo["serviceUUIDs"] = serviceUUIDs.map { $0.uuidString }
+ let serviceUUIDStrings = serviceUUIDs.map { $0.uuidString }
+ deviceInfo["serviceUUIDs"] = serviceUUIDStrings
+ advData["serviceUUIDs"] = serviceUUIDStrings
+ }
+
+ // 本地名称
+ if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
+ advData["localName"] = localName
+ }
+
+ // 发射功率
+ if let txPowerLevel = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber {
+ advData["txPowerLevel"] = txPowerLevel.intValue
}
+ // 服务数据
+ if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
+ var serviceDataDict: [String: String] = [:]
+ for (uuid, data) in serviceData {
+ serviceDataDict[uuid.uuidString] = data.map { String(format: "%02x", $0) }.joined(separator: " ")
+ }
+ advData["serviceData"] = serviceDataDict
+ }
+
+ // 可连接性
+ if let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber {
+ advData["isConnectable"] = isConnectable.boolValue
+ }
+
+ // 完整的广播数据
+ advData["rssi"] = RSSI.intValue
+ advData["timestamp"] = Date().timeIntervalSince1970
+
+ // 缓存设备信息和广播数据
+ discoveredDevices[deviceId] = deviceInfo
+ deviceAdvertisementData[deviceId] = advData
+
+ // 添加广播数据到设备信息中
+ deviceInfo["advertisementData"] = advData
+
+ os_log("📡 发现设备: %{public}@ (ID: %{public}@), RSSI: %d", log: logger, type: .info, deviceName, deviceId, RSSI.intValue)
+ os_log("📡 广播数据: %{public}@", log: logger, type: .debug, String(describing: advData))
+
// 报告给回调
notifyScanResult(devices: [deviceInfo])
}
@@ -1267,4 +1330,4 @@ extension BleService: CBPeripheralDelegate {
os_log("✅ 描述符写入成功: %{public}@", log: logger, type: .debug, descriptor.uuid.uuidString)
}
}
-}
+}
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 fb7f92d49..4d94982ec 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
@@ -12,79 +12,20 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
internal var statusEventSink: FlutterEventSink?
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleServicePlugin")
- // 标记是否已初始化所有服务
- private var servicesInitialized = false
-
// MARK: - 初始化方法
init(_ channel: FlutterMethodChannel) {
self.methodChannel = channel
super.init()
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
-
- // iOS 18环境下,只设置回调但不主动初始化会话
- let bleAccessorySetup = BleAccessorySetup.shared
-
- // 在accessoryAdded回调中延迟设置BleService
- bleAccessorySetup.setDeviceDiscoveredCallback { [weak self] uuid in
- // 确保服务已初始化
- self?.ensureServicesInitialized()
- // 然后处理发现的设备
- self?.handleDiscoveredDevice(uuid: uuid)
- }
-
- // 注释掉自动初始化,改为等待用户主动调用startDeviceAssociation
- // bleAccessorySetup.initializeSession()
-
- os_log("iOS 18环境:已设置设备发现回调,等待用户主动调用关联", log: logger, type: .info)
- } else {
- // 低于iOS 18,直接初始化所有服务
- initializeAllServices()
- }
- #else
- // 非AccessorySetupKit环境,直接初始化所有服务
- initializeAllServices()
- #endif
+ // 直接初始化BLE服务
+ initializeBleService()
}
-
- // 确保所有服务都已初始化
- private func ensureServicesInitialized() {
- if !servicesInitialized {
- // 设置BLE服务回调
- BleService.shared.addCallback(self)
-
- // 设置BleService到AccessorySetup
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- BleAccessorySetup.shared.setBleService(BleService.shared)
- }
- #endif
-
- servicesInitialized = true
- }
- }
-
- // 初始化所有服务(iOS 17及以下版本的标准行为)
- private func initializeAllServices() {
+ // 初始化BLE服务
+ private func initializeBleService() {
// 设置BLE服务回调
BleService.shared.addCallback(self)
-
- // 初始化AccessorySetup,但不自动开始会话
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- BleAccessorySetup.shared.setBleService(BleService.shared)
- BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in
- self?.handleDiscoveredDevice(uuid: uuid)
- }
- // 移除自动初始化,统一由startDeviceAssociation方法触发
- // BleAccessorySetup.shared.initializeSession()
- }
- #endif
-
- os_log("非iOS 18环境:已设置BLE服务但不自动初始化会话", log: logger, type: .info)
- servicesInitialized = true
+ os_log("✅ BLE服务已初始化", log: logger, type: .info)
}
// MARK: - FlutterPlugin Protocol
@@ -106,9 +47,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
- // 检查是否需要初始化服务
- checkAndInitializeServices()
-
switch call.method {
case "isConnected":
result(BleService.shared.isConnected())
@@ -123,31 +61,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
details: nil))
}
- case "isCompanionSupported":
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- result(true)
- } else {
- result(false)
- }
- #else
- result(false)
- #endif
-
- case "getPairedMacAddress":
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- let pairedUUID = BleAccessorySetup.shared.getPairedMacAddress()
- result(pairedUUID)
- } else {
- result(nil)
- }
- #else
- result(nil)
- #endif
-
-
-
case "isBleConnected":
result(BleService.shared.isConnected())
@@ -193,90 +106,46 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
case "playA2DP":
result(BleService.shared.playA2DP())
- // iOS 18+ AccessorySetupKit相关方法
- case "clearAssociations":
- // 清除设备配对关系
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- os_log("清除设备配对关系", log: logger, type: .info)
- BleAccessorySetup.shared.clearAssociations()
- result(true)
- } else {
- result(true)
- }
- #else
- result(true)
- #endif
-
- case "startDeviceAssociation":
- // 用户主动触发设备关联流程
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- os_log("用户主动触发设备关联流程", log: logger, type: .info)
-
- // 确保服务已初始化
- ensureServicesInitialized()
-
- // 重新初始化会话
- BleAccessorySetup.shared.initializeSession()
-
- // 返回成功,实际配对结果会通过回调通知
- result(true)
- } else {
- os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
- result(false)
- }
- #else
- os_log("当前环境不支持AccessorySetupKit", log: logger, type: .info)
- result(false)
- #endif
case "startScan":
let arguments = call.arguments as? [String: Any]
- let devicename = (arguments?["devicename"] as? String) ?? ""
+ let timeout = (arguments?["timeout"] as? Double) ?? 30.0
- // 修复:支持所有iOS版本
- if #available(iOS 18.0, *) {
- // iOS 18+ 使用新的BleScanManager
- os_log("iOS 18+ 使用 BleScanManager 进行扫描", log: logger, type: .info)
- let succ = BleScanManager.shared.startScan(timeout: 15.0)
- result(succ)
- } else {
- // iOS 17及以下使用传统BleService
- os_log("iOS 17及以下使用传统 BleService 进行扫描", log: logger, type: .info)
- BleService.shared.startScan()
+ os_log("🔍 开始传统BLE扫描,超时: %.1f秒", log: logger, type: .info, timeout)
+ BleService.shared.startScan(timeout: timeout)
result(true)
- }
-
- case "getDeviceInfo":
+
+ case "stopScan":
+ os_log("⏹️ 停止BLE扫描", log: logger, type: .info)
+ BleService.shared.stopScan()
+ result(true)
+
+ case "connectToDevice":
let arguments = call.arguments as? [String: Any]
- let address = (arguments?["address"] as? String) ?? ""
- let info = BleScanManager.shared.getDiscoveredDevices()
- result(info)
+ guard let deviceId = arguments?["deviceId"] as? String else {
+ result(FlutterError(code: "INVALID_ARGUMENT",
+ message: "缺少设备ID参数",
+ details: nil))
+ return
+ }
+
+ os_log("🔗 连接到设备: %@", log: logger, type: .info, deviceId)
+ let success = BleService.shared.connectToDevice(uuid: deviceId)
+ result(success)
+
+ case "getDiscoveredDevices":
+ let devices = BleService.shared.getDiscoveredDevices()
+ result(devices)
+
case "requestBluetoothPermission":
- BleScanManager.shared.requestBluetoothPermission();
- result(true)
+ // 移除这个方法调用,让iOS系统自然处理权限
+ result(FlutterMethodNotImplemented)
+
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - 私有方法
- // 检查并按需初始化服务
- private func checkAndInitializeServices() {
- #if canImport(AccessorySetupKit) && swift(>=5.9)
- if #available(iOS 18.0, *) {
- ensureServicesInitialized()
- }
- #endif
- }
-
- // AccessorySetupKit发现设备的处理
- private func handleDiscoveredDevice(uuid: UUID) {
- // BleAccessorySetup内部已进行连接,此处不再重复调用
- // os_log("收到设备发现回调: %{public}@", log: logger, type: .info, uuid.uuidString)
- // 可以在这里添加设备记录或其他操作
- }
-
private func sendEvent(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) {
if let eventSink = eventSink {
DispatchQueue.main.async {
@@ -284,25 +153,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
}
}
}
-
- // 获取顶层视图控制器的辅助方法
- private func getTopViewController(_ viewController: UIViewController) -> UIViewController {
- if let presented = viewController.presentedViewController {
- return getTopViewController(presented)
- }
-
- if let tabBarController = viewController as? UITabBarController,
- let selected = tabBarController.selectedViewController {
- return getTopViewController(selected)
- }
-
- if let navigationController = viewController as? UINavigationController,
- let visibleViewController = navigationController.visibleViewController {
- return getTopViewController(visibleViewController)
- }
-
- return viewController
- }
}
// MARK: - BleService.Callback
diff --git a/local_plugins/user_portrait/ios/user_portrait.podspec b/local_plugins/user_portrait/ios/user_portrait.podspec
deleted file mode 100644
index 65cae4b79..000000000
--- a/local_plugins/user_portrait/ios/user_portrait.podspec
+++ /dev/null
@@ -1,29 +0,0 @@
-#
-# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
-# Run `pod lib lint user_portrait.podspec` to validate before publishing.
-#
-Pod::Spec.new do |s|
- s.name = 'user_portrait'
- s.version = '0.0.1'
- s.summary = '用户画像'
- s.description = <<-DESC
-用户画像
- DESC
- s.homepage = 'http://example.com'
- s.license = { :file => '../LICENSE' }
- s.author = { 'Your Company' => 'email@example.com' }
- s.source = { :path => '.' }
- s.source_files = 'user_portrait/Sources/user_portrait/**/*'
- 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 = {'user_portrait_privacy' => ['user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy']}
-end
diff --git a/local_plugins/user_portrait/ios/user_portrait/Package.swift b/local_plugins/user_portrait/ios/user_portrait/Package.swift
index d8d28ea06..4d8e0e3fc 100644
--- a/local_plugins/user_portrait/ios/user_portrait/Package.swift
+++ b/local_plugins/user_portrait/ios/user_portrait/Package.swift
@@ -13,20 +13,12 @@ let package = Package(
],
dependencies: [],
targets: [
- .target(
- name: "user_portrait",
- 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
- ]
- )
- ]
+ .target(
+ name: "user_portrait",
+ dependencies: [
+
+ ],
+ path: "Sources/user_portrait"
+ )
+ ]
)
diff --git a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy
deleted file mode 100644
index a34b7e2e6..000000000
--- a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- NSPrivacyTrackingDomains
-
- NSPrivacyAccessedAPITypes
-
- NSPrivacyCollectedDataTypes
-
- NSPrivacyTracking
-
-
-
diff --git a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift
index b8cdc8b21..f43d0de1a 100644
--- a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift
+++ b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift
@@ -10,8 +10,8 @@ public class UserPortraitPlugin: NSObject, FlutterPlugin {
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
- case "getPlatformVersion":
- result("iOS " + UIDevice.current.systemVersion)
+ case "clean":
+ result(true)
default:
result(FlutterMethodNotImplemented)
}
diff --git a/local_plugins/user_portrait/pubspec.yaml b/local_plugins/user_portrait/pubspec.yaml
index 1e3f104cc..862da4ec5 100644
--- a/local_plugins/user_portrait/pubspec.yaml
+++ b/local_plugins/user_portrait/pubspec.yaml
@@ -17,21 +17,8 @@ dev_dependencies:
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:
@@ -39,34 +26,5 @@ flutter:
pluginClass: UserPortraitPlugin
ios:
pluginClass: UserPortraitPlugin
-
- # 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
+ swiftPackage:
+ path: ios/user_portrait
diff --git a/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart b/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart
deleted file mode 100644
index 7a2dd7e32..000000000
--- a/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart
+++ /dev/null
@@ -1,27 +0,0 @@
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:user_portrait/user_portrait_method_channel.dart';
-
-void main() {
- TestWidgetsFlutterBinding.ensureInitialized();
-
- MethodChannelUserPortrait platform = MethodChannelUserPortrait();
- const MethodChannel channel = MethodChannel('user_portrait');
-
- setUp(() {
- TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
- channel,
- (MethodCall methodCall) async {
- return '42';
- },
- );
- });
-
- tearDown(() {
- TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
- });
-
- test('getPlatformVersion', () async {
- expect(await platform.getPlatformVersion(), '42');
- });
-}
diff --git a/local_plugins/user_portrait/test/user_portrait_test.dart b/local_plugins/user_portrait/test/user_portrait_test.dart
deleted file mode 100644
index 02c2ecd8a..000000000
--- a/local_plugins/user_portrait/test/user_portrait_test.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:user_portrait/user_portrait.dart';
-import 'package:user_portrait/user_portrait_platform_interface.dart';
-import 'package:user_portrait/user_portrait_method_channel.dart';
-import 'package:plugin_platform_interface/plugin_platform_interface.dart';
-
-class MockUserPortraitPlatform
- with MockPlatformInterfaceMixin
- implements UserPortraitPlatform {
-
- @override
- Future getPlatformVersion() => Future.value('42');
-}
-
-void main() {
- final UserPortraitPlatform initialPlatform = UserPortraitPlatform.instance;
-
- test('$MethodChannelUserPortrait is the default instance', () {
- expect(initialPlatform, isInstanceOf());
- });
-
- test('getPlatformVersion', () async {
- UserPortrait userPortraitPlugin = UserPortrait();
- MockUserPortraitPlatform fakePlatform = MockUserPortraitPlatform();
- UserPortraitPlatform.instance = fakePlatform;
-
- expect(await userPortraitPlugin.getPlatformVersion(), '42');
- });
-}
diff --git a/pubspec.yaml b/pubspec.yaml
index 2d8ec5299..a6c743df9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -45,7 +45,6 @@ dependencies:
haptic_feedback: ^0.5.1+1
tencentcloud_cos_sdk_plugin: ^1.2.3
photo_view: ^0.15.0
-
window_size:
git:
url: https://github.com/google/flutter-desktop-embedding.git
From ebe60910615baff309e6e8a94fe8b7904cb764ec Mon Sep 17 00:00:00 2001
From: fdp <1286779656@qq.com>
Date: Tue, 15 Jul 2025 20:09:10 +0800
Subject: [PATCH 08/20] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BF=BB=E8=AF=91tts?=
=?UTF-8?q?=E6=92=AD=E6=8A=A5=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ios/Podfile.lock | 4 +-
ios/Runner.xcodeproj/project.pbxproj | 7 +-
.../controllers/translation_controller.dart | 1 -
.../agent_service/AgentServiceImpl.swift | 17 +-
.../azure_speech/AzureAsrHelper.kt | 2 +-
.../ios/Classes/AzureAsrHelper.swift | 618 ------------------
.../ios/Classes/AzureSpeechPlugin.h | 17 -
.../ios/Classes/AzureSpeechPlugin.swift | 422 ------------
.../ios/Classes/AzureTtsHelper.swift | 604 -----------------
.../Sources/azure_speech/AzureAsrHelper.swift | 444 ++++++++++---
.../azure_speech/AzureSpeechPlugin.swift | 189 ++++--
.../Sources/azure_speech/RecordFile.swift | 16 +-
12 files changed, 516 insertions(+), 1825 deletions(-)
delete mode 100644 local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift
delete mode 100644 local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h
delete mode 100644 local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift
delete mode 100644 local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 77f53258d..63f38bd9d 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -85,7 +85,7 @@ PODS:
- SDWebImage/Core (~> 5.17)
- spotify_sdk (0.0.1):
- Flutter
- - tencentcloud_cos_sdk_plugin (1.2.4):
+ - tencentcloud_cos_sdk_plugin (1.2.3):
- Flutter
- QCloudCOSXML (= 6.4.7)
- WechatOpenSDK-XCFramework (2.0.4)
@@ -203,7 +203,7 @@ SPEC CHECKSUMS:
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca
- tencentcloud_cos_sdk_plugin: 7bed564dbe72df23e7b5cacb1e51b1a20cb1639c
+ tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 34f3271ab..c04843123 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -252,7 +252,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
D013B7972DFA83E400B2B69B /* XCRemoteSwiftPackageReference "flutterfire" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
@@ -529,6 +529,7 @@
DEVELOPMENT_TEAM = 537SFY28XS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "Deep Voice";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -720,6 +721,7 @@
DEVELOPMENT_TEAM = 537SFY28XS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "Deep Voice";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -745,6 +747,7 @@
DEVELOPMENT_TEAM = 537SFY28XS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "Deep Voice";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -794,7 +797,7 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart
index 703c87575..98c9f6cf9 100644
--- a/lib/modules/translation/controllers/translation_controller.dart
+++ b/lib/modules/translation/controllers/translation_controller.dart
@@ -1,4 +1,3 @@
-import 'dart:ffi';
import 'dart:io';
import '../../../core/utils/permission_util.dart';
diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
index 251984c27..fd90f7047 100644
--- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
+++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
@@ -206,6 +206,12 @@ class AgentServiceImpl: NSObject {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
return false
}
+ guard let asrsetupSuccess = azureAsrHelper?.setupEventListeners(
+ callback: self,
+ ), asrsetupSuccess else {
+ sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
+ return false
+ }
guard let ttsSuccess = azureTtsHelper?.initialize(
ttsAppId: "",
@@ -316,7 +322,7 @@ class AgentServiceImpl: NSObject {
}
guard let success = azureAsrHelper?.startContinuousRecognition(
- callback: self,
+ // callback: self,
audioSourceType: audioSourceType
), success else {
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR")
@@ -346,8 +352,12 @@ class AgentServiceImpl: NSObject {
if !isRecognizing || !isInitialized {
return false
}
-
- azureAsrHelper?.pushAudioData(data: audioData)
+ guard let audioStream = azureAsrHelper?.audioStream else {
+ os_log("音频流未初始化", log: logger,type: .error)
+ return false
+}
+audioStream.saveAudioDataTo(data: audioData)
+
return true
}
@@ -743,6 +753,7 @@ class AgentServiceImpl: NSObject {
}
func dispose() -> Bool {
+ print("ai释放资源")
if isRecognizing {
stopRecognition()
}
diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
index 3adf31b59..bd6ffc884 100644
--- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
+++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
@@ -711,7 +711,7 @@ class AzureAsrHelper(private val context: Context) {
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data
try {
- continuousCallback?.onAudio(finalData)
+ //continuousCallback?.onAudio(finalData)
pushAudioStream?.write(finalData)
recordfile?.saveAudioDataToWav(finalData)
} catch (e: Exception) {
diff --git a/local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift
deleted file mode 100644
index ffb76605c..000000000
--- a/local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift
+++ /dev/null
@@ -1,618 +0,0 @@
-import Foundation
-import AVFoundation
-import MicrosoftCognitiveServicesSpeech
-// 自定义语音处理组件,提供音频流处理等功能
-import speech
-import os.log
-
-
-/**
- * Azure ASR Helper
- *
- * 基于微软Azure语音服务的ASR实现
- * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-recognize-speech
- */
-public class AzureAsrHelper: NSObject {
- private let tag = "AzureAsrHelper"
- // 日志对象
- private let log = OSLog(subsystem: "com.azure.speech", category: "AzureAsrHelper")
-
- // 核心组件
- private var speechConfig: SPXSpeechConfiguration?
- private var recognizer: SPXSpeechRecognizer?
- private var audioConfig: SPXAudioConfiguration?
-
- // 状态管理
- private var _isContinuousRecognitionActive = false
-
- // 配置参数
- private var currentLanguage = "zh-CN"
- private var supportedLanguages = ["zh-CN"]
- private var isAutoDetectLanguage = false
- private var subscriptionKey = ""
- private var region = ""
-
- // 音频源配置
- public enum AudioSourceType {
- /** 使用设备麦克风 */
- case microphone
-
- /** 使用外部提供的音频数据 */
- case external
- }
-
- private var audioSourceType = AudioSourceType.microphone
-
- // 音频处理
- private var externalAudioStream: ExternalAudioPullStream?
-
- /**
- * 初始化Azure语音服务
- *
- * @param subscriptionKey Azure 订阅密钥
- * @param region Azure 区域
- * @param supportedLanguages 支持的语言数组
- * @param audioSourceType 音频源类型
- * @return 初始化是否成功
- */
- public func initialize(
- subscriptionKey: String,
- region: String,
- supportedLanguages: [String] = ["zh-CN"],
- audioSourceType: AudioSourceType = .microphone
- ) -> Bool {
- do {
- // 检查配置是否为空
- if subscriptionKey.isEmpty || region.isEmpty {
- os_log("Azure 配置信息不完整", log: log, type: .error)
- return false
- }
-
- // 释放之前的资源
- dispose()
-
- // 保存配置
- self.subscriptionKey = subscriptionKey
- self.region = region
- self.audioSourceType = audioSourceType
-
- // 设置语言
- if !supportedLanguages.isEmpty {
- self.supportedLanguages = supportedLanguages
- }
-
- // 根据支持的语言数量决定是否启用自动语言检测
- self.isAutoDetectLanguage = supportedLanguages.count >= 2
-
- // 如果只有一种语言,设置为当前语言
- if !isAutoDetectLanguage && !supportedLanguages.isEmpty {
- self.currentLanguage = supportedLanguages[0]
- }
-
- // 创建语音配置
- speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
-
- if isAutoDetectLanguage {
- // 直接启用语言检测模式
- speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode")
- os_log("启用语言检测模式", log: log, type: .info)
- } else {
- // 设置指定的识别语言
- speechConfig?.speechRecognitionLanguage = currentLanguage
- }
-
- // 创建识别器
- return setupRecognizer()
- } catch {
- os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
- return false
- }
- }
-
- /**
- * 向音频流写入音频数据
- * 仅当音频源设置为external时有效
- *
- * @param data 音频数据字节数组
- */
- public func pushAudioData(data: Data) {
- if audioSourceType != .external {
- return
- }
-
- // 使用拉流模式,将数据推入队列
- externalAudioStream?.pushAudio(data)
- }
-
- /**
- * 开始连续语音识别
- *
- * @param callback 连续识别结果回调
- * @param audioSourceType 音频源类型
- * @return 是否成功开始识别
- */
- public func startContinuousRecognition(
- callback: ContinuousRecognizeCallback,
- audioSourceType: AudioSourceType = .microphone
- ) -> Bool {
- guard speechConfig != nil else {
- callback.onError("语音服务未初始化")
- return false
- }
-
- if _isContinuousRecognitionActive {
- return true
- }
-
- self.audioSourceType = audioSourceType
-
- // 重置识别器
- if !setupRecognizer() {
- callback.onError("重置识别器失败")
- return false
- }
-
- do {
- // 设置各种事件监听
- setupEventListeners(callback: callback)
-
- // 启动音频处理
- startAudioProcessing()
-
- // 开始连续识别
- try recognizer?.startContinuousRecognition()
- _isContinuousRecognitionActive = true
-
- os_log("连续识别已启动,音频源: %{public}@", log: log, type: .info, audioSourceType == .microphone ? "麦克风" : "外部")
-
- return true
- } catch {
- stopAudioProcessing()
- _isContinuousRecognitionActive = false
- callback.onError("启动连续识别失败: \(error.localizedDescription)")
- return false
- }
- }
-
- /**
- * 停止连续语音识别
- *
- * @return 是否成功停止
- */
- public func stopContinuousRecognition() -> Bool {
- guard speechConfig != nil else {
- os_log("语音服务未初始化", log: log, type: .error)
- return false
- }
-
- if !_isContinuousRecognitionActive {
- return true
- }
-
- do {
- guard let recognizer = recognizer else {
- os_log("识别器为空,重置状态", log: log, type: .info)
- _isContinuousRecognitionActive = false
- return true
- }
-
- if audioSourceType == .external {
- pushAudioData(data: Data())
- }
-
- // 停止连续识别
- try recognizer.stopContinuousRecognition()
-
- // 停止音频处理
- stopAudioProcessing()
-
- // 会话结束事件会设置_isContinuousRecognitionActive = false
- return true
- } catch {
- // 强制重置状态
- _isContinuousRecognitionActive = false
- os_log("停止连续识别失败: %{public}@", log: log, type: .error, error.localizedDescription)
-
- // 停止音频处理
- stopAudioProcessing()
-
- // 尝试强制关闭识别器
- recognizer = nil
-
- return false
- }
- }
-
- /**
- * 检查连续识别是否活跃
- */
- public func isContinuousRecognitionActive() -> Bool {
- return self._isContinuousRecognitionActive
- }
-
- /**
- * 释放所有资源
- */
- public func dispose() {
- do {
- // 如果正在进行连续识别,先停止
- if _isContinuousRecognitionActive {
- // 直接停止,不等待结果
- try? recognizer?.stopContinuousRecognition()
- _isContinuousRecognitionActive = false
- }
-
- // 停止音频处理
- stopAudioProcessing()
-
- // 释放资源
- recognizer = nil
- speechConfig = nil
- audioConfig = nil
-
- // 确保状态被重置
- _isContinuousRecognitionActive = false
- externalAudioStream = nil
- } catch {
- // 确保状态被重置
- _isContinuousRecognitionActive = false
- externalAudioStream = nil
- audioConfig = nil
- recognizer = nil
- speechConfig = nil
- }
- }
-
- // MARK: - 私有方法
-
- /**
- * 设置识别器
- */
- private func setupRecognizer() -> Bool {
- do {
- // 清理旧的识别器
- recognizer = nil
-
- // 设置音频配置
- switch audioSourceType {
- case .microphone:
- // 使用默认麦克风输入配置
- audioConfig = try SPXAudioConfiguration()
- case .external:
- // 使用拉流方式处理外部音频
- setupExternalAudioStream()
- }
-
- // 创建识别器
- if isAutoDetectLanguage {
- let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
- recognizer = try SPXSpeechRecognizer(
- speechConfiguration: speechConfig!,
- autoDetectSourceLanguageConfiguration: autoDetectConfig,
- audioConfiguration: audioConfig!
- )
- } else {
- recognizer = try SPXSpeechRecognizer(
- speechConfiguration: speechConfig!,
- audioConfiguration: audioConfig!
- )
- }
-
- return true
- } catch {
- os_log("创建识别器失败: %{public}@", log: log, type: .error, error.localizedDescription)
- stopAudioProcessing()
- return false
- }
- }
-
- /**
- * 设置外部音频流 - 使用拉流方式
- */
- private func setupExternalAudioStream() {
- do {
- // 创建外部音频拉流对象
- externalAudioStream = ExternalAudioPullStream()
-
- // 创建音频配置
- audioConfig = SPXAudioConfiguration(streamInput: externalAudioStream!.pullStream)
- } catch {
- os_log("设置外部音频流失败: %{public}@", log: log, type: .error, error.localizedDescription)
- externalAudioStream = nil
- }
- }
-
- /**
- * 设置事件监听器
- */
- private func setupEventListeners(callback: ContinuousRecognizeCallback) {
- guard let recognizer = recognizer else { return }
-
- // 识别中事件
- recognizer.addRecognizingEventHandler { [weak self] (sender, event) in
- guard let self = self else { return }
-
- let result = event.result
- let detectedLanguage = self.getDetectedLanguage(from: result)
- // os_log("识别中: %{public}@", log: self.log, type: .info, result.text ?? "")
- // 直接在当前线程调用回调
- callback.onRecognizing(result.text ?? "", detectedLanguage)
- }
-
- // 识别完成事件
- recognizer.addRecognizedEventHandler { [weak self] (sender, event) in
- guard let self = self else { return }
-
- let result = event.result
- if result.reason == SPXResultReason.recognizedSpeech {
- let detectedLanguage = self.getDetectedLanguage(from: result)
- // 直接在当前线程调用回调
- callback.onResult(result.text ?? "", detectedLanguage)
- }
- }
-
- // 会话开始事件
- recognizer.addSessionStartedEventHandler { (sender, event) in
- // 直接在当前线程调用回调
- callback.onSessionStarted()
- }
-
- // 会话结束事件
- recognizer.addSessionStoppedEventHandler { [weak self] (sender, event) in
- guard let self = self else { return }
-
- // 直接在当前线程调用回调
- callback.onSessionStopped()
- self._isContinuousRecognitionActive = false
- self.stopAudioProcessing()
- }
-
- // 取消事件
- recognizer.addCanceledEventHandler { [weak self] (sender, event) in
- guard let self = self else { return }
-
- let errorDetails = event.errorDetails ?? "未知错误"
- let reason = String(describing: event.reason.rawValue)
-
- os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails)
-
- callback.onCanceled(reason, errorDetails)
- }
- }
-
- /**
- * 获取检测到的语言
- */
- private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
- if !isAutoDetectLanguage {
- return ""
- }
-
- // 尝试从属性中获取语言
- if let properties = result.properties,
- let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") {
- return language
- }
-
- // 尝试另一种方式获取语言
- do {
- let langResult = try SPXAutoDetectSourceLanguageResult(result)
- return langResult.language ?? ""
- } catch {
- return ""
- }
- }
-
- /**
- * 启动音频处理
- */
- private func startAudioProcessing() {
- switch audioSourceType {
- case .microphone:
- // 拉流模式不需要额外启动,SDK会自动拉取数据
- break
- case .external:
- // 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData
- break
- }
- }
-
- /**
- * 停止音频处理
- */
- private func stopAudioProcessing() {
- if let stream = externalAudioStream {
- stream.close()
- externalAudioStream = nil
- // os_log("外部音频流已关闭", log: log, type: .info)
- }
- }
-
-
- /**
- * 外部音频拉流
- * 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式
- */
- private class ExternalAudioPullStream: NSObject {
- private(set) var pullStream: SPXPullAudioInputStream!
- private let queue = LinkedBlockingQueue()
- private var closed = false
-
- override init() {
- super.init()
-
- pullStream = SPXPullAudioInputStream(
- readHandler: { [weak self] (data: NSMutableData, size: UInt) -> Int in
- guard let self = self else { return 0 }
- return self.read(buffer: data, size: Int(size))
- },
- closeHandler: { [weak self] in
- self?.close()
- }
- )
- }
-
- /**
- * 外部调用:推送音频数据到队列
- * @param data 音频数据
- */
- func pushAudio(_ data: Data) {
- if !closed {
- queue.put(data)
- }
- }
-
- /**
- * SDK调用:从队列中拉取数据
- * @param buffer SDK提供的缓冲区
- * @param size 缓冲区大小
- * @return 读取的字节数,0表示流结束
- */
- private func read(buffer: NSMutableData, size: Int) -> Int {
- // 阻塞等待下一块数据
- guard let chunk = queue.take() else {
- return 0 // 队列已关闭
- }
-
- // 如果是空数据,表示流结束
- if chunk.isEmpty {
- return 0
- }
-
- let toCopy = min(chunk.count, size)
- buffer.append(chunk.prefix(toCopy))
- return toCopy
- }
-
- /**
- * SDK调用:关闭流
- */
- func close() {
- closed = true
- queue.close()
- }
- }
-
- /**
- * iOS版LinkedBlockingQueue实现
- * 与Android LinkedBlockingQueue保持一致的API和行为
- */
- private class LinkedBlockingQueue {
- private var queue: [T] = []
- private var isClosed = false
-
- // 使用DispatchSemaphore实现阻塞行为
- private let availableItems: DispatchSemaphore
- private let queueLock = NSLock()
-
- init() {
- self.availableItems = DispatchSemaphore(value: 0)
- }
-
- /**
- * 阻塞式获取元素(等价于Android的take())
- * @return 队列中的元素,如果队列已关闭则返回nil
- */
- func take() -> T? {
- // 等待可用元素(阻塞直到有元素或队列关闭)
- availableItems.wait()
-
- queueLock.lock()
- defer { queueLock.unlock() }
-
- // 检查队列是否已关闭
- if isClosed && queue.isEmpty {
- return nil
- }
-
- // 获取第一个元素
- guard !queue.isEmpty else {
- return nil
- }
-
- return queue.removeFirst()
- }
-
- /**
- * 添加元素(等价于Android的put())
- * @param item 要添加的元素
- */
- func put(_ item: T) {
- queueLock.lock()
- defer { queueLock.unlock() }
-
- // 检查队列是否已关闭
- if isClosed {
- return
- }
-
- // 添加元素(无容量限制,与Android一致)
- queue.append(item)
-
- // 通知有新元素可用
- availableItems.signal()
- }
-
- /**
- * 关闭队列
- * 关闭后不能再添加新元素,但可以继续取出已有元素
- */
- func close() {
- queueLock.lock()
- defer { queueLock.unlock() }
-
- isClosed = true
-
- // 唤醒所有等待的take()操作
- for _ in 0..<100 { // 假设最多100个等待者
- availableItems.signal()
- }
- }
- }
-
- /**
- * 连续识别回调接口
- */
- public protocol ContinuousRecognizeCallback {
- /**
- * 返回识别结果
- *
- * @param text 识别的文本
- * @param detectedLanguage 检测到的语言
- */
- func onResult(_ text: String, _ detectedLanguage: String)
-
- /**
- * 识别进行中调用
- *
- * @param recognizing 正在识别的文本
- * @param detectedLanguage 检测到的语言
- */
- func onRecognizing(_ recognizing: String, _ detectedLanguage: String)
-
- /**
- * 会话开始时调用
- */
- func onSessionStarted()
-
- /**
- * 会话结束时调用
- */
- func onSessionStopped()
-
- /**
- * 识别取消时调用
- *
- * @param reason 取消原因
- * @param errorDetails 错误详情
- */
- func onCanceled(_ reason: String, _ errorDetails: String)
-
- /**
- * 识别出错时调用
- *
- * @param error 错误信息
- */
- func onError(_ error: String)
- }
-}
\ No newline at end of file
diff --git a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h
deleted file mode 100644
index 7f8606465..000000000
--- a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// AzureSpeechPlugin.h
-// azure_speech
-//
-// Created for azure_speech plugin compatibility.
-//
-
-#import
-
-/**
- * Azure Speech Plugin 头文件
- *
- * 注:实际实现使用 Swift,此头文件仅用于 Flutter 框架兼容
- */
-@interface AzureSpeechPlugin : NSObject
-+ (void)registerWithRegistrar:(NSObject*)registrar;
-@end
\ No newline at end of file
diff --git a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift
deleted file mode 100644
index 66e375647..000000000
--- a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift
+++ /dev/null
@@ -1,422 +0,0 @@
-import Flutter
-import UIKit
-import AVFoundation
-import MicrosoftCognitiveServicesSpeech
-// 自定义语音处理组件,提供音频流处理等功能
-import speech
-import os.log
-
-/**
- * Azure Speech Plugin
- *
- * 基于微软Azure语音服务的Flutter插件
- * 提供语音识别(ASR)和语音合成(TTS)功能
- */
-@objc public class AzureSpeechPlugin: NSObject, FlutterPlugin {
- // 日志标签
- private let tag = "AzureSpeechPlugin"
- // 日志对象
- private let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin")
-
- // ASR相关
- private var asrChannel: FlutterMethodChannel?
- private var asrEventChannel: FlutterEventChannel?
- private var asrEventSink: FlutterEventSink?
- private let azureAsrHelper = AzureAsrHelper()
-
- // TTS相关
- private var ttsChannel: FlutterMethodChannel?
- private var ttsEventChannel: FlutterEventChannel?
- private var ttsEventSink: FlutterEventSink?
- private let azureTtsHelper = AzureTtsHelper()
-
- // 是否已添加TTS事件监听器
- private var isTtsListenerAdded = false
-
- // 当前的连续识别回调
- private var currentAsrCallback: AsrCallbackWrapper?
-
- // 插件注册
- public static func register(with registrar: FlutterPluginRegistrar) {
- let instance = AzureSpeechPlugin()
-
- // 初始化ASR通道
- let asrChannel = FlutterMethodChannel(name: "azure_speech/asr", binaryMessenger: registrar.messenger())
- registrar.addMethodCallDelegate(instance, channel: asrChannel)
- instance.asrChannel = asrChannel
-
- // 初始化TTS通道
- let ttsChannel = FlutterMethodChannel(name: "azure_speech/tts", binaryMessenger: registrar.messenger())
- registrar.addMethodCallDelegate(instance, channel: ttsChannel)
- instance.ttsChannel = ttsChannel
-
- // 初始化ASR事件通道
- let asrEventChannel = FlutterEventChannel(name: "azure_speech/asr_events", binaryMessenger: registrar.messenger())
- asrEventChannel.setStreamHandler(instance)
- instance.asrEventChannel = asrEventChannel
-
- // 初始化TTS事件通道
- let ttsEventChannel = FlutterEventChannel(name: "azure_speech/tts_events", binaryMessenger: registrar.messenger())
- ttsEventChannel.setStreamHandler(instance)
- instance.ttsEventChannel = ttsEventChannel
- }
-
- // 发送ASR事件方法
- internal func sendAsrEvent(_ event: [String: Any]) {
- if asrEventSink == nil {
- os_log("无法发送ASR事件:事件通道未准备好", log: log, type: .error)
- return
- }
-
- DispatchQueue.main.async { [weak self] in
- guard let self = self else { return }
- self.asrEventSink?(event)
- }
- }
-
- // 发送TTS事件方法
- private func sendTtsEvent(_ event: [String: Any]) {
- if ttsEventSink == nil {
- os_log("无法发送TTS事件:事件通道未准备好", log: log, type: .error)
- return
- }
-
- DispatchQueue.main.async { [weak self] in
- guard let self = self else { return }
- self.ttsEventSink?(event)
- }
- }
-
- // 设置TTS事件监听器
- private func setupTtsEventListener() {
- if !isTtsListenerAdded {
- azureTtsHelper.addListener(self)
- isTtsListenerAdded = true
- }
- }
-
-
-
- // 处理Flutter方法调用
- public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
- if call.method.hasPrefix("tts_") {
- handleTtsMethodCall(call, result)
- } else {
- handleAsrMethodCall(call, result)
- }
- }
-
- // MARK: - ASR 方法处理
-
- private func handleAsrMethodCall(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
- switch call.method {
- case "initialize":
- guard let args = call.arguments as? [String: Any],
- let subscriptionKey = args["subscriptionKey"] as? String,
- let region = args["region"] as? String else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "必要的参数不能为空", details: nil))
- return
- }
-
- let supportedLanguages = args["supportedLanguages"] as? [String] ?? ["zh-CN"]
-
- // 初始化ASR引擎
- let success = azureAsrHelper.initialize(
- subscriptionKey: subscriptionKey,
- region: region,
- supportedLanguages: supportedLanguages,
- audioSourceType: .microphone
- )
-
- result(success)
-
- case "recognizeOnce":
- // iOS版本不支持recognizeOnce
- result(FlutterError(code: "NOT_SUPPORTED", message: "iOS版本不支持recognizeOnce", details: nil))
-
- case "startContinuousRecognition":
- // 确保事件通道已准备好
- guard asrEventSink != nil else {
- result(FlutterError(code: "EVENT_CHANNEL_NOT_READY", message: "事件通道未准备好,无法开始连续识别", details: nil))
- return
- }
-
- // 解析音频源类型
- let audioSourceType: AzureAsrHelper.AudioSourceType
- if let args = call.arguments as? [String: Any],
- let audioSourceString = args["audioSourceType"] as? String {
- switch audioSourceString.lowercased() {
- case "external":
- audioSourceType = .external
- default:
- audioSourceType = .microphone
- }
- } else {
- audioSourceType = .microphone
- }
-
- // 创建回调包装器
- currentAsrCallback = AsrCallbackWrapper(plugin: self)
-
- let success = azureAsrHelper.startContinuousRecognition(
- callback: currentAsrCallback!,
- audioSourceType: audioSourceType
- )
- result(success)
-
- case "stopContinuousRecognition":
- let success = azureAsrHelper.stopContinuousRecognition()
- currentAsrCallback = nil
- result(success)
-
- case "isContinuousRecognitionActive":
- result(azureAsrHelper.isContinuousRecognitionActive())
-
- case "dispose":
- azureAsrHelper.dispose()
- currentAsrCallback = nil
- result(true)
-
- case "pushAudioData":
- guard let args = call.arguments as? [String: Any],
- let audioBytes = args["data"] as? FlutterStandardTypedData else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "音频数据不能为空", details: nil))
- return
- }
-
- azureAsrHelper.pushAudioData(data: audioBytes.data)
- result(true)
-
- default:
- result(FlutterMethodNotImplemented)
- }
- }
-
- // MARK: - TTS 方法处理
-
- private func handleTtsMethodCall(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
- switch call.method {
- case "tts_initialize":
- guard let args = call.arguments as? [String: Any],
- let subscriptionKey = args["subscriptionKey"] as? String,
- let region = args["region"] as? String else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "必要的参数不能为空", details: nil))
- return
- }
-
- // 初始化TTS引擎
- let language = args["language"] as? String ?? "zh-CN"
- let success = azureTtsHelper.initialize(ttsAppId: "", ttsAppToken: subscriptionKey, ttsResource: region, language: language)
-
- // 设置TTS事件监听器
- setupTtsEventListener()
-
- result(success)
-
- case "tts_set_voice":
- guard let args = call.arguments as? [String: Any],
- let voiceName = args["voiceName"] as? String else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "语音名称不能为空", details: nil))
- return
- }
-
- let success = azureTtsHelper.setVoice(voiceName)
- result(success)
-
- case "tts_speak_once":
- guard let args = call.arguments as? [String: Any],
- let text = args["text"] as? String else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
- return
- }
-
- let success = azureTtsHelper.speakOnce(text)
- result(success)
-
- case "tts_speak_stream":
- guard let args = call.arguments as? [String: Any],
- let text = args["text"] as? String else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
- return
- }
-
- let success = azureTtsHelper.speakStream(text)
- result(success)
-
- case "tts_flush_stream":
- let success = azureTtsHelper.flushStream()
- result(success)
-
- case "tts_stop":
- let success = azureTtsHelper.stop()
- result(success)
-
- case "tts_isSpeaking":
- result(azureTtsHelper.isSpeaking())
-
- case "tts_release":
- azureTtsHelper.dispose()
- result(true)
-
- default:
- result(FlutterMethodNotImplemented)
- }
- }
-}
-
-// MARK: - ASR回调包装器
-private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
- private weak var plugin: AzureSpeechPlugin?
-
- init(plugin: AzureSpeechPlugin) {
- self.plugin = plugin
- }
-
- func onResult(_ text: String, _ detectedLanguage: String) {
- plugin?.sendAsrEvent([
- "type": "result",
- "text": text,
- "language": detectedLanguage
- ])
- }
-
- func onRecognizing(_ recognizing: String, _ detectedLanguage: String) {
- plugin?.sendAsrEvent([
- "type": "recognizing",
- "text": recognizing,
- "language": detectedLanguage
- ])
- }
-
- func onSessionStarted() {
- plugin?.sendAsrEvent([
- "type": "sessionStarted"
- ])
- }
-
- func onSessionStopped() {
- plugin?.sendAsrEvent([
- "type": "sessionStopped"
- ])
- }
-
- func onCanceled(_ reason: String, _ errorDetails: String) {
- plugin?.sendAsrEvent([
- "type": "canceled",
- "reason": reason,
- "errorDetails": errorDetails
- ])
- }
-
- func onError(_ error: String) {
- plugin?.sendAsrEvent([
- "type": "error",
- "error": error
- ])
- }
-}
-
-// MARK: - 事件处理
-extension AzureSpeechPlugin: FlutterStreamHandler {
- public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
- // 根据通道类型设置事件接收器
- if let args = arguments as? [String: Any],
- let channel = args["channel"] as? String {
-
- if channel == "asr" {
- asrEventSink = events
- } else if channel == "tts" {
- ttsEventSink = events
- setupTtsEventListener()
- }
- } else if arguments == nil {
- // 如果没有指定通道,尝试弄清楚是哪个通道在监听
- if asrEventSink == nil && ttsEventSink != nil {
- asrEventSink = events
- } else if asrEventSink != nil && ttsEventSink == nil {
- ttsEventSink = events
- setupTtsEventListener()
- } else {
- // 无法确定哪个通道,默认设置为ASR
- asrEventSink = events
- }
- }
-
- return nil
- }
-
- public func onCancel(withArguments arguments: Any?) -> FlutterError? {
- // 根据通道类型清除事件接收器
- if let args = arguments as? [String: Any],
- let channel = args["channel"] as? String {
-
- if channel == "asr" {
- asrEventSink = nil
- } else if channel == "tts" {
- ttsEventSink = nil
- }
- } else {
- // 如果没有指定通道,清除所有通道
- asrEventSink = nil
- ttsEventSink = nil
- }
-
- return nil
- }
-}
-
-
-
-// MARK: - TTS 事件监听实现
-extension AzureSpeechPlugin: TtsEventListener {
- // 使用@objc特性为方法提供一个不同的Objective-C选择器名称
- @objc(onTtsEvent:)
- public func onEvent(_ event: TtsEvent) {
- var eventMap: [String: Any] = [:]
-
- // 根据事件类型转换
- switch event.type {
- case .synthesisStarted:
- eventMap["type"] = "synthesis_started"
- case .synthesisCompleted:
- eventMap["type"] = "synthesis_completed"
- case .synthesisCanceled:
- eventMap["type"] = "synthesis_canceled"
- if let reason = event.params["reason"] {
- eventMap["reason"] = reason
- }
- if let errorDetails = event.params["errorDetails"] {
- eventMap["errorDetails"] = errorDetails
- }
- case .error:
- eventMap["type"] = "error"
- if let errorCode = event.params["errorCode"] {
- eventMap["errorCode"] = errorCode
- }
- if let errorMessage = event.params["errorMessage"] {
- eventMap["errorMessage"] = errorMessage
- }
- @unknown default:
- eventMap["type"] = "unknown"
- for (key, value) in event.params {
- eventMap[key] = value
- }
- }
-
- sendTtsEvent(eventMap)
- }
-}
-
-// MARK: - 音频数据监听实现
-extension AzureSpeechPlugin: AudioDataListener {
- public func onAudioData(_ data: Data) {
- if ttsEventSink == nil { return }
-
- let eventMap: [String: Any] = [
- "type": "audio_data",
- "data": FlutterStandardTypedData(bytes: data)
- ]
-
- sendTtsEvent(eventMap)
- }
-}
\ No newline at end of file
diff --git a/local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift b/local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift
deleted file mode 100644
index 5a390d707..000000000
--- a/local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift
+++ /dev/null
@@ -1,604 +0,0 @@
-import Foundation
-import AVFoundation
-import MicrosoftCognitiveServicesSpeech
-// 自定义语音处理组件,提供音频流处理等功能
-import speech
-import os.log
-
-/**
- * Azure TTS Helper
- *
- * 基于微软Azure语音服务的TTS实现
- * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
- *
- * 特性:
- * - 对外接口保持同步,内部异步处理
- * - 使用专用队列保证合成顺序
- * - 避免阻塞主线程和其他后台线程
- * - 事件回调由调用方处理线程切换
- *
- * 使用示例:
- * let success = ttsHelper.speakOnce("Hello World") // 立即返回,后台异步处理
- */
- public class AzureTtsHelper: NSObject, ITtsService {
- private let tag = "AzureTtsHelper"
- // 日志对象
- private let log = OSLog(subsystem: "com.azure.speech", category: "AzureTtsHelper")
-
- private static let DEFAULT_LANGUAGE = "zh-CN"
- private static let DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
-
- // Azure语音服务配置
- private var speechConfig: SPXSpeechConfiguration?
- private var synthesizer: SPXSpeechSynthesizer?
- private var isInitialized = false
- private var speaking = false
-
- // 当前配置
- private var currentVoice = DEFAULT_VOICE
- private var currentLanguage = DEFAULT_LANGUAGE
- private var currentRate = "+0%"
- private var currentPitch = "+0%"
- private var currentVolume = "100%"
-
- // 事件监听器列表
- private var eventListeners = NSHashTable.weakObjects()
-
- // 流式文本处理的缓冲区
- private var streamBuffer = ""
- private var lastSpeakTime: TimeInterval = 0
-
- // 内部异步处理队列,保证顺序执行
- private let synthesisQueue = DispatchQueue(label: "com.azure.tts.synthesis", qos: .userInitiated)
- private let synthesisGroup = DispatchGroup()
- private var pendingTasks: [() -> Void] = []
- private let taskLock = NSLock()
-
- /**
- * 初始化语音合成服务
- *
- * @param appId 服务应用ID
- * @param token 服务访问令牌/订阅密钥
- * @param resource 服务资源ID/区域(可选)
- * @param language 语言代码,如"zh-CN"
- * @return 是否初始化成功
- */
- public func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String) -> Bool {
- do {
- // 创建语音配置
- if ttsResource.isEmpty {
- speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: "eastasia")
- } else {
- speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: ttsResource)
- }
-
- // 设置语言
- speechConfig?.speechSynthesisLanguage = language
- currentLanguage = language
-
- // 设置音频输出格式 - 使用16k、16位的PCM格式
- speechConfig?.setPropertyTo("Audio16Khz16BitMonoPcm",
- byName: "SpeechServiceConnection_SynthOutputFormat")
-
- // 创建合成器,使用默认音频输出(扬声器)
- synthesizer = try SPXSpeechSynthesizer(speechConfig!)
-
- // 设置事件监听
- setupEventListeners()
-
- // 设置默认音色 - 中文默认使用晓晓,英文默认使用Jenny
- if language.lowercased().starts(with: "zh") {
- _ = setVoice("zh-CN-XiaoxiaoNeural")
- } else {
- _ = setVoice("en-US-JennyNeural")
- }
-
- // 设置初始化完成
- isInitialized = true
-
- return true
- } catch {
- os_log("语音合成服务初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
- return false
- }
- }
-
- /**
- * 设置语音角色
- *
- * @param voiceName 语音角色名称(不同服务的语音角色命名可能不同)
- * @return 是否设置成功
- */
- public func setVoice(_ voiceName: String) -> Bool {
- if !isInitialized { return false }
-
- do {
- currentVoice = voiceName
-
- // 更新语音名称
- if let config = speechConfig {
- config.speechSynthesisVoiceName = voiceName
- }
-
- // 重新创建合成器
- recreateSynthesizer()
- return true
- } catch {
- os_log("设置语音失败: %{public}@", log: log, type: .error, error.localizedDescription)
- notifyEvent(eventType: .error, params: [
- "errorCode": "VOICE_SET_FAILED",
- "errorMessage": "设置语音失败: \(error.localizedDescription)"
- ])
- return false
- }
- }
-
- /**
- * 单次合成并播放
- *
- * @param text 要合成的文本
- * @return 是否成功开始合成
- */
- public func speakOnce(_ text: String) -> Bool {
- if !isInitialized {
- os_log("语音合成未初始化", log: log, type: .error)
- return false
- }
-
- // 立即返回成功,内部异步处理
- enqueueSynthesisTask {
- self.performSynthesis(text: text)
- }
-
- return true
- }
-
- /**
- * 将合成任务加入队列,保证顺序执行
- */
- private func enqueueSynthesisTask(_ task: @escaping () -> Void) {
- taskLock.lock()
- defer { taskLock.unlock() }
-
- pendingTasks.append(task)
-
- // 如果当前没有任务在执行,开始处理队列
- if pendingTasks.count == 1 {
- processNextTask()
- }
- }
-
- /**
- * 处理队列中的下一个任务
- */
- private func processNextTask() {
- synthesisQueue.async {
- self.synthesisGroup.enter()
-
- self.taskLock.lock()
- guard !self.pendingTasks.isEmpty else {
- self.taskLock.unlock()
- self.synthesisGroup.leave()
- return
- }
- let task = self.pendingTasks.removeFirst()
- self.taskLock.unlock()
-
- // 执行任务
- task()
-
- self.synthesisGroup.leave()
-
- // 处理下一个任务
- self.taskLock.lock()
- if !self.pendingTasks.isEmpty {
- self.taskLock.unlock()
- self.processNextTask()
- } else {
- self.taskLock.unlock()
- }
- }
- }
-
- /**
- * 实际执行合成的方法
- */
- private func performSynthesis(text: String) {
- // 重置状态
- speaking = true
-
- // 生成SSML
- let ssml = generateSsml(text)
-
- do {
- os_log("开始合成: %{public}@", log: log, type: .debug, ssml)
- // 使用同步方法,在后台队列中执行
- let result = try synthesizer?.startSpeakingSsml(ssml)
-
- // 检查结果
- if let result = result {
- os_log("合成完成,结果: %{public}@", log: log, type: .debug, String(describing: result.reason))
- }
- } catch {
- os_log("语音合成失败: %{public}@", log: log, type: .error, error.localizedDescription)
- notifyEvent(eventType: .error, params: [
- "errorCode": "SYNTHESIS_FAILED",
- "errorMessage": error.localizedDescription
- ])
- speaking = false
- }
- }
-
- /**
- * 流式合成文本
- *
- * @param text 要合成的文本片段
- * @return 是否成功处理
- */
- public func speakStream(_ text: String) -> Bool {
- if !isInitialized || text.isEmpty {
- if !isInitialized {
- notifyEvent(eventType: .error, params: [
- "errorCode": "NOT_INITIALIZED",
- "errorMessage": "TTS引擎未初始化"
- ])
- }
- return false
- }
-
- do {
- // 添加新文本到缓冲区
- streamBuffer.append(text)
-
- // 增加500ms防抖逻辑
- let currentTime = Date().timeIntervalSince1970
- if currentTime - lastSpeakTime < 0.6 {
- return true
- }
- lastSpeakTime = currentTime
-
- let currentText = streamBuffer
-
- // 定义标点符号列表
- let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":", "\n"]
-
- // 查找最后一个标点符号的位置
- var lastPunctuationIndex = -1
- for (i, char) in currentText.enumerated().reversed() {
- if punctuationMarks.contains(char) {
- lastPunctuationIndex = i
- break
- }
- }
-
- // 如果找到标点符号,则播放到该标点符号
- if lastPunctuationIndex >= 0 {
- // 提取要播放的文本(包含标点符号)
- let textToSpeak = String(currentText.prefix(lastPunctuationIndex + 1)).trimmingCharacters(in: .whitespacesAndNewlines)
-
- // 剩余的文本保存在缓冲区中
- let startIndex = currentText.index(currentText.startIndex, offsetBy: lastPunctuationIndex + 1)
- streamBuffer = String(currentText[startIndex...])
-
- // 只有非空文本才播放
- if !textToSpeak.isEmpty {
- return speakOnce(textToSpeak)
- }
- }
-
- // 如果没有找到标点符号,则等待更多文本
- return true
- } catch {
- os_log("流式语音合成失败: %{public}@", log: log, type: .error, error.localizedDescription)
- notifyEvent(eventType: .error, params: [
- "errorCode": "STREAM_FAILED",
- "errorMessage": "流式语音合成失败: \(error.localizedDescription)"
- ])
- return false
- }
- }
-
- /**
- * 刷新并播放流式文本缓冲区中的剩余内容
- *
- * @return 是否成功处理
- */
- public func flushStream() -> Bool {
- if !isInitialized {
- notifyEvent(eventType: .error, params: [
- "errorCode": "NOT_INITIALIZED",
- "errorMessage": "TTS引擎未初始化"
- ])
- return false
- }
-
- do {
- // 获取缓冲区中剩余的文本
- let remainingText = streamBuffer.trimmingCharacters(in: .whitespacesAndNewlines)
-
- // 清空缓冲区
- streamBuffer = ""
-
- // 如果缓冲区为空,直接返回成功
- if remainingText.isEmpty {
- return true
- }
-
- // 播放剩余文本
- return speakOnce(remainingText)
- } catch {
- os_log("刷新流式文本失败: %{public}@", log: log, type: .error, error.localizedDescription)
- notifyEvent(eventType: .error, params: [
- "errorCode": "FLUSH_FAILED",
- "errorMessage": "刷新流式文本失败: \(error.localizedDescription)"
- ])
- return false
- }
- }
-
- /**
- * 停止语音合成和播放
- *
- * @return 是否成功停止
- */
- public func stop() -> Bool {
- // 立即更新状态
- speaking = false
-
- // 清除流式缓冲区中的待播放内容
- streamBuffer = ""
-
- // 清空待处理的任务队列
- taskLock.lock()
- pendingTasks.removeAll()
- taskLock.unlock()
-
- // 在后台队列停止合成器,避免阻塞主线程
- synthesisQueue.async {
- if let synthesizer = self.synthesizer {
- do {
- try synthesizer.stopSpeaking()
- os_log("语音合成已停止", log: self.log, type: .info)
-
- // 直接通知停止完成
- self.notifyEvent(eventType: .synthesisCanceled)
- } catch {
- os_log("停止语音合成失败: %{public}@", log: self.log, type: .error, error.localizedDescription)
- self.notifyEvent(eventType: .error, params: [
- "errorCode": "STOP_FAILED",
- "errorMessage": error.localizedDescription
- ])
- }
- }
- }
-
- return true
- }
-
- /**
- * 释放资源
- * 在不再需要服务时调用,释放底层资源
- */
- public func dispose() {
- // 停止播放
- _ = stop()
-
- // 等待所有任务完成
- synthesisGroup.wait()
-
- // 清理资源
- synthesisQueue.async {
- // 释放合成器
- self.synthesizer = nil
-
- // 释放配置
- self.speechConfig = nil
-
- // 清空流缓冲区
- self.streamBuffer = ""
-
- // 重置状态
- self.isInitialized = false
- self.speaking = false
-
- // 清空任务队列
- self.taskLock.lock()
- self.pendingTasks.removeAll()
- self.taskLock.unlock()
- }
- }
-
- /**
- * 添加TTS事件监听器
- *
- * @param listener 事件监听器
- */
- public func addListener(_ listener: TtsEventListener) {
- eventListeners.add(listener as AnyObject)
- }
-
- /**
- * 移除TTS事件监听器
- *
- * @param listener 要移除的事件监听器
- */
- public func removeListener(_ listener: TtsEventListener) {
- eventListeners.remove(listener as AnyObject)
- }
-
- /**
- * 添加音频数据监听器
- * 由于不再支持自定义音频流,此方法实际上不再有效
- *
- * @param listener 音频数据监听器
- */
- public func addAudioDataListener(_ listener: AudioDataListener) {
- os_log("警告:不支持音频数据监听器功能", log: log, type: .info)
- }
-
- /**
- * 移除音频数据监听器
- * 由于不再支持自定义音频流,此方法实际上不再有效
- *
- * @param listener 要移除的音频数据监听器
- */
- public func removeAudioDataListener(_ listener: AudioDataListener) {
- // 不做任何操作
- }
-
- /**
- * 当前是否正在播放/合成
- */
- public func isSpeaking() -> Bool {
- return speaking
- }
-
- // MARK: - 辅助方法
-
- /**
- * 设置事件监听器
- */
- private func setupEventListeners() {
- guard let synthesizer = synthesizer else { return }
-
- // 添加合成开始事件处理器
- synthesizer.addSynthesisStartedEventHandler { [weak self] _, _ in
- guard let self = self else { return }
- self.notifyEvent(eventType: .synthesisStarted)
- }
-
- // 添加合成中事件处理器
- synthesizer.addSynthesizingEventHandler { [weak self] _, _ in
- // 可以在这里处理合成中的事件,目前没有特别操作
- }
-
- // 添加合成完成事件处理器
- synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in
- guard let self = self else { return }
- self.speaking = false
- self.notifyEvent(eventType: .synthesisCompleted)
- }
-
- // 添加合成取消事件处理器
- synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in
- guard let self = self else { return }
-
- self.speaking = false
-
- var params: [String: Any] = [:]
- do {
- let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: e.result)
- if cancellationDetails.reason == SPXCancellationReason.error {
- params["reason"] = String(describing: cancellationDetails.reason.rawValue)
- params["errorDetails"] = cancellationDetails.errorDetails ?? "未知错误"
- }
- } catch {
- params["errorDetails"] = "获取取消详情失败: \(error.localizedDescription)"
- }
- os_log("语音合成取消, %{public}@", log: self.log, type: .info, String(describing: params["errorDetails"] ?? "未知错误"))
-
- self.notifyEvent(eventType: .synthesisCanceled, params: params)
- }
- }
-
- /**
- * 触发事件通知
- */
- private func notifyEvent(eventType: TtsEventType, params: [String: Any] = [:]) {
- let event = TtsEvent(type: eventType, params: params)
-
- // 直接通知事件,由调用方处理线程切换
- for case let listener as TtsEventListener in self.eventListeners.allObjects {
- listener.onEvent(event)
- }
- }
-
- /**
- * 重新创建合成器
- */
- private func recreateSynthesizer() {
- do {
- // 使用默认音频输出配置创建合成器
- synthesizer = try SPXSpeechSynthesizer(speechConfig!)
-
- // 设置事件监听
- setupEventListeners()
- } catch {
- os_log("重新创建合成器失败: %{public}@", log: log, type: .error, error.localizedDescription)
- notifyEvent(eventType: .error, params: [
- "errorCode": "RECREATE_FAILED",
- "errorMessage": "重新创建合成器失败: \(error.localizedDescription)"
- ])
- }
- }
-
- /**
- * 设置语音参数
- */
- private func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
- if !isInitialized { return false }
-
- do {
- currentRate = formatPercentage(rate)
- currentPitch = formatPercentage(pitch)
- currentVolume = "\(min(max(volume, 0), 100))%"
- return true
- } catch {
- os_log("设置语音参数失败: %{public}@", log: log, type: .error, error.localizedDescription)
- notifyEvent(eventType: .error, params: [
- "errorCode": "PARAMS_SET_FAILED",
- "errorMessage": "设置语音参数失败: \(error.localizedDescription)"
- ])
- return false
- }
- }
-
- /**
- * 格式化百分比值
- */
- private func formatPercentage(_ value: Int) -> String {
- return value >= 0 ? "+\(value)%" : "\(value)%"
- }
-
- /**
- * 生成SSML
- */
- private func generateSsml(_ rawText: String) -> String {
- // 1. 定义要静音的符号和表情符号列表
- let symbolsToMute = [
- "#", "*",
- "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
- ]
-
- // 2. 转义 XML 保留字符
- var escapedText = rawText
- .replacingOccurrences(of: "&", with: "&")
- .replacingOccurrences(of: "<", with: "<")
- .replacingOccurrences(of: ">", with: ">")
-
- // 3. 静音处理特殊符号和表情符号
- // 使用空白替换法,直接将符号替换为空字符串
- var processedText = escapedText
- for symbol in symbolsToMute {
- processedText = processedText.replacingOccurrences(of: symbol, with: "")
- }
-
- // 4. 构造简化的SSML文档,减少嵌套层级
- let ssml = """
-
-
-
-
- \(processedText)
-
-
-
-
- """
-
- return ssml
- }
-}
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
index b0f4bcb02..f35a6561e 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
@@ -44,8 +44,12 @@ public class AzureAsrHelper: NSObject {
private var audioSourceType = AudioSourceType.microphone
// 音频处理
- private var externalAudioStream: ExternalAudioPullStream?
+ // private var externalAudioStream: ExternalAudioPullStream?
+ // 音频处理
+ public var audioStream: AudioStream?
+
+ public var recordfile: RecordFile?
/**
* 初始化Azure语音服务
*
@@ -70,7 +74,7 @@ public class AzureAsrHelper: NSObject {
// 释放之前的资源
dispose()
-
+ print("w[w[w]]")
// 保存配置
self.subscriptionKey = subscriptionKey
self.region = region
@@ -91,11 +95,11 @@ public class AzureAsrHelper: NSObject {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
-
+
speechConfig?.setPropertyTo("800", byName: "SpeechServiceConnection_EndSilenceTimeoutMs")
speechConfig?.setPropertyTo("800", byName: "Speech_SegmentationSilenceTimeoutMs")
speechConfig?.setPropertyTo("Time", byName: "Speech_SegmentationStrategy")
-
+
if isAutoDetectLanguage {
// 直接启用语言检测模式
speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode")
@@ -104,29 +108,16 @@ public class AzureAsrHelper: NSObject {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
}
-
+ // 录音文件类
+ recordfile = RecordFile()
// 创建识别器
- return setupRecognizer()
+ return true
} catch {
os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
return false
}
}
- /**
- * 向音频流写入音频数据
- * 仅当音频源设置为external时有效
- *
- * @param data 音频数据字节数组
- */
- public func pushAudioData(data: Data) {
- if audioSourceType != .external {
- return
- }
-
- // 使用拉流模式,将数据推入队列
- externalAudioStream?.pushAudio(data)
- }
/**
* 开始连续语音识别
@@ -136,11 +127,10 @@ public class AzureAsrHelper: NSObject {
* @return 是否成功开始识别
*/
public func startContinuousRecognition(
- callback: ContinuousRecognizeCallback,
audioSourceType: AudioSourceType = .microphone
) -> Bool {
guard speechConfig != nil else {
- callback.onError("语音服务未初始化")
+ //callback.onError("语音服务未初始化")
return false
}
@@ -150,18 +140,19 @@ public class AzureAsrHelper: NSObject {
self.audioSourceType = audioSourceType
- // 重置识别器
- if !setupRecognizer() {
- callback.onError("重置识别器失败")
- return false
- }
do {
- // 设置各种事件监听
- setupEventListeners(callback: callback)
+ guard recognizer != nil else {
+ if !setupRecognizer() {
+ // callback.onError("重置识别器失败")
+ return false
+ }
+ return false
+ }
- // 启动音频处理
- startAudioProcessing()
+ // 启动音频处理
+ audioStream?.startAudioRecord()
+
// 开始连续识别
try recognizer?.startContinuousRecognition()
@@ -171,9 +162,10 @@ public class AzureAsrHelper: NSObject {
return true
} catch {
- stopAudioProcessing()
- _isContinuousRecognitionActive = false
- callback.onError("启动连续识别失败: \(error.localizedDescription)")
+ audioStream?.stopMicrophoneCapture()
+ // stopAudioProcessing()
+ _isContinuousRecognitionActive = false
+ //callback.onError("启动连续识别失败: \(error.localizedDescription)")
return false
}
}
@@ -184,44 +176,42 @@ public class AzureAsrHelper: NSObject {
* @return 是否成功停止
*/
public func stopContinuousRecognition() -> Bool {
+ print("stopContinuousRecognition")
guard speechConfig != nil else {
os_log("语音服务未初始化", log: log, type: .error)
return false
}
-
+ guard let recognizer = recognizer else {
+ os_log("识别器为空,重置状态", log: log, type: .info)
+ return true
+ }
+
if !_isContinuousRecognitionActive {
return true
}
do {
- guard let recognizer = recognizer else {
- os_log("识别器为空,重置状态", log: log, type: .info)
- _isContinuousRecognitionActive = false
- return true
- }
-
+ _isContinuousRecognitionActive = false
+
if audioSourceType == .external {
- pushAudioData(data: Data())
+ //pushAudioData(data: Data())
}
// 停止连续识别
try recognizer.stopContinuousRecognition()
// 停止音频处理
- stopAudioProcessing()
-
+ //stopAudioProcessing()
+ audioStream?.stopMicrophoneCapture()
// 会话结束事件会设置_isContinuousRecognitionActive = false
return true
} catch {
// 强制重置状态
_isContinuousRecognitionActive = false
- os_log("停止连续识别失败: %{public}@", log: log, type: .error, error.localizedDescription)
-
+ os_log("强制停止识别失败", log: log, type: .error)
+
// 停止音频处理
- stopAudioProcessing()
-
- // 尝试强制关闭识别器
- recognizer = nil
+ audioStream?.stopMicrophoneCapture()
return false
}
@@ -239,6 +229,7 @@ public class AzureAsrHelper: NSObject {
*/
public func dispose() {
do {
+ print("释放所有资源:")
// 如果正在进行连续识别,先停止
if _isContinuousRecognitionActive {
// 直接停止,不等待结果
@@ -256,11 +247,11 @@ public class AzureAsrHelper: NSObject {
// 确保状态被重置
_isContinuousRecognitionActive = false
- externalAudioStream = nil
+ //externalAudioStream = nil
} catch {
// 确保状态被重置
_isContinuousRecognitionActive = false
- externalAudioStream = nil
+ //externalAudioStream = nil
audioConfig = nil
recognizer = nil
speechConfig = nil
@@ -274,19 +265,17 @@ public class AzureAsrHelper: NSObject {
*/
private func setupRecognizer() -> Bool {
do {
+ print("设置识别器${_isContinuousRecognitionActive}")
+ // 如果正在进行连续识别,先停止
+ if _isContinuousRecognitionActive {
+ // 直接停止,不等待结果
+ try? recognizer?.stopContinuousRecognition()
+ _isContinuousRecognitionActive = false
+ }
+ setupMicrophoneStream()
// 清理旧的识别器
recognizer = nil
- // 设置音频配置
- switch audioSourceType {
- case .microphone:
- // 使用默认麦克风输入配置
- audioConfig = try SPXAudioConfiguration()
- case .external:
- // 使用拉流方式处理外部音频
- setupExternalAudioStream()
- }
-
// 创建识别器
if isAutoDetectLanguage {
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
@@ -310,32 +299,46 @@ public class AzureAsrHelper: NSObject {
}
}
+
/**
- * 设置外部音频流 - 使用拉流方式
+ * 设置麦克风流 - 使用推流方式
*/
- private func setupExternalAudioStream() {
+ private func setupMicrophoneStream() {
do {
- // 创建外部音频拉流对象
- externalAudioStream = ExternalAudioPullStream()
-
+
+ if (audioStream == nil) {
+ // 创建外部音频拉流对象
+ // 明确指定使用外部音频源(推流模式)
+ audioStream = AudioStream(audioSourceType: audioSourceType)
+ audioStream?.initAudioRecord()
+ }
+
// 创建音频配置
- audioConfig = SPXAudioConfiguration(streamInput: externalAudioStream!.pullStream)
+ if let pushStream = audioStream?.pushAudioStream { // Safely unwrap optional
+ audioConfig = SPXAudioConfiguration(streamInput: pushStream)
+ os_log("设置麦克风流:")
+ }
} catch {
- os_log("设置外部音频流失败: %{public}@", log: log, type: .error, error.localizedDescription)
- externalAudioStream = nil
+ os_log("设置麦克风流失败: %{public}@", log: log, type: .error, error.localizedDescription)
+ audioStream = nil
}
}
/**
* 设置事件监听器
*/
- private func setupEventListeners(callback: ContinuousRecognizeCallback) {
- guard let recognizer = recognizer else { return }
-
+ public func setupEventListeners(callback: ContinuousRecognizeCallback) -> Bool{
+ print("设置ssssss监听器:${speechConfig}")
+
+ // 重设识别器
+ if (!setupRecognizer()) {
+ return false
+ }
+ guard let recognizer = recognizer else { return false}
// 识别中事件
recognizer.addRecognizingEventHandler { [weak self] (sender, event) in
guard let self = self else { return }
-
+ print("识别中事件:")
let result = event.result
let detectedLanguage = self.getDetectedLanguage(from: result)
// os_log("识别中: %{public}@", log: self.log, type: .info, result.text ?? "")
@@ -346,7 +349,7 @@ public class AzureAsrHelper: NSObject {
// 识别完成事件
recognizer.addRecognizedEventHandler { [weak self] (sender, event) in
guard let self = self else { return }
-
+ print("识别完成事件:")
let result = event.result
if result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: result)
@@ -359,22 +362,23 @@ public class AzureAsrHelper: NSObject {
recognizer.addSessionStartedEventHandler { (sender, event) in
// 直接在当前线程调用回调
callback.onSessionStarted()
+ print("会话开始事件:")
}
// 会话结束事件
recognizer.addSessionStoppedEventHandler { [weak self] (sender, event) in
guard let self = self else { return }
-
+ print("会话结束事件:")
// 直接在当前线程调用回调
- callback.onSessionStopped()
- self._isContinuousRecognitionActive = false
- self.stopAudioProcessing()
+ // callback.onSessionStopped()
+ // self._isContinuousRecognitionActive = false
+ //self.stopAudioProcessing()
}
// 取消事件
recognizer.addCanceledEventHandler { [weak self] (sender, event) in
guard let self = self else { return }
-
+ print("取消事件:")
let errorDetails = event.errorDetails ?? "未知错误"
let reason = String(describing: event.reason.rawValue)
@@ -382,6 +386,7 @@ public class AzureAsrHelper: NSObject {
callback.onCanceled(reason, errorDetails)
}
+ return true
}
/**
@@ -407,32 +412,273 @@ public class AzureAsrHelper: NSObject {
}
}
+
/**
- * 启动音频处理
+ * 停止音频处理
*/
- private func startAudioProcessing() {
- switch audioSourceType {
- case .microphone:
- // 拉流模式不需要额外启动,SDK会自动拉取数据
- break
- case .external:
- // 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData
- break
- }
+ private func stopAudioProcessing() {
+print("stopAudioProcessing")
+ // if let stream = externalAudioStream {
+ // stream.close()
+ // externalAudioStream = nil
+ // // os_log("外部音频流已关闭", log: log, type: .info)
+ // }
}
-
+ //录音音频文件
/**
- * 停止音频处理
+ * 开启录音
*/
- private func stopAudioProcessing() {
- if let stream = externalAudioStream {
- stream.close()
- externalAudioStream = nil
- // os_log("外部音频流已关闭", log: log, type: .info)
+ public func enableRecord(filePath: String) {
+
+
+ recordfile?.closeFile(isSave: true)
+ recordfile?.creatingFiles(atPath: filePath) // Fixed method call
+
+ }
+
+ /**
+ * 移动文件到新路径
+ */
+ public func moveFile(sourcePath: String, destPath: String)-> Bool {
+
+
+ recordfile?.moveFile(from: sourcePath, to: destPath) // Fixed method call
+ return true
+ }
+
+
+ /**
+ * 重命名指定路径的音频文件
+ */
+ public func renameFile(filePath: String, newName: String)-> Bool {
+
+
+
+ recordfile?.renameFile(at: filePath, to: newName) // Fixed method call
+ return true
+ }
+
+ /**
+ * 停止录音
+ */
+ public func pauseRecord() {
+
+
+ recordfile?.isPause = true
+ }
+
+
+ /**
+ * 关闭录音
+ */
+ public func stopRecord(isSave: Bool) {
+
+
+ recordfile?.isPause = false
+ recordfile?.closeFile(isSave: true)
+ }
+
+ public class AudioStream: NSObject {
+ public private(set) var pushAudioStream: SPXPushAudioInputStream?
+ private let writeQueue = LinkedBlockingQueue()
+ private var audioEngine: AVAudioEngine?
+ private var audioFormat: AVAudioFormat?
+ private let audioSourceType: AudioSourceType // 添加引用
+ private var isRunning = false
+ private var isWriting = false
+ private let bufferSize: Int = 4096
+ private let writeThread = DispatchQueue(label: "audio.stream.writer")
+ init(audioSourceType: AudioSourceType) {
+ self.audioSourceType = audioSourceType
+ }
+ public func initAudioRecord() {
+ audioFormat = getOptimalAudioFormat()
+ pushAudioStream = SPXPushAudioInputStream()
+ isRunning=true
+ startWriteThread()
+ }
+ /// 获取最佳音频格式 (iOS 通常支持标准采样率)
+ public func getOptimalAudioFormat() -> AVAudioFormat? {
+ let sampleRate: Double = 16000 // iOS 通常支持 16kHz
+ return AVAudioFormat(
+ commonFormat: .pcmFormatInt16,
+ sampleRate: sampleRate,
+ channels: 1,
+ interleaved: true
+ )
+ }
+ public func startAudioRecord() {
+ isWriting=true
+ print("startAudioRecord=audioSourceType\(audioSourceType)")
+ switch audioSourceType {
+ case .microphone:
+ startMicrophoneCapture()
+ case .external:
+ startExternalCapture()
+ }
+ }
+
+ public func startWriteThread() {
+ writeThread.async { [weak self] in
+ guard let self = self else { return }
+
+ while self.isRunning {
+ if !self.isWriting {
+ usleep(10_000)
+ continue
+ }
+
+ guard let dataToWrite = self.writeQueue.take() else { continue }
+
+ print("写入数据长度: \(dataToWrite.count)")
+ self.pushAudioStream?.write(dataToWrite)
}
}
-
+}
+
+ /**
+ * 向音频流写入音频数据
+ * 仅当音频源设置为external时有效
+ *
+ * @param data 音频数据字节数组
+ */
+ public func saveAudioDataTo(data: Data) {
+ if audioSourceType != .external {
+ return
+ }
+
+ // 放入队列,由写线程写入
+ writeQueue.put(data)
+ }
+ private func startMicrophoneCapture() {
+ do {
+ let audioSession = AVAudioSession.sharedInstance()
+ // 改用 playAndRecord 模式(同时支持播放和录音)
+ try audioSession.setCategory(
+ .playAndRecord,
+ mode: .default,
+ options: [.allowBluetooth, .defaultToSpeaker]
+ )
+ try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
+
+ audioEngine = AVAudioEngine()
+ guard let inputNode = audioEngine?.inputNode else {
+ throw NSError(domain: "AudioSetup", code: 1)
+ }
+
+ // Use hardware's native format
+ let hardwareFormat = inputNode.inputFormat(forBus: 0)
+
+ // Create converter to target format
+ guard let targetFormat = audioFormat,
+ let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else {
+ throw NSError(domain: "AudioSetup", code: 2)
+ }
+
+ inputNode.installTap(onBus: 0, bufferSize: UInt32(bufferSize), format: hardwareFormat) {
+ [weak self] buffer, time in
+ guard let self = self, self.isWriting else { return }
+
+ // Convert to target format
+ let convertedBuffer = AVAudioPCMBuffer(
+ pcmFormat: targetFormat,
+ frameCapacity: AVAudioFrameCount(targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate)
+)!
+///print("进入 tap 回调,frameLength: \(buffer.frameLength)")
+ var error: NSError?
+ let status = converter.convert(
+ to: convertedBuffer,
+ error: &error,
+ withInputFrom: { inNumPackets, outStatus in
+ outStatus.pointee = .haveData
+ return buffer
+ }
+ )
+ //print("转换状态: \(status.rawValue), 错误: \(String(describing: error))")
+
+ if status == .haveData, error == nil {
+ let data = self.audioBufferToData(convertedBuffer)
+ // print("准备写入数据,大小: \(data.count)")
+
+ self.writeQueue.put(data)
+
+ }
+ }
+
+ try audioEngine?.start()
+ } catch {
+ print("麦克风启动失败: \(error)")
+ }
+}
+
+ private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data {
+ let frameLength = Int(buffer.frameLength)
+ let channelCount = 1
+ let dataLength = frameLength * channelCount * MemoryLayout.size
+
+ // Handle 16-bit integer format
+ if let int16Data = buffer.int16ChannelData {
+ return Data(
+ bytes: int16Data.pointee,
+ count: dataLength
+ )
+ }
+ // Handle float format
+ else if let floatData = buffer.floatChannelData {
+ var int16Array = [Int16](repeating: 0, count: frameLength)
+ let floatBuffer = floatData.pointee
+
+ for i in 0.. Bool {
+ public func renameFile(at sourcePath: String, to newName: String) -> Bool {
let sourceURL = URL(fileURLWithPath: sourcePath)
guard FileManager.default.fileExists(atPath: sourcePath) else {
@@ -162,7 +162,7 @@ class RecordFile {
}
// 移动文件
- func moveFile(from sourcePath: String, to destPath: String) -> Bool {
+ public func moveFile(from sourcePath: String, to destPath: String) -> Bool {
let sourceURL = URL(fileURLWithPath: sourcePath)
let destURL = URL(fileURLWithPath: destPath)
@@ -192,7 +192,7 @@ class RecordFile {
}
// 关闭文件
- func closeFile(isSave: Bool) -> Bool {
+ public func closeFile(isSave: Bool) -> Bool {
defer {
fileHandle = nil
currentAudioFile = nil
From 241535a2175e987b3ecb27585b41252210ff7c66 Mon Sep 17 00:00:00 2001
From: liwei1dao
Date: Wed, 16 Jul 2025 12:01:07 +0800
Subject: [PATCH 09/20] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/data/services/ble_manager.dart | 14 +-
.../controllers/pairing_controller.dart | 170 ++++++++++---
.../Sources/ble_service/BleService.swift | 240 +++++++++++++-----
.../ble_service/SwiftBleServicePlugin.swift | 14 +-
.../ble_service/lib/ble_service.dart | 38 ++-
5 files changed, 368 insertions(+), 108 deletions(-)
diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart
index 30c247463..329604300 100644
--- a/lib/data/services/ble_manager.dart
+++ b/lib/data/services/ble_manager.dart
@@ -782,9 +782,19 @@ class BleManager extends GetxService {
}
}
- Future