wolfplus 1 year ago
parent
commit
4d473c55c3
  1. 2
      ios/Podfile
  2. 14
      ios/Podfile.lock
  3. 120
      ios/Runner.xcodeproj/project.pbxproj
  4. 804
      ios/Runner.xcodeproj/project.pbxproj.backup
  5. 15
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  6. 3
      lib/modules/home/views/home_view.dart
  7. 0
      local_plugins/agent_service/LICENSE
  8. 994
      local_plugins/agent_service/ios/Classes/AgentService.swift
  9. 797
      local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift
  10. 11
      local_plugins/agent_service/ios/Classes/AgentServiceListener.swift
  11. 2
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.m
  12. 227
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift
  13. 259
      local_plugins/agent_service/ios/Classes/SwiftAgentServicePlugin.swift
  14. 25
      local_plugins/agent_service/ios/agent_service.podspec
  15. 15
      local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h
  16. 12
      local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.m
  17. 12
      local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift
  18. 6
      local_plugins/azure_speech/ios/Classes/azure_speech-Bridging-Header.h
  19. 5
      local_plugins/azure_speech/ios/Classes/azure_speech.h
  20. 8
      local_plugins/azure_speech/ios/azure_speech.podspec
  21. 684
      local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift
  22. 650
      local_plugins/ble_service/ios/Classes/BleService.swift
  23. 128
      local_plugins/ble_service/ios/Classes/OpusAudioProcessor.swift
  24. 138
      local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift
  25. 92
      local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/OpusUnit.h
  26. 20
      local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexKit.h
  27. 40
      local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexUnit.h
  28. BIN
      local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Info.plist
  29. 6
      local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Modules/module.modulemap
  30. BIN
      local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/SpeexKit
  31. 8
      local_plugins/ble_service/ios/ble_service.podspec
  32. 58
      local_plugins/ble_service/pubspec.yaml

2
ios/Podfile

@ -29,7 +29,7 @@ flutter_ios_podfile_setup
target 'Runner' do target 'Runner' do
use_frameworks! use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do target 'RunnerTests' do
inherit! :search_paths inherit! :search_paths

14
ios/Podfile.lock

@ -77,7 +77,7 @@ PODS:
- firebase_core (3.13.0): - firebase_core (3.13.0):
- Firebase/CoreOnly (= 11.10.0) - Firebase/CoreOnly (= 11.10.0)
- Flutter - Flutter
- FirebaseAppCheckInterop (11.12.0) - FirebaseAppCheckInterop (11.13.0)
- FirebaseAuth (11.10.0): - FirebaseAuth (11.10.0):
- FirebaseAppCheckInterop (~> 11.0) - FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0) - FirebaseAuthInterop (~> 11.0)
@ -87,7 +87,7 @@ PODS:
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GTMSessionFetcher/Core (< 5.0, >= 3.4) - GTMSessionFetcher/Core (< 5.0, >= 3.4)
- RecaptchaInterop (~> 101.0) - RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (11.12.0) - FirebaseAuthInterop (11.13.0)
- FirebaseCore (11.10.0): - FirebaseCore (11.10.0):
- FirebaseCoreInternal (~> 11.10.0) - FirebaseCoreInternal (~> 11.10.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
@ -380,8 +380,8 @@ SPEC CHECKSUMS:
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
azure_speech: d65fa5c1f04be9cee7303abf4caea30680e814fd azure_speech: 11927e53ab90a2def1370da1d44e4915a97c0290
ble_service: 7987f876505b5c2429303597d7604092d2545566 ble_service: 246764cd69adead824a6596fc2fa9e103dc1d2a4
chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8 chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8
classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
@ -395,9 +395,9 @@ SPEC CHECKSUMS:
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
firebase_auth: 34c515a8fa76a4ef678ac5ad5cfa3bb3120f5ab5 firebase_auth: 34c515a8fa76a4ef678ac5ad5cfa3bb3120f5ab5
firebase_core: 2d4534e7b489907dcede540c835b48981d890943 firebase_core: 2d4534e7b489907dcede540c835b48981d890943
FirebaseAppCheckInterop: 73b173e5ec45192e2d522ad43f526a82ad10b852 FirebaseAppCheckInterop: 72066489c209823649a997132bcd9269bc33a4bb
FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32 FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32
FirebaseAuthInterop: b583210c039a60ed3f1e48865e1f3da44a796595 FirebaseAuthInterop: 4fa327ec3c551a80a6929561f83af80b1dd44937
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
@ -442,6 +442,6 @@ SPEC CHECKSUMS:
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
PODFILE CHECKSUM: c27b71cd46016bc2643e657c88b59244c316e68a PODFILE CHECKSUM: d5ff9fcce4b2bc91659dc6d34600a8300e453909
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

120
ios/Runner.xcodeproj/project.pbxproj

@ -7,16 +7,18 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
094D08D02FDF5BCF4300904C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89BDD8FCD8E4A95DFFAC950E /* Pods_Runner.framework */; }; 0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C3762DDE122500FD4F41 /* Opus */; };
0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C37B2DDE142500FD4F41 /* Opus */; };
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; }; 0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
8C1EC4B8888A2A7B8741B084 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 971EED197AE19E5EA8C93AB7 /* Pods_RunnerTests.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */; };
B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -47,16 +49,17 @@
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; }; 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4AC518688D83E399D06C65D9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; 3CFCAD0427BE38C23B922B35 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
89BDD8FCD8E4A95DFFAC950E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
8AEFEF9B7F1F2853E3BE4328 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
971EED197AE19E5EA8C93AB7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -64,11 +67,10 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AE1151546CDAD222B8569F48 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
CD485A3D7C665ACD79DE1A05 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; }; D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
D45B01309FCD51D40429D22A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; }; E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
D6F4721705FE7CF4B2C9A7C7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -76,7 +78,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
094D08D02FDF5BCF4300904C /* Pods_Runner.framework in Frameworks */, B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -84,7 +86,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
8C1EC4B8888A2A7B8741B084 /* Pods_RunnerTests.framework in Frameworks */, B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -94,12 +96,12 @@
1ACB686EC7DC36C6E33D372F /* Pods */ = { 1ACB686EC7DC36C6E33D372F /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4AC518688D83E399D06C65D9 /* Pods-Runner.debug.xcconfig */, 3CFCAD0427BE38C23B922B35 /* Pods-Runner.debug.xcconfig */,
AE1151546CDAD222B8569F48 /* Pods-Runner.release.xcconfig */, 85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */,
D6F4721705FE7CF4B2C9A7C7 /* Pods-Runner.profile.xcconfig */, 1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */,
8AEFEF9B7F1F2853E3BE4328 /* Pods-RunnerTests.debug.xcconfig */, 209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */,
CD485A3D7C665ACD79DE1A05 /* Pods-RunnerTests.release.xcconfig */, E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */,
D45B01309FCD51D40429D22A /* Pods-RunnerTests.profile.xcconfig */, FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@ -108,8 +110,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */, 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */,
89BDD8FCD8E4A95DFFAC950E /* Pods_Runner.framework */, 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */,
971EED197AE19E5EA8C93AB7 /* Pods_RunnerTests.framework */, F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -178,7 +180,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = ( buildPhases = (
4555DFF031A57477C09E2572 /* [CP] Check Pods Manifest.lock */, B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */, 331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */, 331C807F294A63A400263BE5 /* Resources */,
B3515CD9628E3332AFC88CD4 /* Frameworks */, B3515CD9628E3332AFC88CD4 /* Frameworks */,
@ -197,15 +199,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
1A23E53C090955A193472A0D /* [CP] Check Pods Manifest.lock */, B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
4F94027D1F57DF2EE72F6FDE /* [CP] Embed Pods Frameworks */, 1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */,
56059DFFB76EF409A9B11842 /* [CP] Copy Pods Resources */, C482126191919ADDA94A189A /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -278,26 +280,21 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
1A23E53C090955A193472A0D /* [CP] Check Pods Manifest.lock */ = { 1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = ( name = "[CP] Embed Pods Frameworks";
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = ( outputFileListPaths = (
); "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
@ -316,7 +313,22 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
4555DFF031A57477C09E2572 /* [CP] Check Pods Manifest.lock */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -338,24 +350,29 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
4F94027D1F57DF2EE72F6FDE /* [CP] Embed Pods Frameworks */ = { B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Embed Pods Frameworks"; inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", );
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
56059DFFB76EF409A9B11842 /* [CP] Copy Pods Resources */ = { C482126191919ADDA94A189A /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -372,21 +389,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@ -516,7 +518,7 @@
}; };
331C8088294A63A400263BE5 /* Debug */ = { 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 8AEFEF9B7F1F2853E3BE4328 /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = 209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -535,7 +537,7 @@
}; };
331C8089294A63A400263BE5 /* Release */ = { 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = CD485A3D7C665ACD79DE1A05 /* Pods-RunnerTests.release.xcconfig */; baseConfigurationReference = E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -552,7 +554,7 @@
}; };
331C808A294A63A400263BE5 /* Profile */ = { 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = D45B01309FCD51D40429D22A /* Pods-RunnerTests.profile.xcconfig */; baseConfigurationReference = FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;

804
ios/Runner.xcodeproj/project.pbxproj.backup

@ -0,0 +1,804 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C3762DDE122500FD4F41 /* Opus */; };
0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C37B2DDE142500FD4F41 /* Opus */; };
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */; };
B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3CFCAD0427BE38C23B922B35 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */,
0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */,
B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B3515CD9628E3332AFC88CD4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
1ACB686EC7DC36C6E33D372F /* Pods */ = {
isa = PBXGroup;
children = (
3CFCAD0427BE38C23B922B35 /* Pods-Runner.debug.xcconfig */,
85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */,
1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */,
209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */,
E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */,
FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
1E5DDD8701F6FB27AA502080 /* Frameworks */ = {
isa = PBXGroup;
children = (
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */,
6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */,
F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
1ACB686EC7DC36C6E33D372F /* Pods */,
1E5DDD8701F6FB27AA502080 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
B3515CD9628E3332AFC88CD4 /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */,
C482126191919ADDA94A189A /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
C482126191919ADDA94A189A /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
0AD3C3752DDE122500FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/alta/swift-opus";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.0.2;
};
};
0AD3C37A2DDE139600FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/alta/swift-opus";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.0.2;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
0AD3C3762DDE122500FD4F41 /* Opus */ = {
isa = XCSwiftPackageProductDependency;
package = 0AD3C3752DDE122500FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */;
productName = Opus;
};
0AD3C37B2DDE142500FD4F41 /* Opus */ = {
isa = XCSwiftPackageProductDependency;
package = 0AD3C37A2DDE139600FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */;
productName = Opus;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

15
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@ -0,0 +1,15 @@
{
"originHash" : "bd9b3a14091d57af541f2718cbec3c63a0b618a9a7c0674743c7cf43998a352d",
"pins" : [
{
"identity" : "swift-opus",
"kind" : "remoteSourceControl",
"location" : "https://github.com/alta/swift-opus",
"state" : {
"revision" : "5f0164b8915fb9001265c54413aa0f0efbd3df06",
"version" : "0.0.2"
}
}
],
"version" : 3
}

3
lib/modules/home/views/home_view.dart

@ -226,8 +226,7 @@ class HomeView extends GetView<HomeController> {
// 设备信息 // 设备信息
SizedBox(height: 12.h), SizedBox(height: 12.h),
Obx(() { Obx(() {
print('连接状态: ${controller.connectionStatus}'); // 添加调试打印
print('错误信息: ${controller.connectionErrorMsg}'); // 添加调试打印
// 如果连接失败且有错误信息,显示错误提示 // 如果连接失败且有错误信息,显示错误提示
if (controller.connectionStatus == ConnectionStatus.failed && if (controller.connectionStatus == ConnectionStatus.failed &&
controller.connectionErrorMsg.isNotEmpty) { controller.connectionErrorMsg.isNotEmpty) {

0
local_plugins/agent_service/LICENSE

994
local_plugins/agent_service/ios/Classes/AgentService.swift

@ -1,994 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import open_ai_service
import chat_storage
/// 代理服务 - 处理语音识别、TTS和AI对话相关逻辑
///
/// 负责集成Azure语音服务、OpenAI服务和本地存储服务,
/// 提供语音识别、语音合成、AI对话等功能
class AgentService {
// 常量定义
private let TAG = "AgentService"
// 上下文和监听器
private var listeners = NSHashTable<AnyObject>.weakObjects()
// 配置参数
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
private var openaiApiKey: String = ""
private var openaiBaseUrl: String = ""
private var openaiModel: String = "gpt-3.5-turbo"
private var mcpServer: String = ""
// Azure服务
private var azureAsrHelper: AzureAsrHelper?
private var azureTtsHelper: AzureTtsHelper?
// OpenAI服务
private var openAIService: OpenAIServiceBridge?
// 聊天存储服务
private var chatStorageHelper: ChatStorageHelperBridge?
// 会话ID,用于区分不同聊天上下文
private let sessionId = "default_agent"
// 历史聊天消息缓存
private var historyMessages = [[String: Any]]()
// 系统提示词
private var systemPrompt = ""
// 状态
private var isInitialized = false
private var isRecognitionActive = false
private var isTtsSpeaking = false
private var hasSpeechDetected = false
private var isAiStreaming = false
// AI流生成相关
private var currentAiTask: DispatchWorkItem?
// 空闲检测相关
private var idleCheckTimer: Timer?
private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数
/// 初始化
init() {
// 创建OpenAI服务桥接器
openAIService = OpenAIServiceBridge()
// 创建聊天存储桥接器
chatStorageHelper = ChatStorageHelperBridge()
}
/**
* 初始化系统提示词
*/
private func initSystemPrompt() {
systemPrompt = """
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。
遵循以下指导原则:
1. 保持简短精炼的回答,因为用户是通过语音与你交流
2. 优先使用中文回复,除非用户明确要求使用其他语言
3. 当用户问题不明确时,礼貌地请求更多信息
4. 避免过长的列表,尽量将信息分成小段
5. 不要使用需要视觉展示的元素(如表格、图表或代码块)
6. 记住用户之前的对话内容,保持对话连贯
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。
"""
}
/**
* 初始化
*
* - Parameter config: 配置参数,包含所需的所有API密钥和设置
* - Returns: 是否初始化成功
*/
func initialize(config: [String: Any]) -> Bool {
if isInitialized { return true }
do {
// 从配置中获取参数
if let azureKey = config["azureSpeechKey"] as? String {
azureSpeechKey = azureKey
}
if let azureRegion = config["azureSpeechRegion"] as? String {
azureSpeechRegion = azureRegion
}
if let openaiKey = config["openaiApiKey"] as? String {
openaiApiKey = openaiKey
}
if let baseUrl = config["openaiBaseUrl"] as? String {
openaiBaseUrl = baseUrl
}
if let model = config["openaiModel"] as? String {
openaiModel = model
}
if let server = config["mcpServer"] as? String {
mcpServer = server
}
// 自定义系统提示词
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty {
systemPrompt = customSystemPrompt
} else {
// 使用默认系统提示词
initSystemPrompt()
}
// 检查必要参数
if azureSpeechKey.isEmpty || azureSpeechRegion.isEmpty || openaiApiKey.isEmpty {
print("\(TAG): 初始化失败:关键配置参数缺失")
return false
}
// 初始化OpenAI服务
openAIService?.initialize(apiKey: openaiApiKey, baseUrl: openaiBaseUrl, model: openaiModel, mcpServer: mcpServer)
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(self)
let asrInitResult = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
audioSourceType: .microphone
) ?? false
if !asrInitResult {
print("\(TAG): Azure ASR初始化失败")
return false
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper(self)
let ttsInitResult = azureTtsHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
callback: self
) ?? false
if !ttsInitResult {
print("\(TAG): Azure TTS初始化失败")
return false
}
// 加载最近的聊天记录
loadChatHistory()
isInitialized = true
print("\(TAG): 代理服务初始化成功")
return true
} catch {
print("\(TAG): 初始化失败:\(error.localizedDescription)")
return false
}
}
/**
* 添加事件监听器
*
* - Parameter listener: 要添加的监听器
*/
func addListener(_ listener: AgentServiceListener) {
listeners.add(listener as AnyObject)
}
/**
* 设置事件监听器(替换现有监听器)
*
* - Parameter listener: 要设置的监听器
*/
func setListener(_ listener: AgentServiceListener) {
listeners.removeAllObjects()
listeners.add(listener as AnyObject)
}
/**
* 移除事件监听器
*
* - Parameter listener: 要移除的监听器
*/
func removeListener(_ listener: AgentServiceListener) {
listeners.remove(listener as AnyObject)
}
/**
* 移除所有事件监听器
*/
func clearListeners() {
listeners.removeAllObjects()
}
/**
* 启动空闲检测
*/
private func startIdleCheck() {
stopIdleCheck() // 先停止现有的检查
if !isRecognitionActive { return }
// 创建定时器
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in
guard let self = self else { return }
// 如果状态仍然是空闲的,则停止识别
if self.isRecognitionActive && !self.hasSpeechDetected && !self.isTtsSpeaking && !self.isAiStreaming {
self.stopRecognition()
self.sendEvent("auto_stop", ["reason": "idle_timeout", "seconds": self.maxIdleSeconds])
}
}
}
/**
* 停止空闲检测
*/
private func stopIdleCheck() {
idleCheckTimer?.invalidate()
idleCheckTimer = nil
}
/**
* 重启空闲检测
* 当状态发生变化时调用
*/
private func restartIdleCheck() {
if isRecognitionActive {
startIdleCheck()
}
}
/**
* 开始语音识别
*
* - Returns: 是否成功开始识别
*/
func startRecognition() -> Bool {
if !isInitialized {
print("\(TAG): 服务未初始化")
return false
}
if isRecognitionActive { return true }
isRecognitionActive = true
hasSpeechDetected = false
do {
// 创建连续识别回调
class ContinuousRecognizeCallbackImpl: AzureAsrHelper.ContinuousRecognizeCallback {
private weak var service: AgentService?
init(_ service: AgentService) {
self.service = service
}
func onRecognizing(recognizing: String, detectedLanguage: String) {
guard let service = service else { return }
if !recognizing.isEmpty {
// 检测到语音,更新状态
let previousHasSpeech = service.hasSpeechDetected
service.hasSpeechDetected = true
// 状态发生变化时重启空闲检测
if !previousHasSpeech {
service.restartIdleCheck()
}
service.sendEvent("recognizing", [
"text": recognizing,
"language": detectedLanguage
])
// 如果TTS正在播放或AI正在生成,则触发打断
if service.isTtsSpeaking || service.isAiStreaming {
service.interruptCurrentResponse()
}
}
}
func onResult(text: String, detectedLanguage: String) {
guard let service = service else { return }
if !text.isEmpty {
service.sendEvent("recognition_result", [
"text": text,
"language": detectedLanguage
])
service.processWithOpenAI(text: text)
}
// 重置状态,继续识别
let previousHasSpeech = service.hasSpeechDetected
service.hasSpeechDetected = false
// 状态发生变化时重启空闲检测
if previousHasSpeech {
service.restartIdleCheck()
}
}
func onSessionStarted() {
guard let service = service else { return }
service.sendEvent("recognition_started", ["status": "started"])
// 启动空闲检测
service.startIdleCheck()
}
func onSessionStopped() {
guard let service = service else { return }
service.isRecognitionActive = false
service.stopIdleCheck()
service.sendEvent("recognition_stopped", ["status": "stopped"])
}
func onCanceled(reason: String, errorDetails: String) {
guard let service = service else { return }
service.isRecognitionActive = false
service.stopIdleCheck()
service.sendEvent("recognition_canceled", [
"reason": reason,
"details": errorDetails
])
}
func onError(error: String) {
guard let service = service else { return }
service.isRecognitionActive = false
service.stopIdleCheck()
print("\(service.TAG): 语音识别出错: \(error)")
service.sendEvent("error", [
"code": "RECOGNITION_ERROR",
"message": error
])
}
}
let callback = ContinuousRecognizeCallbackImpl(self)
let result = azureAsrHelper?.startContinuousRecognition(callback) ?? false
if !result {
isRecognitionActive = false
print("\(TAG): 启动语音识别失败")
sendEvent("error", [
"code": "RECOGNITION_START_ERROR",
"message": "启动语音识别失败"
])
}
return result
} catch {
isRecognitionActive = false
print("\(TAG): 启动语音识别失败: \(error.localizedDescription)")
sendEvent("error", [
"code": "RECOGNITION_START_ERROR",
"message": error.localizedDescription
])
return false
}
}
/**
* 停止语音识别
*/
func stopRecognition() {
if !isRecognitionActive { return }
print("\(TAG): 停止语音识别")
// 停止识别
let _ = azureAsrHelper?.stopContinuousRecognition()
isRecognitionActive = false
stopIdleCheck()
print("\(TAG): 语音识别已停止")
}
/**
* 打断当前响应
* 停止TTS播放和AI流输出
*/
func interruptCurrentResponse() {
if isAiStreaming || isTtsSpeaking {
// 停止TTS播放
stopTts()
// 停止AI流输出
stopAiStream()
// 发送打断事件
sendEvent("response_interrupted", ["status": "interrupted"])
}
}
/**
* 停止AI流输出
*/
private func stopAiStream() {
if isAiStreaming {
// 取消当前AI生成任务
currentAiTask?.cancel()
currentAiTask = nil
// 通知OpenAI服务终止当前流式请求
openAIService?.cancelCurrentStream()
// 更新状态
isAiStreaming = false
// 记录日志
print("\(TAG): AI流输出已停止")
}
}
/**
* 处理文本输入
* 作为语音输入的补充,直接处理文本并通过事件返回结果
*
* - Parameters:
* - text: 用户输入文本
* - speakResponse: 是否朗读回复,默认为false
* - Returns: 是否成功开始处理
*/
func processTextInput(text: String, speakResponse: Bool = false) -> Bool {
if !isInitialized {
print("\(TAG): 服务未初始化")
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"])
return false
}
if text.isEmpty {
print("\(TAG): 文本输入不能为空")
sendEvent("error", ["code": "EMPTY_TEXT", "message": "文本输入不能为空"])
return false
}
// 使用OpenAI处理文本
processWithOpenAI(text: text, speakResponse: speakResponse)
return true
}
/**
* 使用OpenAI处理语音识别结果
*
* - Parameter text: 用户输入文本
*/
private func processWithOpenAI(text: String) {
processWithOpenAI(text: text, speakResponse: true)
}
/**
* 使用OpenAI处理文本消息
*
* - Parameters:
* - text: 用户输入文本
* - speakResponse: 是否使用TTS朗读回复
*/
private func processWithOpenAI(text: String, speakResponse: Bool = true) {
print("\(TAG): 用户问题: \(text)")
// 创建用户文本消息并处理
guard let userMessage = openAIService?.createUserMessage(text: text) else {
print("\(TAG): 创建用户消息失败")
return
}
processWithOpenAIInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse)
}
/**
* 使用OpenAI处理图片
*
* - Parameters:
* - imagePath: 图片文件路径
* - text: 可选的文本描述或问题
* - speakResponse: 是否朗读回复
* - Returns: 是否成功开始处理
*/
func processImageInput(imagePath: String, text: String = "", speakResponse: Bool = false) -> Bool {
if !isInitialized {
print("\(TAG): 服务未初始化")
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"])
return false
}
if imagePath.isEmpty {
print("\(TAG): 图片路径不能为空")
sendEvent("error", ["code": "EMPTY_IMAGE_PATH", "message": "图片路径不能为空"])
return false
}
// 通知开始处理图片
sendEvent("image_processing", [
"status": "processing",
"imagePath": imagePath
])
// 异步处理图片
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
// 将图片转换为Base64格式
guard let imageBase64 = self.openAIService?.fileToBase64(filePath: imagePath) else {
DispatchQueue.main.async {
print("\(self.TAG): 图片转换失败: \(imagePath)")
self.sendEvent("error", [
"code": "IMAGE_CONVERSION_FAILED",
"message": "图片转换失败"
])
}
return
}
// 通知图片准备完成
DispatchQueue.main.async {
self.sendEvent("image_ready", [
"status": "ready",
"imagePath": imagePath
])
// 处理包含图片的消息
self.processImageWithOpenAI(imageBase64: imageBase64, text: text, speakResponse: speakResponse)
}
}
return true
}
/**
* 使用OpenAI处理图片
*
* - Parameters:
* - imageBase64: Base64编码的图片数据
* - text: 可选的文本描述或问题
* - speakResponse: 是否朗读回复
*/
private func processImageWithOpenAI(imageBase64: String, text: String = "", speakResponse: Bool = false) {
print("\(TAG): 处理图片输入: \(text.isEmpty ? "无附加文本" : "附带文本: \(text)")")
// 创建带图片的用户消息并处理
guard let userMessage = openAIService?.createUserMessageWithImage(text: text, imageBase64: imageBase64) else {
print("\(TAG): 创建带图片的用户消息失败")
return
}
// 图片描述用于存储
let displayText = text.isEmpty ? "[图片]" : text
processWithOpenAIInternal(userMessage: userMessage, displayText: displayText, speakResponse: speakResponse, hasImage: true)
}
/**
* 内部方法:通用的OpenAI处理逻辑
*
* - Parameters:
* - userMessage: 用户消息(可以是文本或图片格式)
* - displayText: 用于显示和存储的文本
* - speakResponse: 是否朗读回复
* - hasImage: 是否包含图片
*/
private func processWithOpenAIInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) {
// 如果有正在进行的AI流式输出,先停止它
stopAiStream()
// 创建AI任务
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
// 设置状态为正在流式输出
self.isAiStreaming = true
// 使用历史记录作为上下文发送到OpenAI
var responseBuilder = ""
// 添加系统提示词到历史消息的副本中
var messagesWithSystemPrompt: [[String: Any]] = []
// 先添加系统提示词
if !self.systemPrompt.isEmpty {
if let systemMessage = self.openAIService?.createSystemMessage(text: self.systemPrompt) {
messagesWithSystemPrompt.append(systemMessage)
}
}
// 再添加历史消息
messagesWithSystemPrompt.append(contentsOf: self.historyMessages)
messagesWithSystemPrompt.append(userMessage)
// 将用户消息添加到历史记录
self.addToHistoryMessages(userMessage)
// 流式回调
class StreamCallbackBridge: NSObject, OpenAIStreamCallback {
private weak var service: AgentService?
private var responseBuilder: String
private let speakResponse: Bool
private let displayText: String
private let hasImage: Bool
init(_ service: AgentService, responseBuilder: String = "", speakResponse: Bool, displayText: String, hasImage: Bool) {
self.service = service
self.responseBuilder = responseBuilder
self.speakResponse = speakResponse
self.displayText = displayText
self.hasImage = hasImage
super.init()
}
func onToken(token: String) {
guard let service = service else { return }
responseBuilder.append(token)
if speakResponse {
_ = service.azureTtsHelper?.speakStream(token)
}
// 发送流式回复token
service.sendEvent("assistant_token", ["token": token])
}
func onComplete() {
guard let service = service else { return }
// 视情况决定是否朗读回复
if speakResponse {
_ = service.azureTtsHelper?.flushStream()
}
if !responseBuilder.isEmpty {
// 发送完整回复,包含是否有图片的标记
var responseData: [String: Any] = [
"text": responseBuilder,
"userInput": displayText
]
if hasImage {
responseData["hasImage"] = true
}
service.sendEvent("assistant_response", responseData)
// 添加AI回复到历史记录
if let assistantMessage = service.openAIService?.createAssistantMessage(text: responseBuilder) {
service.addToHistoryMessages(assistantMessage)
}
// 保存聊天记录
service.saveChatMessage(userMessage: displayText, assistantMessage: responseBuilder)
}
// 标记AI流式输出已完成
service.isAiStreaming = false
service.currentAiTask = nil
}
func onError(error: Error) {
guard let service = service else { return }
print("\(service.TAG): AI处理出错: \(error.localizedDescription)")
service.sendEvent("error", [
"code": "AI_ERROR",
"message": error.localizedDescription
])
// 标记AI流式输出已完成
service.isAiStreaming = false
service.currentAiTask = nil
}
func onFunctionCall(call: [String: Any]) {
guard let service = service else { return }
if let name = call["name"] as? String {
service.sendEvent("function_call", [
"name": name,
"arguments": call
])
if name == "exit_interaction" {
service.stopRecognition()
}
}
}
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) {
guard let service = service else { return }
service.sendEvent("function_call_result", [
"function_call": functionCall,
"result": functionCallResult,
])
}
}
let callback = StreamCallbackBridge(self, responseBuilder: responseBuilder, speakResponse: speakResponse, displayText: displayText, hasImage: hasImage)
self.openAIService?.sendMessageStream(messages: messagesWithSystemPrompt, callback: callback)
}
// 保存任务引用并在全局队列中执行
currentAiTask = workItem
DispatchQueue.global(qos: .userInitiated).async(execute: workItem)
}
/**
* 加载最近的聊天历史记录
*/
private func loadChatHistory() {
// 清空当前历史记录
historyMessages.removeAll()
// 使用ChatStorageHelper直接获取最近消息
guard let recentMessages = chatStorageHelper?.getRecentMessages(sessionId: sessionId, count: 10) else {
print("\(TAG): 没有找到历史记录")
return
}
// 将消息添加到历史记录
for message in recentMessages {
if let sender = message["sender"] as? String,
let content = message["message"] as? String {
if sender == "user" {
if let userMessage = openAIService?.createUserMessage(text: content) {
addToHistoryMessages(userMessage)
}
} else if sender == "assistant" {
if let assistantMessage = openAIService?.createAssistantMessage(text: content) {
addToHistoryMessages(assistantMessage)
}
}
}
}
print("\(TAG): 已加载\(recentMessages.count)条历史记录")
}
/**
* 添加消息到历史记录,保持最近10条
*/
private func addToHistoryMessages(_ message: [String: Any]) {
// 添加新消息
historyMessages.append(message)
// 如果超过10条,删除最早的消息
while historyMessages.count > 10 {
historyMessages.removeFirst()
}
}
/**
* TTS播放函数
*
* - Parameter text: 要播放的文本
*/
func speakText(text: String) {
if text.isEmpty { return }
// 更新状态
isTtsSpeaking = true
restartIdleCheck() // 状态变化,重启检测
// 播放文本
_ = azureTtsHelper?.speakText(text)
}
/**
* 停止TTS播放
*/
func stopTts() {
if isTtsSpeaking {
_ = azureTtsHelper?.stopSpeaking()
isTtsSpeaking = false
restartIdleCheck() // 状态变化,重启检测
sendEvent("tts_stopped", ["status": "stopped"])
}
}
/**
* 保存聊天消息
*/
private func saveChatMessage(userMessage: String, assistantMessage: String) {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else { return }
// 保存用户消息
let userMessageId = self.chatStorageHelper?.saveMessage(
sessionId: self.sessionId,
message: userMessage,
sender: "user"
) ?? -1
if userMessageId != -1 {
// 保存AI回复
let assistantMessageId = self.chatStorageHelper?.saveMessage(
sessionId: self.sessionId,
message: assistantMessage,
sender: "assistant"
) ?? -1
if assistantMessageId == -1 {
print("\(self.TAG): 保存助手消息失败")
}
} else {
print("\(self.TAG): 保存用户消息失败")
}
}
}
/**
* 清除聊天历史
*/
func clearChatHistory(completion: @escaping (Bool) -> Void) {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else {
DispatchQueue.main.async {
completion(false)
}
return
}
// 清除指定会话的所有消息
let success = self.chatStorageHelper?.deleteMessages(sessionId: self.sessionId) ?? false
if success {
// 清空内存中的历史记录
self.historyMessages.removeAll()
print("\(self.TAG): 聊天历史已清除")
} else {
print("\(self.TAG): 清除聊天历史失败")
}
DispatchQueue.main.async {
completion(success)
}
}
}
/**
* 发送事件
*/
private func sendEvent(_ eventName: String, _ data: [String: Any]) {
// 向所有监听器发送事件
let allListeners = listeners.allObjects
for case let listener as AgentServiceListener in allListeners {
listener.onEvent(eventName: eventName, data: data)
}
}
/**
* 释放资源
*/
func dispose() {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
// 清除所有监听器
clearListeners()
// 重置单例状态,以便下次使用时可以重新初始化
isInitialized = false
print("\(TAG): 代理服务资源已释放")
}
}
// MARK: - AzureTtsHelper.TtsCallback
extension AgentService: AzureTtsHelper.TtsCallback {
func onSynthesisStarted() {
isTtsSpeaking = true
// 状态变化,重置空闲检测
restartIdleCheck()
sendEvent("tts_started", ["status": "started"])
}
func onSynthesizing() {
// 可以在此添加TTS合成中的处理逻辑
}
func onSynthesisCompleted() {
isTtsSpeaking = false
// 状态变化,重启空闲检测
restartIdleCheck()
sendEvent("tts_completed", ["status": "completed"])
}
func onSynthesisCanceled() {
isTtsSpeaking = false
// 状态变化,重启空闲检测
restartIdleCheck()
sendEvent("tts_canceled", ["status": "canceled"])
}
}
// MARK: - 外部插件桥接器
/// 桥接OpenAIService插件
class OpenAIServiceBridge {
private let service = OpenAIService()
/// 初始化服务
func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) {
service.initialize(apiKey: apiKey, baseUrl: baseUrl, model: model, mcpServer: mcpServer)
}
/// 创建系统消息
func createSystemMessage(text: String) -> [String: Any] {
return service.createSystemMessage(text)
}
/// 创建用户消息
func createUserMessage(text: String) -> [String: Any] {
return service.createUserMessage(text)
}
/// 创建助手消息
func createAssistantMessage(text: String) -> [String: Any] {
return service.createAssistantMessage(text)
}
/// 创建带图片的用户消息
func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any]? {
return service.createUserMessageWithImage(text: text, imageBase64: imageBase64)
}
/// 文件转Base64
func fileToBase64(filePath: String) -> String? {
return service.fileToBase64(filePath: filePath)
}
/// 发送流式消息
func sendMessageStream(messages: [[String: Any]], callback: OpenAIStreamCallback) {
service.sendMessageStream(messages: messages, callback: callback)
}
/// 取消当前流
func cancelCurrentStream() {
service.cancelCurrentStream()
}
}
/// OpenAI流式回调协议
@objc protocol OpenAIStreamCallback {
func onToken(token: String)
func onComplete()
func onError(error: Error)
func onFunctionCall(call: [String: Any])
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any])
}
/// 桥接ChatStorageHelper插件
class ChatStorageHelperBridge {
private let helper = ChatStorageHelper()
/// 保存消息
func saveMessage(sessionId: String, message: String, sender: String) -> Int64 {
return helper.saveMessage(sessionId: sessionId, message: message, sender: sender)
}
/// 获取最近消息
func getRecentMessages(sessionId: String, count: Int) -> [[String: Any]]? {
return helper.getRecentMessages(sessionId: sessionId, count: count)
}
/// 删除消息
func deleteMessages(sessionId: String) -> Bool {
return helper.deleteMessages(sessionId: sessionId)
}
}

797
local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift

@ -0,0 +1,797 @@
import Foundation
import AVFoundation
import ble_service
import speech
/// 代理服务事件监听器接口
protocol AgentServiceListener {
/// 当事件发生时调用
/// - Parameters:
/// - eventName: 事件名称
/// - data: 事件数据
func onEvent(eventName: String, data: [String: Any])
}
/// 代理服务实现类,负责语音识别、OpenAI对话和语音合成
class AgentServiceImpl: NSObject {
// MARK: - 常量
private let TAG = "AgentServiceImpl"
// MARK: - 单例实现
static let shared = AgentServiceImpl()
// MARK: - 属性
// 监听器集合
private var listeners = Set<AgentServiceListener>()
private let listenersLock = NSLock()
// Azure 语音相关
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
private var isRecognizing: Bool = false
private var isSpeaking: Bool = false
// 语音识别和合成服务
private var azureAsrHelper: AzureAsrHelper?
private var azureTtsHelper: AzureTtsHelper?
// OpenAI相关
private var apiKey: String = ""
private var baseUrl: String = "https://api.openai.com"
private var model: String = "gpt-3.5-turbo"
private var systemPrompt: String = ""
private var chatHistory: [[String: Any]] = []
// MCP服务相关
private var mcpServer: String = ""
// 状态标志
private var isInitialized: Bool = false
private var hasSpeechDetected: Bool = false
private var isAiStreaming: Bool = false
// 空闲检测相关
private var idleCheckTimer: Timer?
private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数
// 音频播放器
private var audioPlayer: AudioPlayer?
// MARK: - 初始化
/// 私有初始化方法
private override init() {
super.init()
// 设置音频会话
setupAudioSession()
// 初始化音频播放器
audioPlayer = AudioPlayer()
// 初始化语音助手服务
azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper()
// 设置BleService回调
BleService.shared.setDelegate(self)
}
// MARK: - 监听器管理
/// 添加事件监听器
/// - Parameter listener: 要添加的监听器
func addListener(_ listener: AgentServiceListener) {
listenersLock.lock()
listeners.insert(listener)
listenersLock.unlock()
}
/// 移除事件监听器
/// - Parameter listener: 要移除的监听器
func removeListener(_ listener: AgentServiceListener) {
listenersLock.lock()
listeners.remove(listener)
listenersLock.unlock()
}
/// 清除所有监听器
func clearListeners() {
listenersLock.lock()
listeners.removeAll()
listenersLock.unlock()
}
/// 向所有监听器发送事件
/// - Parameters:
/// - eventName: 事件名称
/// - data: 事件数据
private func sendEvent(name eventName: String, data: [String: Any]) {
listenersLock.lock()
let currentListeners = self.listeners
listenersLock.unlock()
for listener in currentListeners {
listener.onEvent(eventName: eventName, data: data)
}
}
/// 发送错误事件
/// - Parameter message: 错误信息
private func sendError(_ message: String, code: String = "ERROR") {
sendEvent(name: "error", data: ["code": code, "message": message])
}
// MARK: - 初始化配置
/// 初始化系统提示词
private func initSystemPrompt() {
systemPrompt = """
你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。
[系统提示词内容省略...]
"""
}
/// 初始化配置
/// - Parameter config: 配置参数
/// - Returns: 是否初始化成功
func initialize(config: [String: Any]) -> Bool {
if isInitialized { return true }
// 设置OpenAI配置
if let openaiApiKey = config["openaiApiKey"] as? String {
self.apiKey = openaiApiKey
} else {
sendError("OpenAI API密钥缺失", code: "CONFIG_ERROR")
return false
}
if let openaiBaseUrl = config["openaiBaseUrl"] as? String {
self.baseUrl = openaiBaseUrl
}
if let openaiModel = config["openaiModel"] as? String {
self.model = openaiModel
}
// 设置系统提示词
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty {
self.systemPrompt = customSystemPrompt
} else {
initSystemPrompt()
}
// 设置Azure语音配置
if let azureSpeechKey = config["azureSpeechKey"] as? String {
self.azureSpeechKey = azureSpeechKey
} else {
sendError("Azure语音密钥缺失", code: "CONFIG_ERROR")
return false
}
if let azureSpeechRegion = config["azureSpeechRegion"] as? String {
self.azureSpeechRegion = azureSpeechRegion
} else {
sendError("Azure语音区域缺失", code: "CONFIG_ERROR")
return false
}
// 设置MCP服务
if let mcpServer = config["mcpServer"] as? String {
self.mcpServer = mcpServer
}
// 初始化Azure语音服务
let asrInitSuccess = initializeAzureSpeech()
isInitialized = asrInitSuccess
return asrInitSuccess
}
// MARK: - 音频会话设置
/// 设置音频会话
private func setupAudioSession() {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth])
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
sendError("设置音频会话失败: \(error.localizedDescription)")
}
}
// MARK: - Azure 语音服务
/// 初始化Azure语音服务
private func initializeAzureSpeech() -> Bool {
// 添加事件监听器
azureAsrHelper?.addListener(self)
azureTtsHelper?.addListener(self)
// 初始化ASR服务
guard let asrSuccess = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
supportedLanguages: ["zh-CN"]
), asrSuccess else {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
return false
}
// 初始化TTS服务
guard let ttsSuccess = azureTtsHelper?.initialize(
ttsAppId: "", // Azure TTS不需要appId
ttsAppToken: azureSpeechKey,
ttsResource: azureSpeechRegion,
language: "zh-CN"
), ttsSuccess else {
sendError("初始化语音合成服务失败", code: "TTS_INIT_ERROR")
return false
}
// 设置TTS语音
_ = azureTtsHelper?.setVoice("zh-CN-XiaoxiaoNeural")
return true
}
// MARK: - 空闲检测
/// 启动空闲检测
private func startIdleCheck() {
stopIdleCheck() // 先停止现有的检查
if !isRecognizing { return }
// 创建一个新的定时器
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in
guard let self = self else { return }
if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming {
self.stopRecognition()
self.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout",
"seconds": self.maxIdleSeconds
])
}
}
}
/// 停止空闲检测
private func stopIdleCheck() {
idleCheckTimer?.invalidate()
idleCheckTimer = nil
}
/// 重启空闲检测
private func restartIdleCheck() {
if isRecognizing {
startIdleCheck()
}
}
// MARK: - 语音识别
/// 开始语音识别
/// - Returns: 是否成功启动识别
func startRecognition() -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
// 防止重复启动
if isRecognizing {
stopRecognition()
}
// 启动语音识别
guard let success = azureAsrHelper?.startContinuousRecognition(), success else {
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR")
return false
}
return true
}
/// 停止语音识别
/// - Returns: 是否成功停止识别
func stopRecognition() -> Bool {
if !isRecognizing {
return true
}
// 停止语音识别
guard let success = azureAsrHelper?.stopContinuousRecognition(), success else {
return false
}
return true
}
/// 推送音频数据
/// - Parameter audioData: 音频数据
/// - Returns: 是否成功推送
func pushAudioData(_ audioData: Data) -> Bool {
if !isRecognizing || !isInitialized {
return false
}
// 将音频数据推送到语音识别引擎
return azureAsrHelper?.pushAudioData(audioData: audioData) ?? false
}
// MARK: - 语音合成
/// 语音合成播放文本
/// - Parameter text: 要播放的文本
/// - Returns: 是否成功开始播放
func speakText(_ text: String) -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
if text.isEmpty {
return false
}
// 使用TTS服务进行语音合成
return azureTtsHelper?.speakOnce(text) ?? false
}
/// 停止语音合成
/// - Returns: 是否成功停止合成
func stopTts() -> Bool {
if !isSpeaking {
return true
}
// 停止TTS服务
return azureTtsHelper?.stop() ?? false
}
/// 打断当前响应
/// - Returns: 是否成功打断
func interruptCurrentResponse() -> Bool {
var interrupted = false
// 停止TTS播放
if isSpeaking {
interrupted = stopTts() || interrupted
}
// 停止AI流输出
if isAiStreaming {
isAiStreaming = false
interrupted = true
}
// 发送打断事件
if interrupted {
sendEvent(name: "response_interrupted", data: ["status": "interrupted"])
}
return interrupted
}
// MARK: - 文本处理
/// 处理文本输入
/// - Parameters:
/// - text: 文本内容
/// - speakResponse: 是否朗读响应
/// - Returns: 是否成功处理
func processTextInput(_ text: String, speakResponse: Bool) -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
if text.isEmpty {
sendError("文本输入不能为空", code: "EMPTY_TEXT")
return false
}
// TODO: 实现与OpenAI的通信
// 这里只是模拟处理
// 将用户消息添加到聊天历史
let userMessage: [String: Any] = [
"role": "user",
"content": text
]
chatHistory.append(userMessage)
// 更新状态
isAiStreaming = true
// 模拟助手响应
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
// 模拟流式输出
let response = "这是对"\(text)"的回复。我是AI助手,很高兴为您服务。"
// 逐词输出模拟
let words = response.split(separator: " ")
for (index, word) in words.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in
guard let self = self, self.isAiStreaming else { return }
self.sendEvent(name: "assistant_token", data: ["token": String(word) + " "])
// 如果需要语音播报,则合成语音
if speakResponse {
self.azureTtsHelper?.speakStream(String(word) + " ")
}
}
}
// 完整响应
DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in
guard let self = self else { return }
// 结束AI流式输出
self.isAiStreaming = false
// 发送完整响应
self.sendEvent(name: "assistant_response", data: ["text": response])
// 将助手消息添加到聊天历史
let assistantMessage: [String: Any] = [
"role": "assistant",
"content": response
]
self.chatHistory.append(assistantMessage)
// 如果需要语音播报,则完成语音流
if speakResponse {
self.azureTtsHelper?.flushStream()
}
}
}
return true
}
// MARK: - 图片处理
/// 处理图片输入
/// - Parameters:
/// - imagePath: 图片路径
/// - text: 文本描述
/// - speakResponse: 是否朗读响应
/// - Returns: 是否成功处理
func processImageInput(imagePath: String, text: String, speakResponse: Bool) -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
if imagePath.isEmpty {
sendError("图片路径不能为空", code: "EMPTY_IMAGE_PATH")
return false
}
// 发送图片处理中事件
sendEvent(name: "image_processing", data: [:])
// TODO: 实现与OpenAI Vision API的通信
// 这里只是模拟处理
// 模拟图片处理
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
guard let self = self else { return }
// 图片就绪事件
self.sendEvent(name: "image_ready", data: [:])
// 更新状态
self.isAiStreaming = true
// 模拟AI分析结果
let imageDescription = "这是一张图片分析结果。"
let additionalText = text.isEmpty ? "" : "关于您提问的"\(text)","
let response = "\(imageDescription)\(additionalText)我可以看到这是一张图片。"
// 发送AI助手token流
let words = response.split(separator: " ")
for (index, word) in words.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in
guard let self = self, self.isAiStreaming else { return }
let token = String(word) + " "
self.sendEvent(name: "assistant_token", data: ["token": token])
// 如果需要语音播报,则合成语音
if speakResponse {
self.azureTtsHelper?.speakStream(token)
}
}
}
// 发送完整响应
DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in
guard let self = self else { return }
// 结束AI流式输出
self.isAiStreaming = false
self.sendEvent(name: "assistant_response", data: [
"text": response,
"userInput": text.isEmpty ? "[图片]" : text,
"hasImage": true
])
// 如果需要语音播报,则完成语音
if speakResponse {
self.azureTtsHelper?.flushStream()
}
}
}
return true
}
// MARK: - 聊天历史管理
/// 清除聊天历史
/// - Returns: 是否成功清除
func clearChatHistory() -> Bool {
chatHistory.removeAll()
return true
}
// MARK: - 资源释放
/// 释放资源
/// - Returns: 是否成功释放
func dispose() -> Bool {
// 停止语音识别
if isRecognizing {
stopRecognition()
}
// 停止语音合成
if isSpeaking {
stopTts()
}
// 停止空闲检测
stopIdleCheck()
// 移除BleService代理
BleService.shared.setDelegate(nil)
// 释放Azure Speech资源
azureAsrHelper?.removeListener(self)
azureTtsHelper?.removeListener(self)
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
// 释放音频播放器
audioPlayer = nil
// 清除监听器
clearListeners()
isInitialized = false
return true
}
}
// MARK: - AsrEventListener 实现
extension AgentServiceImpl: AsrEventListener {
func onEvent(_ event: AsrEvent) {
switch event.type {
case .sessionStarted:
sendEvent(name: "recognition_started", data: [:])
isRecognizing = true
startIdleCheck()
audioPlayer?.playStartSound()
case .sessionStopped:
sendEvent(name: "recognition_stopped", data: [:])
isRecognizing = false
stopIdleCheck()
audioPlayer?.playStopSound()
case .recognizing:
if let text = event.params["text"] as? String {
// 检测到语音,更新状态
let previousHasSpeech = hasSpeechDetected
hasSpeechDetected = true
if !previousHasSpeech {
restartIdleCheck()
}
var data: [String: Any] = ["text": text]
if let language = event.params["language"] as? String, !language.isEmpty {
data["language"] = language
}
sendEvent(name: "recognizing", data: data)
// 如果TTS正在播放或AI正在生成,则触发打断
if isSpeaking || isAiStreaming {
interruptCurrentResponse()
}
}
case .recognized:
if let text = event.params["text"] as? String, !text.isEmpty {
var data: [String: Any] = ["text": text]
if let language = event.params["language"] as? String, !language.isEmpty {
data["language"] = language
}
sendEvent(name: "recognition_result", data: data)
// 处理识别结果,发送到OpenAI
processTextInput(text, speakResponse: true)
}
// 重置状态,继续识别
let previousHasSpeech = hasSpeechDetected
hasSpeechDetected = false
if previousHasSpeech {
restartIdleCheck()
}
case .canceled:
var data: [String: Any] = [:]
if let reason = event.params["reason"] as? String {
data["reason"] = reason
}
if let errorDetails = event.params["errorDetails"] as? String {
data["details"] = errorDetails
}
sendEvent(name: "recognition_canceled", data: data)
isRecognizing = false
stopIdleCheck()
case .error:
var data: [String: Any] = [:]
if let errorCode = event.params["errorCode"] as? String {
data["errorCode"] = errorCode
}
if let message = event.params["message"] as? String {
data["message"] = message
} else if let error = event.params["error"] as? String {
data["message"] = error
} else {
data["message"] = "未知错误"
}
sendEvent(name: "recognition_error", data: data)
isRecognizing = false
stopIdleCheck()
default:
break
}
}
}
// MARK: - TtsEventListener 实现
extension AgentServiceImpl: TtsEventListener {
func onEvent(_ event: TtsEvent) {
switch event.type {
case .synthesisStarted:
isSpeaking = true
restartIdleCheck()
sendEvent(name: "tts_started", data: [:])
case .synthesisCompleted:
isSpeaking = false
restartIdleCheck()
sendEvent(name: "tts_completed", data: [:])
case .synthesisCanceled:
isSpeaking = false
restartIdleCheck()
var data: [String: Any] = [:]
if let reason = event.params["reason"] as? String {
data["reason"] = reason
}
if let errorDetails = event.params["errorDetails"] as? String {
data["details"] = errorDetails
}
sendEvent(name: "tts_canceled", data: data)
case .error:
isSpeaking = false
restartIdleCheck()
var data: [String: Any] = [:]
if let errorCode = event.params["errorCode"] as? String {
data["errorCode"] = errorCode
}
if let errorMessage = event.params["errorMessage"] as? String {
data["message"] = errorMessage
} else {
data["message"] = "TTS错误"
}
sendEvent(name: "error", data: data)
default:
break
}
}
}
// MARK: - BleService.Callback 实现
extension AgentServiceImpl: BleService.Callback {
func onScanResult(devices: [[String: Any]]) {
// 不处理扫描结果
}
func onConnectionStateChanged(state: Int) {
// 可以在这里处理连接状态变化,例如通知UI更新
}
func onAudioDataReceived(data: Data) {
// 直接将音频数据推送到Azure Speech进行识别
pushAudioData(data)
}
func onWakeupSignalReceived() {
NSLog("\(TAG): 收到唤醒信号")
// 停止当前TTS,避免冲突
stopTts()
// 如果已经在识别,则重新启动
stopRecognition()
startRecognition()
}
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) {
// 可以处理设备信息,例如电量、设备名称等
}
}
// MARK: - 音频播放器
/// 简单的音频播放器,用于播放提示音
class AudioPlayer {
private var player: AVAudioPlayer?
/// 播放开始识别提示音
func playStartSound() {
playSound(named: "start")
}
/// 播放停止识别提示音
func playStopSound() {
playSound(named: "stop")
}
/// 播放音频文件
/// - Parameter named: 音频文件名
private func playSound(named: String) {
guard let path = Bundle.main.path(forResource: named, ofType: "wav") else {
print("找不到音频文件: \(named).wav")
return
}
let url = URL(fileURLWithPath: path)
do {
player = try AVAudioPlayer(contentsOf: url)
player?.prepareToPlay()
player?.play()
} catch {
print("播放音频失败: \(error.localizedDescription)")
}
}
}

11
local_plugins/agent_service/ios/Classes/AgentServiceListener.swift

@ -1,11 +0,0 @@
import Foundation
/// 代理服务事件监听器协议
/// 用于接收代理服务发送的各类事件
protocol AgentServiceListener: AnyObject {
/// 事件回调
/// - Parameters:
/// - eventName: 事件名称
/// - data: 事件数据
func onEvent(eventName: String, data: [String: Any])
}

2
local_plugins/agent_service/ios/Classes/AgentServicePlugin.m

@ -7,6 +7,6 @@
@implementation AgentServicePlugin @implementation AgentServicePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftAgentServicePlugin registerWithRegistrar:registrar]; [AgentServicePlugin registerWithRegistrar:registrar];
} }
@end @end

227
local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift

@ -0,0 +1,227 @@
import Flutter
import UIKit
import AVFoundation
/// 代理服务插件,负责连接Flutter端与原生服务
public class AgentServicePlugin: NSObject, FlutterPlugin {
private let TAG = "AgentServicePlugin"
private var eventSink: FlutterEventSink?
// MARK: - 插件注册
/// 插件注册方法
public static func register(with registrar: FlutterPluginRegistrar) {
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger())
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger())
let instance = AgentServicePlugin()
registrar.addMethodCallDelegate(instance, channel: methodChannel)
eventChannel.setStreamHandler(instance)
// 添加监听器
AgentServiceImpl.shared.addListener(instance)
NSLog("\(instance.TAG): 插件已加载")
}
// MARK: - 方法处理
/// 处理来自Flutter的方法调用
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
NSLog("\(TAG): 收到方法调用 - \(call.method)")
switch call.method {
case "initialize":
handleInitialize(call, result: result)
case "startAgentService":
handleStartAgentService(call, result: result)
case "stopAgentService":
handleStopAgentService(result: result)
case "startConversation":
handleStartConversation(result: result)
case "stopConversation":
handleStopConversation(result: result)
case "processTextInput":
handleProcessTextInput(call, result: result)
case "speakText":
handleSpeakText(call, result: result)
case "stopTts":
handleStopTts(result: result)
case "clearChatHistory":
handleClearChatHistory(result: result)
case "interruptCurrentResponse":
handleInterruptCurrentResponse(result: result)
case "processImageInput":
handleProcessImageInput(call, result: result)
case "dispose":
handleDispose(result: result)
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - 方法实现
private func handleInitialize(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let config = args["config"] as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil))
return
}
// 初始化AgentServiceImpl
let success = AgentServiceImpl.shared.initialize(config: config)
result(success)
}
private func handleStartAgentService(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let config = args["config"] as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil))
return
}
NSLog("\(TAG): 启动AgentService,配置参数: \(config)")
// 初始化AgentServiceImpl
let success = AgentServiceImpl.shared.initialize(config: config)
if success {
result(true)
} else {
result(FlutterError(code: "SERVICE_START_ERROR", message: "AgentService启动失败", details: nil))
}
}
private func handleStopAgentService(result: @escaping FlutterResult) {
NSLog("\(TAG): 停止AgentService")
let success = AgentServiceImpl.shared.dispose()
result(success)
}
private func handleStartConversation(result: @escaping FlutterResult) {
NSLog("\(TAG): 开始对话")
let success = AgentServiceImpl.shared.startRecognition()
result(success)
}
private func handleStopConversation(result: @escaping FlutterResult) {
NSLog("\(TAG): 停止对话")
let success = AgentServiceImpl.shared.stopRecognition()
result(success)
}
private func handleProcessTextInput(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
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 speakResponse = args["speakResponse"] as? Bool ?? false
NSLog("\(TAG): 处理文本输入 - \(text)")
let success = AgentServiceImpl.shared.processTextInput(text, speakResponse: speakResponse)
result(success)
}
private func handleSpeakText(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return
}
NSLog("\(TAG): 语音合成 - \(text)")
let success = AgentServiceImpl.shared.speakText(text)
result(success)
}
private func handleStopTts(result: @escaping FlutterResult) {
NSLog("\(TAG): 停止语音合成")
let success = AgentServiceImpl.shared.stopTts()
result(success)
}
private func handleClearChatHistory(result: @escaping FlutterResult) {
NSLog("\(TAG): 清除聊天历史")
let success = AgentServiceImpl.shared.clearChatHistory()
result(success)
}
private func handleInterruptCurrentResponse(result: @escaping FlutterResult) {
NSLog("\(TAG): 中断当前响应")
let success = AgentServiceImpl.shared.interruptCurrentResponse()
result(success)
}
private func handleProcessImageInput(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let imagePath = args["imagePath"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil))
return
}
let text = args["text"] as? String ?? ""
let speakResponse = args["speakResponse"] as? Bool ?? false
NSLog("\(TAG): 处理图片输入 - \(imagePath)")
let success = AgentServiceImpl.shared.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse)
result(success)
}
private func handleDispose(result: @escaping FlutterResult) {
NSLog("\(TAG): 释放资源")
// 移除监听器并释放资源
AgentServiceImpl.shared.removeListener(self)
let success = AgentServiceImpl.shared.dispose()
result(success)
}
// MARK: - 实例析构
deinit {
// 确保在实例销毁时移除监听器
AgentServiceImpl.shared.removeListener(self)
NSLog("\(TAG): 插件已卸载")
}
}
// MARK: - AgentServiceListener 实现
extension AgentServicePlugin: AgentServiceListener {
public func onEvent(eventName: String, data: [String: Any]) {
guard let eventSink = eventSink else { return }
// 将事件转换为JSON字符串
let eventData: [String: Any] = [
"event": eventName,
"data": data
]
do {
let jsonData = try JSONSerialization.data(withJSONObject: eventData, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) {
// 确保在主线程发送事件到Flutter
DispatchQueue.main.async {
eventSink(jsonString)
}
}
} catch {
NSLog("\(TAG): 错误 - 事件数据序列化失败: \(error.localizedDescription)")
}
}
}
// MARK: - FlutterStreamHandler 实现
extension AgentServicePlugin: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}

259
local_plugins/agent_service/ios/Classes/SwiftAgentServicePlugin.swift

@ -1,259 +0,0 @@
import Flutter
import UIKit
import open_ai_service
import chat_storage
/// Swift版Agent Service插件
/// 处理Flutter与原生层的通信
public class SwiftAgentServicePlugin: NSObject, FlutterPlugin {
// Flutter方法通道
private var methodChannel: FlutterMethodChannel?
// Flutter事件通道
private var eventChannel: FlutterEventChannel?
// 事件sink,用于向Flutter发送事件
private var eventSink: FlutterEventSink?
// 核心服务实例
private var agentService: AgentService?
/// 注册插件
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftAgentServicePlugin()
// 初始化方法通道
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: methodChannel)
instance.methodChannel = methodChannel
// 初始化事件通道
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger())
eventChannel.setStreamHandler(instance)
instance.eventChannel = eventChannel
}
/// 处理方法调用
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "initialize":
handleInitialize(call, result)
case "startConversation":
handleStartConversation(result)
case "stopConversation":
handleStopConversation(result)
case "processTextInput":
handleProcessTextInput(call, result)
case "speakText":
handleSpeakText(call, result)
case "stopTts":
handleStopTts(result)
case "clearChatHistory":
handleClearChatHistory(result)
case "interruptCurrentResponse":
handleInterruptCurrentResponse(result)
case "processImageInput":
handleProcessImageInput(call, result)
case "dispose":
handleDispose(result)
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - 方法处理
/// 初始化
private func handleInitialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let config = args["config"] as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "缺少配置参数", details: nil))
return
}
// 初始化代理服务
if agentService == nil {
agentService = AgentService()
// 设置事件监听器
agentService?.setListener(self)
}
// 执行初始化
let success = agentService?.initialize(config: config) ?? false
result(success)
}
/// 开始对话
private func handleStartConversation(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
let success = service.startRecognition()
result(success)
}
/// 停止对话
private func handleStopConversation(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
service.stopRecognition()
result(true)
}
/// 处理文本输入
private func handleProcessTextInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
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 speakResponse = (args["speakResponse"] as? Bool) ?? false
let success = service.processTextInput(text: text, speakResponse: speakResponse)
result(success)
}
/// 朗读文本
private func handleSpeakText(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil))
return
}
service.speakText(text: text)
result(true)
}
/// 停止语音合成
private func handleStopTts(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
service.stopTts()
result(true)
}
/// 清除聊天历史
private func handleClearChatHistory(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
service.clearChatHistory { success in
result(success)
}
}
/// 中断当前响应
private func handleInterruptCurrentResponse(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
service.interruptCurrentResponse()
result(true)
}
/// 处理图片输入
private func handleProcessImageInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
guard let args = call.arguments as? [String: Any],
let imagePath = args["imagePath"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的图片路径参数", details: nil))
return
}
let text = (args["text"] as? String) ?? ""
let speakResponse = (args["speakResponse"] as? Bool) ?? false
let success = service.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse)
result(success)
}
/// 释放资源
private func handleDispose(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(true) // 已经是nil,直接返回成功
return
}
service.dispose()
agentService = nil
result(true)
}
}
// MARK: - FlutterStreamHandler
extension SwiftAgentServicePlugin: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}
// MARK: - AgentServiceListener
extension SwiftAgentServicePlugin: AgentServiceListener {
func onEvent(eventName: String, data: [String: Any]) {
guard let sink = eventSink else { return }
do {
// 构建事件JSON
let eventMap: [String: Any] = [
"event": eventName,
"data": data
]
// 将事件转换为JSON字符串
let jsonData = try JSONSerialization.data(withJSONObject: eventMap, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) {
// 发送到Flutter
DispatchQueue.main.async {
sink(jsonString)
}
}
} catch {
print("事件序列化失败: \(error.localizedDescription)")
}
}
}

25
local_plugins/agent_service/ios/agent_service.podspec

@ -0,0 +1,25 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint agent_service.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'agent_service'
s.version = '0.0.1'
s.summary = 'A Flutter plugin for agent service with voice interaction capabilities.'
s.description = <<-DESC
A Flutter plugin that provides voice interaction, AI conversation and text-to-speech functionalities.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'azure_speech'
s.dependency 'ble_service'
s.platform = :ios, '11.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'
end

15
local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h

@ -1,4 +1,17 @@
//
// AzureSpeechPlugin.h
// azure_speech
//
// Created for azure_speech plugin compatibility.
//
#import <Flutter/Flutter.h> #import <Flutter/Flutter.h>
@interface AzureSpeechPlugin : NSObject<FlutterPlugin> /**
* Azure Speech Plugin 头文件
*
* 注:实际实现使用 Swift,此头文件仅用于 Flutter 框架兼容
*/
@interface AzureSpeechPlugin : NSObject <FlutterPlugin>
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar;
@end @end

12
local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.m

@ -1,12 +0,0 @@
#import "AzureSpeechPlugin.h"
#if __has_include(<azure_speech/azure_speech-Swift.h>)
#import <azure_speech/azure_speech-Swift.h>
#else
#import "azure_speech-Swift.h"
#endif
@implementation AzureSpeechPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftAzureSpeechPlugin registerWithRegistrar:registrar];
}
@end

12
local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift

@ -11,7 +11,7 @@ import speech
* 基于微软Azure语音服务的Flutter插件 * 基于微软Azure语音服务的Flutter插件
* 提供语音识别(ASR)和语音合成(TTS)功能 * 提供语音识别(ASR)和语音合成(TTS)功能
*/ */
public class SwiftAzureSpeechPlugin: NSObject, FlutterPlugin { @objc public class AzureSpeechPlugin: NSObject, FlutterPlugin {
// 日志标签 // 日志标签
private let tag = "AzureSpeechPlugin" private let tag = "AzureSpeechPlugin"
@ -32,7 +32,7 @@ public class SwiftAzureSpeechPlugin: NSObject, FlutterPlugin {
// 插件注册 // 插件注册
public static func register(with registrar: FlutterPluginRegistrar) { public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftAzureSpeechPlugin() let instance = AzureSpeechPlugin()
// 初始化ASR通道 // 初始化ASR通道
let asrChannel = FlutterMethodChannel(name: "azure_speech/asr", binaryMessenger: registrar.messenger()) let asrChannel = FlutterMethodChannel(name: "azure_speech/asr", binaryMessenger: registrar.messenger())
@ -248,7 +248,7 @@ public class SwiftAzureSpeechPlugin: NSObject, FlutterPlugin {
} }
// MARK: - 事件处理 // MARK: - 事件处理
extension SwiftAzureSpeechPlugin: FlutterStreamHandler { extension AzureSpeechPlugin: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
// 根据通道类型设置事件接收器 // 根据通道类型设置事件接收器
if let args = arguments as? [String: Any], if let args = arguments as? [String: Any],
@ -297,7 +297,7 @@ extension SwiftAzureSpeechPlugin: FlutterStreamHandler {
} }
// MARK: - ASR 事件监听实现 // MARK: - ASR 事件监听实现
extension SwiftAzureSpeechPlugin: AsrEventListener { extension AzureSpeechPlugin: AsrEventListener {
// 为ASR事件监听提供明确的选择器名称 // 为ASR事件监听提供明确的选择器名称
@objc(onAsrEvent:) @objc(onAsrEvent:)
public func onEvent(_ event: AsrEvent) { public func onEvent(_ event: AsrEvent) {
@ -334,7 +334,7 @@ extension SwiftAzureSpeechPlugin: AsrEventListener {
} }
// MARK: - TTS 事件监听实现 // MARK: - TTS 事件监听实现
extension SwiftAzureSpeechPlugin: TtsEventListener { extension AzureSpeechPlugin: TtsEventListener {
// 使用@objc特性为方法提供一个不同的Objective-C选择器名称 // 使用@objc特性为方法提供一个不同的Objective-C选择器名称
@objc(onTtsEvent:) @objc(onTtsEvent:)
public func onEvent(_ event: TtsEvent) { public func onEvent(_ event: TtsEvent) {
@ -374,7 +374,7 @@ extension SwiftAzureSpeechPlugin: TtsEventListener {
} }
// MARK: - 音频数据监听实现 // MARK: - 音频数据监听实现
extension SwiftAzureSpeechPlugin: AudioDataListener { extension AzureSpeechPlugin: AudioDataListener {
public func onAudioData(_ data: Data) { public func onAudioData(_ data: Data) {
if ttsEventSink == nil { return } if ttsEventSink == nil { return }

6
local_plugins/azure_speech/ios/Classes/azure_speech-Bridging-Header.h

@ -1,6 +0,0 @@
#ifndef azure_speech_Bridging_Header_h
#define azure_speech_Bridging_Header_h
#import <Foundation/Foundation.h>
#endif /* azure_speech_Bridging_Header_h */

5
local_plugins/azure_speech/ios/Classes/azure_speech.h

@ -1,5 +0,0 @@
#import <Foundation/Foundation.h>
#import "AzureSpeechPlugin.h"
// 添加其他公共头文件导出

8
local_plugins/azure_speech/ios/azure_speech.podspec

@ -13,13 +13,17 @@ A Flutter plugin for Azure Speech Services providing speech recognition and text
s.license = { :file => '../LICENSE' } s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' } s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' } s.source = { :path => '.' }
s.source_files = 'Classes/**/*' s.source_files = 'Classes/**/*.{swift}'
s.dependency 'Flutter' s.dependency 'Flutter'
s.dependency 'speech' s.dependency 'speech'
s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.30.0' s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.30.0'
s.platform = :ios, '11.0' s.platform = :ios, '11.0'
# Flutter.framework does not contain a i386 slice. # Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
'SWIFT_EMIT_LOC_STRINGS' => 'YES'
}
s.swift_version = '5.0' s.swift_version = '5.0'
end end

684
local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift

@ -1,72 +1,24 @@
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
import os.log import os.log
import ObjectiveC // 添加ObjC运行时支持
import AVFoundation import AVFoundation
// 条件编译只导入在iOS 18+上可用的框架
#if canImport(AccessorySetupKit) && swift(>=5.9)
import AccessorySetupKit import AccessorySetupKit
import UIKit import UIKit
#endif
class BleAccessorySetup: NSObject { class BleAccessorySetup: NSObject {
// 单例实现 // 单例实现
static let shared = BleAccessorySetup() static let shared = BleAccessorySetup()
// 基本属性 // 基本属性
private var bleService: Any? 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 logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup")
// iOS 18+ 特定属性和回调
#if canImport(AccessorySetupKit) && swift(>=5.9)
@available(iOS 18.0, *)
class AccessorySessionWrapper {
var session: ASAccessorySession
var isActivated = false
init() throws {
do {
os_log("尝试初始化ASAccessorySession...", log: BleAccessorySetup.shared.logger, type: .debug)
// 根据WWDC24示例,使用无参初始化
self.session = try ASAccessorySession()
os_log("ASAccessorySession初始化成功", log: BleAccessorySetup.shared.logger, type: .info)
} catch {
os_log("ASAccessorySession初始化错误: %{public}@", log: BleAccessorySetup.shared.logger, type: .error, error.localizedDescription)
// 检查Info.plist配置
if let plistSupports = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") {
os_log("NSAccessorySetupKitSupports: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: plistSupports))
} else {
os_log("NSAccessorySetupKitSupports未配置", log: BleAccessorySetup.shared.logger, type: .error)
}
if let services = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") {
os_log("NSAccessorySetupBluetoothServices: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: services))
} else {
os_log("NSAccessorySetupBluetoothServices未配置", log: BleAccessorySetup.shared.logger, type: .error)
}
if let companyIds = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: companyIds))
} else {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers未配置", log: BleAccessorySetup.shared.logger, type: .error)
}
throw error
}
}
}
var accessorySessionWrapper: Any?
// 添加初始化完成回调
private var onSessionActivated: (() -> Void)? private var onSessionActivated: (() -> Void)?
typealias DeviceDiscoveredCallback = (UUID) -> Void typealias DeviceDiscoveredCallback = (UUID) -> Void
private var onDeviceDiscovered: DeviceDiscoveredCallback? private var onDeviceDiscovered: DeviceDiscoveredCallback?
#endif
private var isInitializing = false
private override init() { private override init() {
super.init() super.init()
@ -74,238 +26,149 @@ class BleAccessorySetup: NSObject {
// 设置BLE服务实例 // 设置BLE服务实例
func setBleService(_ service: Any) { func setBleService(_ service: Any) {
self.bleService = service self.bleService = service as? BleService
os_log("BLE服务实例已设置", log: logger, type: .info)
} }
// 设置设备发现回调 // 设置设备发现回调
func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) { func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
#if canImport(AccessorySetupKit) && swift(>=5.9) self.onDeviceDiscovered = callback
if #available(iOS 18.0, *) {
self.onDeviceDiscovered = callback
os_log("设备发现回调已设置", log: logger, type: .debug)
}
#endif
} }
// 初始化设备发现会话 // 初始化设备发现会话
func initializeDiscoverySession() { func initializeDiscoverySession() {
#if canImport(AccessorySetupKit) && swift(>=5.9) os_log("开始初始化设备发现会话", log: logger, type: .info)
if #available(iOS 18.0, *) {
// 避免重复初始化 if isInitializing || isSessionActivated {
if isInitializing { os_log("会话已在初始化或已激活,跳过初始化", log: logger, type: .debug)
os_log("初始化正在进行中,请等待...", log: logger, type: .debug) return
return }
}
isInitializing = true
// 如果会话已经存在且已激活,无需重新初始化
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, wrapper.isActivated { do {
os_log("会话已经初始化并激活", log: logger, type: .info) os_log("创建ASAccessorySession", log: logger, type: .debug)
return session = try ASAccessorySession()
}
isInitializing = true
os_log("初始化iOS 18 AccessorySetupKit会话...", log: logger, type: .debug)
// 检查Info.plist配置并详细打印
if let plistCheck = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") {
os_log("NSAccessorySetupKitSupports配置: %{public}@", log: logger, type: .debug, String(describing: plistCheck))
} else {
os_log("NSAccessorySetupKitSupports未配置", log: logger, type: .error)
}
if let services = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") {
os_log("NSAccessorySetupBluetoothServices配置: %{public}@", log: logger, type: .debug, String(describing: services))
} else {
os_log("NSAccessorySetupBluetoothServices未配置", log: logger, type: .error)
}
if let companyIds = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers配置: %{public}@", log: logger, type: .debug, String(describing: companyIds))
} else {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers未配置", log: logger, type: .error)
}
// 创建会话 (使用try-catch) os_log("激活会话", log: logger, type: .debug)
do { session?.activate(on: .main) { [weak self] event in
os_log("尝试创建AccessorySessionWrapper...", log: logger, type: .debug) guard let self = self else { return }
accessorySessionWrapper = try AccessorySessionWrapper()
os_log("AccessorySessionWrapper创建成功", log: logger, type: .info)
// 激活会话并设置事件处理 os_log("收到会话事件: %{public}@", log: self.logger, type: .debug, String(describing: event.eventType))
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper {
os_log("尝试激活ASAccessorySession...", log: logger, type: .debug) switch event.eventType {
wrapper.session.activate(on: .main) { [weak self] event in case .activated:
guard let self = self else { os_log("会话已激活", log: self.logger, type: .info)
os_log("self已被释放,无法处理事件", log: BleAccessorySetup.shared.logger, type: .error) self.isSessionActivated = true
return DispatchQueue.main.async(execute: DispatchWorkItem(block: {
} self.onSessionActivated?()
self.onSessionActivated = nil
}))
case .accessoryAdded:
if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier {
os_log("发现设备: %{public}@", log: self.logger, type: .info, uuid.uuidString)
os_log("收到ASAccessorySession事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue) // 记录设备详细信息
let name = "设备_\(uuid.uuidString.prefix(8))"
os_log("设备名称: %{public}@", log: self.logger, type: .debug, name)
// 直接获取公司ID无需使用可选链
let descriptor = accessory.descriptor
let companyIdentifier = descriptor.bluetoothCompanyIdentifier.rawValue
os_log("设备公司ID: 0x%{public}x", log: self.logger, type: .debug, companyIdentifier)
switch event.eventType { self.onDeviceDiscovered?(uuid)
case .activated: } else {
os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info) os_log("发现设备但缺少UUID信息", log: self.logger, type: .error)
if let accessory = event.accessory {
// 标记会话已激活 os_log("设备信息: %{public}@", log: self.logger, type: .debug, String(describing: accessory))
if let wrapper = self.accessorySessionWrapper as? AccessorySessionWrapper {
wrapper.isActivated = true
// 触发激活回调
DispatchQueue.main.async {
self.onSessionActivated?()
self.onSessionActivated = nil // 防止重复调用
}
}
case .accessoryAdded:
if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier {
os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName)
self.onDeviceDiscovered?(uuid)
} else {
os_log("设备添加事件但缺少设备信息或UUID", log: self.logger, type: .debug)
}
case .accessoryRemoved:
if let accessory = event.accessory {
os_log("设备已移除: %{public}@", log: self.logger, type: .info, accessory.displayName)
}
// 根据WWDC24视频,正确的事件类型为accessoryChanged
case .accessoryChanged:
if let accessory = event.accessory {
os_log("设备已更新: %{public}@", log: self.logger, type: .info, accessory.displayName)
}
// 根据WWDC24视频,失败事件类型已变更
case .pickerSetupFailed:
if let error = event.error {
os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription)
} else {
os_log("会话出错但无错误详情", log: self.logger, type: .debug)
}
default:
os_log("收到其他会话事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue)
} }
} }
// 检查现有授权设备 case .accessoryRemoved:
let accessoriesCount = wrapper.session.accessories.count os_log("设备已移除", log: self.logger, type: .debug)
os_log("发现%{public}d个已授权设备", log: logger, type: .info, accessoriesCount) if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier {
os_log("移除设备UUID: %{public}@", log: self.logger, type: .debug, uuid.uuidString)
if accessoriesCount > 0 {
// 如果有现有设备,触发回调
for accessory in wrapper.session.accessories {
if let uuid = accessory.bluetoothIdentifier {
os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName)
self.onDeviceDiscovered?(uuid)
} else {
os_log("已授权设备缺少UUID: %{public}@", log: logger, type: .debug, accessory.displayName)
}
}
} }
} else {
os_log("accessorySessionWrapper类型转换失败", log: logger, type: .error) case .accessoryChanged:
os_log("设备已变更", log: self.logger, type: .debug)
case .pickerSetupFailed:
os_log("选择器设置失败: %{public}@", log: self.logger, type: .error, event.error?.localizedDescription ?? "未知错误")
default:
os_log("未处理的事件类型: %{public}@", log: self.logger, type: .debug, String(describing: event.eventType))
break
} }
} catch { }
os_log("致命错误:AccessorySetupKit初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription)
// 处理现有授权设备
// 重置初始化状态 if let accessories = session?.accessories {
isInitializing = false os_log("检查当前已授权设备:共%d个", log: logger, type: .debug, accessories.count)
for accessory in accessories {
// 释放失败的会话 if let uuid = accessory.bluetoothIdentifier {
accessorySessionWrapper = nil os_log("处理已授权设备:%@", log: logger, type: .info, uuid.uuidString)
let name = "设备_\(uuid.uuidString.prefix(8))"
// 尝试通知可能正在等待的回调 os_log("已授权设备名称:%@", log: logger, type: .debug, name)
DispatchQueue.main.async { self.onDeviceDiscovered?(uuid)
self.onSessionActivated?() } else {
self.onSessionActivated = nil os_log("发现已授权设备但无UUID", log: logger, type: .error)
}
} }
} else {
os_log("无已授权设备", log: logger, type: .debug)
} }
} else { } catch {
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) // 出错时重置状态
os_log("初始化会话失败:%@", log: logger, type: .error, error.localizedDescription)
isInitializing = false
session = nil
// 即使在不支持的系统上,也要调用回调 DispatchQueue.main.async(execute: DispatchWorkItem(block: {
DispatchQueue.main.async {
self.onSessionActivated?() self.onSessionActivated?()
self.onSessionActivated = nil self.onSessionActivated = nil
} }))
}
#else
os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info)
// 即使在不支持的环境中,也要调用回调
DispatchQueue.main.async {
self.onSessionActivated?()
self.onSessionActivated = nil
} }
#endif
// 完成初始化过程
isInitializing = false isInitializing = false
os_log("设备发现会话初始化完成", log: logger, type: .debug)
} }
// 初始化会话并在完成后执行回调 // 初始化会话并在完成后执行回调
func initializeSessionAndRun(completion: @escaping () -> Void) { func initializeSessionAndRun(completion: @escaping () -> Void) {
#if canImport(AccessorySetupKit) && swift(>=5.9) // 如果会话已存在且已激活
if #available(iOS 18.0, *) { if session != nil && isSessionActivated {
os_log("安全初始化会话...", log: logger, type: .debug)
// 如果会话已存在且已激活
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, wrapper.isActivated {
os_log("会话已初始化并激活,直接执行回调", log: logger, type: .info)
completion()
return
}
// 如果会话存在但未激活,设置回调
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, !wrapper.isActivated {
os_log("会话已创建但未激活,等待激活...", log: logger, type: .debug)
self.onSessionActivated = completion
return
}
// 需要从头创建会话
os_log("需要创建新会话...", log: logger, type: .debug)
self.onSessionActivated = completion
initializeDiscoverySession()
} else {
// 不支持iOS 18,直接执行回调
completion() completion()
return
}
// 如果会话存在但未激活,设置回调
if session != nil && !isSessionActivated {
self.onSessionActivated = completion
return
} }
#else
// 不支持AccessorySetupKit,直接执行回调 // 需要从头创建会话
completion() self.onSessionActivated = completion
#endif initializeDiscoverySession()
} }
// 重置会话状态 // 重置会话状态
func resetSession() { func resetSession() {
#if canImport(AccessorySetupKit) && swift(>=5.9) // 取消可能挂起的回调
if #available(iOS 18.0, *) { onSessionActivated = nil
os_log("重置AccessorySetupKit会话...", log: logger, type: .debug)
// 清理现有会话
// 取消可能挂起的回调 session?.invalidate()
onSessionActivated = nil
// 重置状态
// 清理现有会话 session = nil
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { isSessionActivated = false
os_log("清理现有会话", log: logger, type: .debug) isInitializing = false
wrapper.session.invalidate()
}
// 重置状态
accessorySessionWrapper = nil
isInitializing = false
os_log("会话已重置", log: logger, type: .info)
}
#endif
} }
// 安全显示设备选择器 // 安全显示设备选择器
func safeShowDevicePicker() { func safeShowDevicePicker() {
// 重置会话状态,确保干净的重新初始化
resetSession() resetSession()
initializeSessionAndRun { [weak self] in initializeSessionAndRun { [weak self] in
@ -316,247 +179,166 @@ class BleAccessorySetup: NSObject {
// 显示设备选择器 // 显示设备选择器
func showDevicePicker() { func showDevicePicker() {
#if canImport(AccessorySetupKit) && swift(>=5.9) os_log("尝试显示设备选择器", log: logger, type: .info)
if #available(iOS 18.0, *) {
os_log("尝试显示设备选择器...", log: logger, type: .debug) // 检查会话是否已初始化
if session == nil {
// 检查会话是否已初始化 os_log("会话未初始化,重新安全初始化", log: logger, type: .debug)
if accessorySessionWrapper == nil { safeShowDevicePicker()
os_log("会话未初始化,尝试自动初始化...", log: logger, type: .debug) return
safeShowDevicePicker() }
return
// 检查会话是否已激活
if !isSessionActivated {
os_log("会话尚未激活,等待激活后再显示选择器", log: logger, type: .debug)
onSessionActivated = { [weak self] in
self?.showDevicePicker()
} }
return
}
// 创建设备描述符和显示项
os_log("创建设备描述符", log: logger, type: .debug)
let descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB)
os_log("设置公司标识符:0x%x", log: logger, type: .debug, 0xEFAB)
// 检查和记录发现设置
if let session = self.session {
os_log("当前会话设置已激活: %{public}@", log: logger, type: .debug,
String(describing: self.isSessionActivated))
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { let currentAccessories = session.accessories
// 检查会话是否已激活 os_log("当前会话有%d个已知设备", log: logger, type: .debug, currentAccessories.count)
if !wrapper.isActivated { }
os_log("会话尚未激活,等待激活后再显示...", log: logger, type: .debug)
onSessionActivated = { [weak self] in // 创建默认图像
self?.showDevicePicker() let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
}
return // 创建显示项
} let displayItem = ASPickerDisplayItem(
name: "语音助理耳机",
os_log("找到会话包装器,准备显示选择器", log: logger, type: .info) productImage: deviceImage,
descriptor: descriptor
// 根据WWDC24示例代码创建设备描述符和显示项 )
os_log("创建设备描述符...", log: logger, type: .debug) os_log("创建显示项:语音助理耳机", log: logger, type: .debug)
let descriptor = ASDiscoveryDescriptor()
// 显示选择器
// 设置蓝牙服务UUID os_log("即将显示设备选择器", log: logger, type: .debug)
// let serviceUUID = "0000abc0-0000-1111-2222-123456789abc" // BleConst.PRIMARY_SERVICE_UUID DispatchQueue.main.async(execute: DispatchWorkItem(block: {
// os_log("📝 设置服务UUID: %{public}@", log: logger, type: .debug, serviceUUID) self.session?.showPicker(for: [displayItem]) { error in
// descriptor.bluetoothServiceUUID = CBUUID(string: serviceUUID) if let error = error {
os_log("设备选择器错误: %{public}@", log: self.logger, type: .error,
// 设置制造商ID error.localizedDescription)
os_log("设置公司ID: 0xABEF", log: logger, type: .debug) } else {
// descriptor.setValue(UInt16(0xABEF), forKey: "bluetoothCompanyIdentifier") os_log("设备选择器已显示", log: self.logger, type: .debug)
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB)
// 创建默认图像
os_log("获取设备图标...", log: logger, type: .debug)
let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
// 创建显示项
os_log("创建选择器显示项...", log: logger, type: .debug)
let displayItem = ASPickerDisplayItem(
name: "语音助理耳机",
productImage: deviceImage,
descriptor: descriptor
)
// 确保在主线程上调用
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
os_log("调用showPicker方法...", log: self.logger, type: .debug)
// 再次检查Info.plist配置
let bundle = Bundle.main
os_log("Info.plist检查:", log: self.logger, type: .debug)
if let supports = bundle.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") as? [String] {
os_log("NSAccessorySetupKitSupports: %{public}@", log: self.logger, type: .debug, supports.description)
}
if let services = bundle.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") as? [String] {
os_log("NSAccessorySetupBluetoothServices: %{public}@", log: self.logger, type: .debug, services.description)
}
if let companyIds = bundle.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") as? [String] {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers: %{public}@", log: self.logger, type: .debug, companyIds.description)
}
os_log("iOS 18环境下不检查蓝牙状态,避免创建CBCentralManager", log: self.logger, type: .debug)
// 使用标准方法显示选择器
wrapper.session.showPicker(for: [displayItem]) { [weak self] error in
guard let self = self else { return }
if let error = error {
os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
// 错误码解析
if let nsError = error as? NSError {
os_log("错误域: %{public}@, 错误码: %{public}d", log: self.logger, type: .error, nsError.domain, nsError.code)
// 解析常见错误
switch nsError.code {
case 550:
os_log("错误550: CBCentralManager冲突 - 进程中已存在使用全局权限的CBCentralManager", log: self.logger, type: .error)
case 500...599:
os_log("错误%{public}d: 可能是AccessorySetupKit内部错误", log: self.logger, type: .error, nsError.code)
default:
os_log("未知错误码", log: self.logger, type: .error)
}
if let info = nsError.userInfo as? [String: Any] {
os_log("错误信息: %{public}@", log: self.logger, type: .error, info.description)
}
}
} else {
os_log("设备选择器显示已触发", log: self.logger, type: .info)
}
}
} }
} else {
os_log("会话未初始化,无法显示设备选择器", log: logger, type: .error)
} }
} else { }))
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
}
#else
os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info)
#endif
} }
// 处理发现的设备 // 处理发现的设备
func handleDiscoveredAccessory(uuid: UUID) { func handleDiscoveredAccessory(uuid: UUID) {
#if canImport(AccessorySetupKit) && swift(>=5.9) guard let service = bleService else {
if #available(iOS 18.0, *) { return
os_log("处理UUID为%{public}@的发现设备", log: logger, type: .debug, uuid.uuidString)
guard let service = bleService as? BleService else {
os_log("BLE服务未设置或类型转换失败", log: logger, type: .error)
return
}
os_log("按照iOS 18规范,直接使用UUID调用特定连接方法", log: logger, type: .debug)
service.connectToAccessory(uuid: uuid)
} }
#endif
service.connectToAccessory(uuid: uuid)
} }
// 从指定的视图控制器显示设备选择器 // 从指定的视图控制器显示设备选择器
func showDevicePickerFromViewController(_ viewController: UIViewController) { func showDevicePickerFromViewController(_ viewController: UIViewController) {
#if canImport(AccessorySetupKit) && swift(>=5.9) safeShowDevicePickerWithUI(presentingVC: viewController)
if #available(iOS 18.0, *) {
os_log("从视图控制器显示设备选择器...", log: logger, type: .debug)
safeShowDevicePickerWithUI(presentingVC: viewController)
} else {
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
}
#else
os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info)
#endif
} }
// 带UI上下文的安全显示选择器 // 带UI上下文的安全显示选择器
private func safeShowDevicePickerWithUI(presentingVC: UIViewController) { private func safeShowDevicePickerWithUI(presentingVC: UIViewController) {
#if canImport(AccessorySetupKit) && swift(>=5.9) os_log("准备从视图控制器显示设备选择器", log: logger, type: .info)
if #available(iOS 18.0, *) { resetSession()
resetSession()
initializeSessionAndRun { [weak self, weak presentingVC] in
guard let self = self, let presentingVC = presentingVC else {
os_log("视图控制器或self已被释放", log: self?.logger ?? OSLog.default, type: .error)
return
}
initializeSessionAndRun { [weak self, weak presentingVC] in if self.session != nil && self.isSessionActivated {
guard let self = self, let presentingVC = presentingVC else { return } os_log("会话已激活,准备显示设备选择器UI", log: self.logger, type: .debug)
DispatchQueue.main.async {
if let wrapper = self.accessorySessionWrapper as? AccessorySessionWrapper { let descriptor = ASDiscoveryDescriptor()
os_log("会话已激活,准备显示选择器", log: self.logger, type: .info) descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB)
os_log("设置公司标识符:0x%x", log: self.logger, type: .debug, 0xEFAB)
// 从主线程创建和显示 os_log("描述符配置: %{public}@", log: self.logger, type: .debug,
DispatchQueue.main.async { [weak self] in String(describing: descriptor))
guard let self = self else { return }
let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
os_log("创建设备描述符...", log: self.logger, type: .debug)
let descriptor = ASDiscoveryDescriptor() let displayItem = ASPickerDisplayItem(
name: "语音助理耳机",
// 设置服务UUID和制造商ID productImage: deviceImage,
// let serviceUUID = "0000abc0-0000-1111-2222-123456789abc" descriptor: descriptor
// os_log("设置服务UUID: %{public}@", log: self.logger, type: .debug, serviceUUID) )
// descriptor.bluetoothServiceUUID = CBUUID(string: serviceUUID)
os_log("显示设备选择器UI", log: self.logger, type: .debug)
os_log("设置公司ID: 0xABEF", log: self.logger, type: .debug) self.session?.showPicker(for: [displayItem]) { error in
// descriptor.setValue(UInt16(0xABEF), forKey: "bluetoothCompanyIdentifier") os_log("设备选择器UI回调", log: self.logger, type: .debug)
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB)
// 创建默认图像
let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
// 创建显示项
let displayItem = ASPickerDisplayItem(
name: "语音助理耳机",
productImage: deviceImage,
descriptor: descriptor
)
// 检查权限,但不创建CBCentralManager
os_log("iOS 18环境下不检查蓝牙状态,避免创建CBCentralManager", log: self.logger, type: .debug)
// 显示选择器 if let error = error {
wrapper.session.showPicker(for: [displayItem]) { [weak self] error in os_log("设备选择器返回错误: %{public}@", log: self.logger, type: .error,
guard let self = self else { return } error.localizedDescription)
if let error = error { let nsError = error as NSError
os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) os_log("错误域: %{public}@, 错误码: %d", log: self.logger, type: .error,
nsError.domain, nsError.code)
// 错误码解析
if let nsError = error as? NSError {
os_log("错误域: %{public}@, 错误码: %{public}d", log: self.logger, type: .error, nsError.domain, nsError.code)
// 解析常见错误
switch nsError.code {
case 550:
os_log("错误550: CBCentralManager冲突 - 进程中已存在使用全局权限的CBCentralManager", log: self.logger, type: .error)
case 500...599:
os_log("错误%{public}d: 可能是AccessorySetupKit内部错误", log: self.logger, type: .error, nsError.code)
default:
os_log("未知错误码", log: self.logger, type: .error)
}
if let info = nsError.userInfo as? [String: Any] {
os_log("错误信息: %{public}@", log: self.logger, type: .error, info.description)
}
}
} else {
os_log("设备选择器显示已触发", log: self.logger, type: .info)
}
} }
} }
} }
} else {
os_log("会话未激活或为nil,无法显示设备选择器", log: self.logger, type: .error)
} }
} }
#endif
} }
// 检查蓝牙是否可用,不使用CBCentralManager // 检查蓝牙是否可用
func isBluetoothAvailable() -> Bool { func isBluetoothAvailable() -> Bool {
#if canImport(AccessorySetupKit) && swift(>=5.9) // 日志输出
if #available(iOS 18.0, *) { os_log("检查蓝牙可用性", log: logger, type: .debug)
// iOS 18+环境下不进行蓝牙状态检查
os_log("iOS 18环境下,跳过蓝牙状态检查", log: logger, type: .debug) // 默认返回true,适用于iOS 18环境
if bleService == nil {
os_log("BleService未设置,默认返回true", log: logger, type: .info)
return true return true
} }
#endif
// 使用AVAudioSession检测是否有蓝牙设备连接 // 尝试类型转换
let audioSession = AVAudioSession.sharedInstance() guard let _ = bleService as? BleService else {
let outputs = audioSession.currentRoute.outputs os_log("BleService类型转换失败", log: logger, type: .debug)
return true
}
// 检查是否有蓝牙输出 // 使用AVAudioSession检查蓝牙连接
for output in outputs { do {
if output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP || output.portType == .bluetoothLE { let audioSession = AVAudioSession.sharedInstance()
return true let outputs = audioSession.currentRoute.outputs
// 检查蓝牙音频输出设备
for output in outputs {
if output.portType == .bluetoothA2DP ||
output.portType == .bluetoothHFP ||
output.portType == .bluetoothLE {
os_log("发现蓝牙音频输出设备", log: logger, type: .debug)
return true
}
} }
// 未找到蓝牙设备
os_log("未找到蓝牙音频设备", log: logger, type: .debug)
} catch {
os_log("检查音频会话出错: %{public}@", log: logger, type: .error, error.localizedDescription)
} }
// 没有检测到蓝牙设备,但不确定蓝牙是否已打开 // 默认返回true,避免阻止功能
return false return true
} }
} }

650
local_plugins/ble_service/ios/Classes/BleService.swift

@ -1,6 +1,8 @@
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
import os.log import os.log
import SpeexKit // 使用杰理的SpeexKit库替代OpusSwift
import AVFoundation
@available(iOS 13.0, *) @available(iOS 13.0, *)
class BleService: NSObject { class BleService: NSObject {
@ -18,6 +20,9 @@ class BleService: NSObject {
// 数据相关回调 // 数据相关回调
func onAudioDataReceived(data: Data) func onAudioDataReceived(data: Data)
// 唤醒信号回调
func onWakeupSignalReceived()
// 设备信息相关回调 // 设备信息相关回调
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) func onDeviceInfoReceived(infoType: Int, infoData: [String: Any])
} }
@ -30,37 +35,38 @@ class BleService: NSObject {
private var audioCharacteristic: CBCharacteristic? private var audioCharacteristic: CBCharacteristic?
private var isScanning = false private var isScanning = false
private var delegate: Callback? private var callbacks = [Callback]()
private var connectionState = BleConst.STATE_DISCONNECTED private var connectionState = BleConst.STATE_DISCONNECTED
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService") private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService")
// MARK: - Opus音频处理器
private var opusProcessor: OpusAudioProcessor?
// MARK: - 初始化 // MARK: - 初始化
private override init() { private override init() {
super.init() super.init()
// iOS 18+ 下不在初始化阶段创建 CBCentralManager
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { if #available(iOS 18.0, *) {
os_log("iOS 18+: 延迟初始化CBCentralManager,等待AccessorySetupKit完成", log: logger, type: .info) // iOS 18+下不立即初始化,等待AccessorySetupKit
} else { } else {
initializeCentralManager() initializeCentralManager()
} }
#else #else
initializeCentralManager() initializeCentralManager()
#endif #endif
// 初始化Opus音频处理器
initializeOpusProcessor()
} }
// 延迟初始化中央管理器 // 延迟初始化中央管理器
private func initializeCentralManager() { private func initializeCentralManager() {
// 防止重复初始化
if centralManager != nil { if centralManager != nil {
return return
} }
os_log("正在初始化CBCentralManager", log: logger, type: .info)
// 初始化中央管理器,提供恢复标识以支持后台恢复
let options: [String: Any] = [ let options: [String: Any] = [
CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral", CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral",
CBCentralManagerOptionShowPowerAlertKey: true CBCentralManagerOptionShowPowerAlertKey: true
@ -69,13 +75,60 @@ class BleService: NSObject {
centralManager = CBCentralManager(delegate: self, queue: nil, options: options) centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
} }
// MARK: - 公共方法 // 初始化Opus处理器
private func initializeOpusProcessor() {
opusProcessor = OpusAudioProcessor()
opusProcessor?.setCallback(self)
// 立即启动Opus解码流,保持整个生命周期内活跃
startOpusDecoding()
}
// 启动Opus解码流
private func startOpusDecoding() {
let opusStarted = opusProcessor?.startDecoding(
sampleRate: 16000, // 设备端Opus编码采样率
channels: 1, // 单声道
frameSize: 640 // 40ms at 16kHz = 640 samples
) ?? false
if opusStarted {
os_log("Opus解码流启动成功,将在整个BLE生命周期内保持活跃", log: logger, type: .info)
} else {
os_log("Opus解码流启动失败", log: logger, type: .error)
}
}
// MARK: - 回调管理
/// 添加回调代理
func addCallback(_ callback: Callback) -> Bool {
if !callbacks.contains(where: { $0 === callback }) {
callbacks.append(callback)
os_log("已添加BLE回调, 当前回调数: %d", log: logger, type: .debug, callbacks.count)
return true
}
return false
}
/// 移除回调代理
func removeCallback(_ callback: Callback) -> Bool {
if let index = callbacks.firstIndex(where: { $0 === callback }) {
callbacks.remove(at: index)
os_log("已移除BLE回调, 当前回调数: %d", log: logger, type: .debug, callbacks.count)
return true
}
return false
}
/// 设置回调代理 /// 清除所有回调
func setDelegate(_ delegate: Callback) { func clearCallbacks() {
self.delegate = delegate callbacks.removeAll()
os_log("已清除所有BLE回调", log: logger, type: .debug)
} }
// MARK: - 公共方法
/// 获取中央管理器,如果未初始化则先初始化 /// 获取中央管理器,如果未初始化则先初始化
func getCentralManager() -> CBCentralManager { func getCentralManager() -> CBCentralManager {
if centralManager == nil { if centralManager == nil {
@ -86,21 +139,18 @@ class BleService: NSObject {
/// 开始扫描BLE设备 /// 开始扫描BLE设备
func startScan() { func startScan() {
// iOS 18+ 需要处理AccessorySetupKit兼容性
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { if #available(iOS 18.0, *) {
os_log("iOS 18+环境: 不进行传统扫描,请使用AccessorySetupKit配对", log: logger, type: .info) // iOS 18+不使用传统扫描
return return
} }
#endif #endif
// iOS 17及以下版本继续使用传统扫描
if centralManager == nil { if centralManager == nil {
initializeCentralManager() initializeCentralManager()
} }
guard let manager = centralManager, manager.state == .poweredOn else { guard let manager = centralManager, manager.state == .poweredOn else {
os_log("蓝牙未打开", log: logger, type: .error)
return return
} }
@ -110,13 +160,12 @@ class BleService: NSObject {
isScanning = true isScanning = true
// 扫描特定服务的设备
let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID] let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]
manager.scanForPeripherals(withServices: serviceUUIDs, options: [ manager.scanForPeripherals(withServices: serviceUUIDs, options: [
CBCentralManagerScanOptionAllowDuplicatesKey: false CBCentralManagerScanOptionAllowDuplicatesKey: false
]) ])
os_log("开始扫描BLE设备", log: logger, type: .info) os_log("开始扫描BLE设备...", log: logger, type: .info)
} }
/// 停止扫描 /// 停止扫描
@ -124,24 +173,21 @@ class BleService: NSObject {
if isScanning && centralManager != nil { if isScanning && centralManager != nil {
centralManager?.stopScan() centralManager?.stopScan()
isScanning = false isScanning = false
os_log("停止扫描BLE设备", log: logger, type: .info) os_log("停止BLE扫描", log: logger, type: .info)
} }
} }
/// 连接到指定设备 /// 连接到指定设备
func connect(to peripheral: CBPeripheral) { func connect(to peripheral: CBPeripheral) {
if connectionState == BleConst.STATE_CONNECTED || connectionState == BleConst.STATE_CONNECTING { if connectionState == BleConst.STATE_CONNECTED || connectionState == BleConst.STATE_CONNECTING {
os_log("已连接或正在连接中", log: logger, type: .info)
return return
} }
// 确保中央管理器已初始化
if centralManager == nil { if centralManager == nil {
initializeCentralManager() initializeCentralManager()
} }
guard let manager = centralManager else { guard let manager = centralManager else {
os_log("蓝牙管理器未初始化", log: logger, type: .error)
return return
} }
@ -154,19 +200,21 @@ class BleService: NSObject {
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
CBConnectPeripheralOptionNotifyOnNotificationKey: true CBConnectPeripheralOptionNotifyOnNotificationKey: true
]) ])
os_log("开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString)
} }
/// 断开当前连接 /// 断开当前连接
func disconnect() { func disconnect() {
guard let peripheral = peripheralDevice, guard let peripheral = peripheralDevice,
connectionState == BleConst.STATE_CONNECTED else { connectionState == BleConst.STATE_CONNECTED else {
os_log("未连接设备", log: logger, type: .info)
return return
} }
if centralManager != nil { if centralManager != nil {
updateConnectionState(BleConst.STATE_DISCONNECTING) updateConnectionState(BleConst.STATE_DISCONNECTING)
centralManager?.cancelPeripheralConnection(peripheral) centralManager?.cancelPeripheralConnection(peripheral)
os_log("开始断开设备连接", log: logger, type: .info)
} }
} }
@ -175,92 +223,231 @@ class BleService: NSObject {
return connectionState == BleConst.STATE_CONNECTED return connectionState == BleConst.STATE_CONNECTED
} }
// MARK: - 获取设备信息 // MARK: - 回调通知方法
/// 通知所有回调扫描结果
private func notifyScanResult(devices: [[String: Any]]) {
for callback in callbacks {
callback.onScanResult(devices: devices)
}
}
/// 通知所有回调连接状态变化
private func notifyConnectionStateChanged(state: Int) {
for callback in callbacks {
callback.onConnectionStateChanged(state: state)
}
}
/// 通知所有回调接收到音频数据
private func notifyAudioDataReceived(data: Data) {
for callback in callbacks {
callback.onAudioDataReceived(data: data)
}
}
/// 通知所有回调接收到唤醒信号
private func notifyWakeupSignalReceived() {
for callback in callbacks {
callback.onWakeupSignalReceived()
}
}
/// 通知所有回调接收到设备信息
private func notifyDeviceInfoReceived(infoType: Int, infoData: [String: Any]) {
for callback in callbacks {
callback.onDeviceInfoReceived(infoType: infoType, infoData: infoData)
}
}
// MARK: - 设备信息和控制命令
/// 获取设备版本号 /// 获取设备版本号
func getDeviceVersion() -> Bool { func getDeviceVersion() -> Bool {
return sendCommand(BleConst.CMD_GET_VERSION) os_log("获取耳机版本号...", log: logger, type: .info)
let paramData = Data([0xFF])
return sendCommand(BleConst.CMD_GET_VERSION, data: paramData)
} }
/// 获取产品ID /// 获取产品ID
func getProductId() -> Bool { func getProductId() -> Bool {
return sendCommand(BleConst.CMD_GET_PRODUCT_ID) os_log("获取耳机产品ID...", log: logger, type: .info)
let paramData = Data([0xFF])
return sendCommand(BleConst.CMD_GET_PRODUCT_ID, data: paramData)
}
/// 获取颜色ID
func getColorId() -> Bool {
os_log("获取耳机颜色ID...", log: logger, type: .info)
let paramData = Data([0xFF])
return sendCommand(BleConst.CMD_GET_COLOR_ID, data: paramData)
} }
/// 获取电池信息 /// 获取电池信息
func getBatteryInfo() -> Bool { func getBatteryInfo() -> Bool {
return sendCommand(BleConst.CMD_GET_BATTERY_INFO) os_log("获取耳机电量信息...", log: logger, type: .info)
let paramData = Data([0xFF])
return sendCommand(BleConst.CMD_GET_BATTERY_INFO, data: paramData)
} }
// MARK: - 编解码控制 // MARK: - 编解码控制
/// 打开编码器 /// 打开编码器
func openEncoder() -> Bool { func openEncoder() -> Bool {
return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_ENCODE_ON) os_log("打开编码(设备侧)...", log: logger, type: .info)
let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
} }
/// 打开解码器 /// 打开解码器
func openDecoder() -> Bool { func openDecoder() -> Bool {
return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_DECODE_ON) os_log("打开解码...", log: logger, type: .info)
// 确保Opus解码器处于活跃状态(如果不是则启动)
if opusProcessor?.isDecoding() != true {
startOpusDecoding()
}
// 发送命令通知设备开始解码
let paramData = Data([BleConst.CODEC_CONTROL_DECODE_ON, BleConst.AUDIO_CHANNEL_RIGHT])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
} }
/// 打开A2DP解码器 /// 打开A2DP解码器
func openA2DPDecoder() -> Bool { func openA2DPDecoder() -> Bool {
return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_A2DP_PLAY) os_log("打开A2DP解码...", log: logger, type: .info)
let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_RIGHT])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
} }
/// 关闭编解码器 /// 关闭编解码器
func closeCodec() -> Bool { func closeCodec() -> Bool {
return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_CLOSE) os_log("关闭编解码...", log: logger, type: .info)
// 不停止Opus解码流,只发送命令通知设备关闭编解码
let paramData = Data([BleConst.CODEC_CONTROL_CLOSE, BleConst.AUDIO_CHANNEL_RIGHT])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
} }
/// 播放A2DP /// 播放A2DP
func playA2DP() -> Bool { func playA2DP() -> Bool {
os_log("切换至A2DP播放模式...", log: logger, type: .info)
return sendCommand(BleConst.CMD_PLAY_A2DP) return sendCommand(BleConst.CMD_PLAY_A2DP)
} }
/// 音量增大
func volumeUp() -> Bool {
os_log("设备音量增加...", log: logger, type: .info)
return sendCommand(BleConst.CMD_VOLUME_UP)
}
/// 音量减小
func volumeDown() -> Bool {
os_log("设备音量减小...", log: logger, type: .info)
return sendCommand(BleConst.CMD_VOLUME_DOWN)
}
/// 设置声道模式
func setChannelMode(_ channelMode: UInt8) -> Bool {
let modeDesc: String
switch channelMode {
case BleConst.AUDIO_CHANNEL_LEFT:
modeDesc = "左声道"
case BleConst.AUDIO_CHANNEL_RIGHT:
modeDesc = "右声道"
case BleConst.AUDIO_CHANNEL_STEREO:
modeDesc = "立体声"
default:
os_log("无效的声道模式: %d", log: logger, type: .error, channelMode)
return false
}
os_log("设置%@模式...", log: logger, type: .info, modeDesc)
let paramData = Data([0xA1, channelMode])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
}
// MARK: - 私有方法 // MARK: - 私有方法
/// 更新连接状态并通知代理 /// 更新连接状态并通知回调
private func updateConnectionState(_ state: Int) { private func updateConnectionState(_ state: Int) {
connectionState = state connectionState = state
delegate?.onConnectionStateChanged(state: state) notifyConnectionStateChanged(state: state)
} }
/// 向设备发送命令(无参数) /// 向设备发送命令
private func sendCommand(_ cmd: UInt8) -> Bool { private func sendCommand(_ cmd: UInt8, data: Data = Data()) -> Bool {
guard let characteristic = writeCharacteristic, guard let characteristic = writeCharacteristic,
connectionState == BleConst.STATE_CONNECTED else { connectionState == BleConst.STATE_CONNECTED else {
os_log("设备未连接或写特征未找到", log: logger, type: .error) os_log("发送命令失败: 设备未连接", log: logger, type: .error)
return false return false
} }
// 构造帧: 帧头 + 命令ID + 长度(0) + 校验和 do {
let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x00, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x00]) // 命令数据长度
let cmdLength = UInt8(data.count)
peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse)
return true // 准备完整的命令包:帧头 + 命令ID + 长度 + 数据 + CRC
} var cmdPacket = Data()
cmdPacket.append(BleConst.APP_REQUEST_HEADER) // 帧头
/// 向设备发送带参数的命令 cmdPacket.append(cmd) // 命令类型
private func sendCommandWithParam(_ cmd: UInt8, param: UInt8) -> Bool { cmdPacket.append(cmdLength) // 命令长度
guard let characteristic = writeCharacteristic,
connectionState == BleConst.STATE_CONNECTED else { // 添加命令数据(如果有)
os_log("设备未连接或写特征未找到", log: logger, type: .error) if !data.isEmpty {
cmdPacket.append(data)
}
// 计算CRC校验 - 对除CRC外的所有字节
let crc = calculateCRC8(data: cmdPacket)
cmdPacket.append(crc)
// 记录命令详情
var dataHexString = ""
if !data.isEmpty {
dataHexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ")
}
os_log("发送命令: CMD=0x%02X, 长度=%d, 数据=[%@], CRC=0x%02X",
log: logger, type: .debug, cmd, cmdLength, dataHexString, crc)
// 发送数据
peripheralDevice?.writeValue(cmdPacket, for: characteristic, type: .withResponse)
return true
} catch {
os_log("发送命令异常: %@", log: logger, type: .error, error.localizedDescription)
return false return false
} }
}
/// 计算CRC8/MAXIM校验值
private func calculateCRC8(data: Data) -> UInt8 {
var crc: UInt8 = 0
let polynomial: UInt8 = 0x31
// 构造帧: 帧头 + 命令ID + 长度(1) + 参数 + 校验和 for byte in data {
let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x01, param, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x01 ^ param]) crc ^= byte
for _ in 0..<8 {
if (crc & 0x80) != 0 {
crc = ((crc << 1) ^ polynomial)
} else {
crc = (crc << 1)
}
}
}
peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse) return crc
return true }
/// 处理收到的音频数据
private func processAudioData(_ data: Data) {
// 使用Opus处理器处理音频数据
opusProcessor?.processAudioData(data)
} }
/// 处理接收到的响应数据 /// 处理接收到的响应数据
private func handleResponseData(_ data: Data) { private func handleResponseData(_ data: Data) {
guard data.count >= 4 else { guard data.count >= 4 else {
os_log("接收数据格式错误", log: logger, type: .error) os_log("响应数据格式错误:数据长度过短", log: logger, type: .error)
return return
} }
@ -278,54 +465,300 @@ class BleService: NSObject {
return return
} }
// 记录响应详情
var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ")
os_log("收到设备响应: %@", log: logger, type: .debug, hexString)
// 检查数据长度
if data.count != Int(length) + 4 { // 帧头 + 命令 + 长度 + 数据 + CRC
os_log("设备响应数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count)
return
}
// 计算CRC校验 - 不包括最后一个字节(CRC)
let payload = data.prefix(data.count - 1)
let calculatedCrc = calculateCRC8(data: payload)
let receivedCrc = data[data.count - 1]
if calculatedCrc != receivedCrc {
os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc)
return
}
// 提取响应数据
let responseData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data()
// 根据命令类型处理设备响应 // 根据命令类型处理设备响应
switch cmd { switch cmd {
case BleConst.CMD_GET_VERSION: case BleConst.CMD_GET_VERSION:
if length > 0 && data.count >= 4 + Int(length) { var resultMap: [String: Any] = [
let versionData = data.subdata(in: 3..<(3 + Int(length))) "success": true,
if let versionStr = String(data: versionData, encoding: .utf8) { "command": "versionInfo",
delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_VERSION), "rawData": responseData
infoData: ["version": versionStr]) ]
if !responseData.isEmpty {
if let versionStr = String(data: responseData, encoding: .utf8) {
resultMap["version"] = versionStr
os_log("接收到目标版本号: %@", log: logger, type: .info, versionStr)
} else {
// 如果无法解析为字符串,以点分隔的数字表示版本
let versionNums = responseData
.map { $0 }
.filter { $0 != 0xFF } // 过滤掉无效字节
.map { String($0) }
.joined(separator: ".")
resultMap["version"] = versionNums
os_log("接收到目标版本号(数字格式): %@", log: logger, type: .info, versionNums)
} }
} }
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
case BleConst.CMD_GET_PRODUCT_ID: case BleConst.CMD_GET_PRODUCT_ID:
if length >= 2 && data.count >= 5 { var resultMap: [String: Any] = [
let productId = UInt16(data[3]) << 8 | UInt16(data[4]) "success": true,
delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_PRODUCT_ID), "command": "productId",
infoData: ["productId": productId]) "rawData": responseData
]
if responseData.count >= 2 {
let productId = UInt16(responseData[0]) << 8 | UInt16(responseData[1])
resultMap["productId"] = productId
os_log("接收到产品ID: %d", log: logger, type: .info, productId)
} else if !responseData.isEmpty {
// 单字节产品ID
let productId = UInt16(responseData[0])
resultMap["productId"] = productId
os_log("接收到产品ID: %d", log: logger, type: .info, productId)
} }
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
case BleConst.CMD_GET_COLOR_ID:
var resultMap: [String: Any] = [
"success": true,
"command": "colorInfo",
"rawData": responseData
]
if !responseData.isEmpty {
let colorId = Int(responseData[0])
resultMap["colorId"] = colorId
os_log("接收到耳机颜色ID: %d", log: logger, type: .info, colorId)
}
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
case BleConst.CMD_GET_BATTERY_INFO: case BleConst.CMD_GET_BATTERY_INFO:
if length >= 1 && data.count >= 4 { var resultMap: [String: Any] = [
let batteryLevel = Int(data[3]) "success": true,
delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_BATTERY_INFO), "command": "batteryInfo",
infoData: ["batteryLevel": batteryLevel]) "rawData": responseData
]
if responseData.count >= 3 {
// 处理电池信息 - 左耳、右耳、充电仓
let leftEarBattery = Int(responseData[0] & 0x7F)
let leftEarCharging = (responseData[0] & 0x80) != 0
let rightEarBattery = Int(responseData[1] & 0x7F)
let rightEarCharging = (responseData[1] & 0x80) != 0
let caseBattery = Int(responseData[2] & 0x7F)
let caseCharging = (responseData[2] & 0x80) != 0
// 添加到结果Map
resultMap["leftEarBattery"] = leftEarBattery
resultMap["leftEarCharging"] = leftEarCharging
resultMap["rightEarBattery"] = rightEarBattery
resultMap["rightEarCharging"] = rightEarCharging
resultMap["caseBattery"] = caseBattery
resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
os_log("接收到电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)",
log: logger, type: .info,
leftEarBattery, leftEarCharging ? "充电中" : "未充电",
rightEarBattery, rightEarCharging ? "充电中" : "未充电",
caseBattery, caseCharging ? "充电中" : "未充电")
} else if !responseData.isEmpty {
// 兼容单字节电量
let batteryLevel = Int(responseData[0])
resultMap["batteryLevel"] = batteryLevel
os_log("接收到电量信息(简化): %d%%", log: logger, type: .info, batteryLevel)
} }
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
case BleConst.CMD_CONTROL_CODEC:
var resultMap: [String: Any] = [
"success": true,
"command": Int(cmd),
"rawData": responseData
]
if responseData.count >= 2 {
let codecStatus = Int(responseData[0])
let channelMode = Int(responseData[1])
// 解码状态描述
let statusDesc: String
switch codecStatus {
case Int(BleConst.CODEC_CONTROL_CLOSE):
statusDesc = "已关闭编解码"
case Int(BleConst.CODEC_CONTROL_DECODE_ON):
statusDesc = "已打开解码"
case Int(BleConst.CODEC_CONTROL_A2DP_PLAY):
statusDesc = "A2DP播放模式"
case Int(BleConst.CODEC_CONTROL_ENCODE_ON):
statusDesc = "已打开编码"
default:
statusDesc = "未知状态(\(codecStatus))"
}
// 声道模式描述
let channelDesc: String
switch channelMode {
case Int(BleConst.AUDIO_CHANNEL_LEFT):
channelDesc = "左声道"
case Int(BleConst.AUDIO_CHANNEL_RIGHT):
channelDesc = "右声道"
case Int(BleConst.AUDIO_CHANNEL_STEREO):
channelDesc = "立体声"
default:
channelDesc = "未知声道(\(channelMode))"
}
resultMap["codecStatus"] = codecStatus
resultMap["channelMode"] = channelMode
resultMap["statusDesc"] = statusDesc
resultMap["channelDesc"] = channelDesc
os_log("编解码控制响应: %@, %@", log: logger, type: .info, statusDesc, channelDesc)
}
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
default: default:
os_log("未知的响应命令: %{public}d", log: logger, type: .debug, cmd) // 对于未知命令,返回原始数据
let resultMap: [String: Any] = [
"success": true,
"command": Int(cmd),
"rawData": responseData,
"isUnknown": true
]
os_log("收到未知命令响应: 0x%02X", log: logger, type: .debug, cmd)
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
} }
} }
/// 处理设备主动上报的通知数据 /// 处理设备主动上报的通知数据
private func handleNotificationData(_ data: Data) { private func handleNotificationData(_ data: Data) {
guard data.count >= 4 else { return } guard data.count >= 4 else {
os_log("设备主动上报数据格式错误:数据长度过短", log: logger, type: .error)
return
}
let cmd = data[1] let header = data[0] // 0xCC
let infoType = data[1]
let length = data[2] let length = data[2]
switch cmd { var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ")
os_log("收到设备主动上报: %@", log: logger, type: .debug, hexString)
// 检查数据长度
if data.count != Int(length) + 4 { // 帧头 + 类型 + 长度 + 数据 + CRC
os_log("设备主动上报数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count)
return
}
// 计算CRC校验 - 不包括最后一个字节(CRC)
let payload = data.prefix(data.count - 1)
let calculatedCrc = calculateCRC8(data: payload)
let receivedCrc = data[data.count - 1]
if calculatedCrc != receivedCrc {
os_log("设备主动上报CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc)
return
}
// 提取上报数据
let notifyData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data()
// 根据上报类型处理
switch infoType {
case BleConst.CMD_GET_BATTERY_INFO:
var resultMap: [String: Any] = [
"success": true,
"command": Int(infoType),
"rawData": notifyData,
"isNotification": true // 标记为主动上报
]
if notifyData.count >= 3 {
// 处理电池信息 - 左耳、右耳、充电仓
let leftEarBattery = Int(notifyData[0] & 0x7F)
let leftEarCharging = (notifyData[0] & 0x80) != 0
let rightEarBattery = Int(notifyData[1] & 0x7F)
let rightEarCharging = (notifyData[1] & 0x80) != 0
let caseBattery = Int(notifyData[2] & 0x7F)
let caseCharging = (notifyData[2] & 0x80) != 0
// 添加到结果Map
resultMap["leftEarBattery"] = leftEarBattery
resultMap["leftEarCharging"] = leftEarCharging
resultMap["rightEarBattery"] = rightEarBattery
resultMap["rightEarCharging"] = rightEarCharging
resultMap["caseBattery"] = caseBattery
resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
os_log("收到主动上报电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)",
log: logger, type: .info,
leftEarBattery, leftEarCharging ? "充电中" : "未充电",
rightEarBattery, rightEarCharging ? "充电中" : "未充电",
caseBattery, caseCharging ? "充电中" : "未充电")
}
notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap)
case BleConst.CMD_WAKE_UP: case BleConst.CMD_WAKE_UP:
// 处理唤醒指令 os_log("收到唤醒指令", log: logger, type: .info)
os_log("收到设备唤醒指令", log: logger, type: .info) // 接收到唤醒信号,打开编码器 (设备侧)
// 可以在这里添加唤醒回调 openEncoder()
notifyWakeupSignalReceived()
default: default:
os_log("未知的通知命令: %{public}d", log: logger, type: .debug, cmd) // 对于未知上报类型,返回原始数据
let resultMap: [String: Any] = [
"success": true,
"command": Int(infoType),
"rawData": notifyData,
"isUnknown": true,
"isNotification": true // 标记为主动上报
]
os_log("收到未知主动上报类型: 0x%02X", log: logger, type: .debug, infoType)
notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap)
} }
} }
// MARK: - 资源清理
/// 清理所有资源
func cleanup() {
stopScan()
disconnect()
// 停止Opus解码
opusProcessor?.cleanup()
opusProcessor = nil
os_log("BleService资源已清理", log: logger, type: .debug)
}
// MARK: - AccessorySetupKit // MARK: - AccessorySetupKit
/// 在接收到AccessorySetupKit的accessoryAdded事件后调用 /// 在接收到AccessorySetupKit的accessoryAdded事件后调用
@ -337,11 +770,7 @@ class BleService: NSObject {
initializeCentralManager() initializeCentralManager()
} }
os_log("通过AccessorySetupKit连接设备: %{public}@", log: logger, type: .info, uuid.uuidString)
// 检查centralManager状态
guard let manager = centralManager, manager.state == .poweredOn else { guard let manager = centralManager, manager.state == .poweredOn else {
os_log("CBCentralManager尚未准备好或状态不是poweredOn: %d", log: logger, type: .error, centralManager?.state.rawValue ?? -1)
return return
} }
@ -349,43 +778,40 @@ class BleService: NSObject {
let peripherals = manager.retrievePeripherals(withIdentifiers: [uuid]) let peripherals = manager.retrievePeripherals(withIdentifiers: [uuid])
if let peripheral = peripherals.first { if let peripheral = peripherals.first {
os_log("找到AccessorySetupKit配对的设备,尝试连接", log: logger, type: .info)
connect(to: peripheral) connect(to: peripheral)
} else {
os_log("未找到对应UUID的设备: %{public}@", log: logger, type: .error, uuid.uuidString)
} }
} }
#endif #endif
} }
} }
// MARK: - OpusAudioProcessor.AudioDataCallback
@available(iOS 13.0, *)
extension BleService: OpusAudioProcessor.AudioDataCallback {
/// 接收到解码后的PCM音频数据
func onAudioDataReceived(data: Data) {
notifyAudioDataReceived(data: data)
}
}
// MARK: - CBCentralManagerDelegate // MARK: - CBCentralManagerDelegate
@available(iOS 13.0, *) @available(iOS 13.0, *)
extension BleService: CBCentralManagerDelegate { extension BleService: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) { func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state { switch central.state {
case .poweredOn: case .poweredOn:
os_log("蓝牙已开启", log: logger, type: .info)
// 蓝牙已开启,可以开始扫描 // 蓝牙已开启,可以开始扫描
break
case .poweredOff: case .poweredOff:
os_log("蓝牙已关闭", log: logger, type: .info)
updateConnectionState(BleConst.STATE_DISCONNECTED) updateConnectionState(BleConst.STATE_DISCONNECTED)
case .resetting: case .resetting, .unauthorized, .unsupported, .unknown:
os_log("蓝牙正在重置", log: logger, type: .info) // 其他状态不处理
break
case .unauthorized:
os_log("蓝牙未授权", log: logger, type: .error)
case .unsupported:
os_log("设备不支持蓝牙", log: logger, type: .error)
case .unknown:
os_log("蓝牙状态未知", log: logger, type: .info)
@unknown default: @unknown default:
os_log("蓝牙状态未定义", log: logger, type: .info) break
} }
} }
@ -407,33 +833,30 @@ extension BleService: CBCentralManagerDelegate {
} }
// 报告给回调 // 报告给回调
delegate?.onScanResult(devices: [deviceInfo]) notifyScanResult(devices: [deviceInfo])
} }
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
os_log("已连接到设备: %{public}@", log: logger, type: .info, peripheral.name ?? "Unknown")
updateConnectionState(BleConst.STATE_CONNECTED) updateConnectionState(BleConst.STATE_CONNECTED)
// 查找服务 // 查找服务
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID])
// 确保Opus解码器处于活跃状态
if opusProcessor?.isDecoding() != true {
startOpusDecoding()
}
} }
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
os_log("连接失败: %{public}@", log: logger, type: .error, error?.localizedDescription ?? "Unknown error")
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
} }
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
os_log("设备断开连接: %{public}@", log: logger, type: .info, error?.localizedDescription ?? "Normal disconnection")
updateConnectionState(BleConst.STATE_DISCONNECTED) updateConnectionState(BleConst.STATE_DISCONNECTED)
} }
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
os_log("恢复先前的连接状态", log: logger, type: .info)
// 从恢复状态中获取之前连接的外设 // 从恢复状态中获取之前连接的外设
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral], if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral],
let peripheral = peripherals.first { let peripheral = peripherals.first {
@ -458,15 +881,12 @@ extension BleService: CBCentralManagerDelegate {
extension BleService: CBPeripheralDelegate { extension BleService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let error = error { if let error = error {
os_log("服务发现错误: %{public}@", log: logger, type: .error, error.localizedDescription)
return return
} }
guard let services = peripheral.services else { return } guard let services = peripheral.services else { return }
for service in services { for service in services {
os_log("发现服务: %{public}@", log: logger, type: .debug, service.uuid.uuidString)
if service.uuid == BleConst.PRIMARY_SERVICE_UUID { if service.uuid == BleConst.PRIMARY_SERVICE_UUID {
// 主服务,发现写入和通知特征 // 主服务,发现写入和通知特征
peripheral.discoverCharacteristics([BleConst.WRITE_CHAR_UUID, BleConst.NOTIFY_CHAR_UUID], for: service) peripheral.discoverCharacteristics([BleConst.WRITE_CHAR_UUID, BleConst.NOTIFY_CHAR_UUID], for: service)
@ -479,15 +899,12 @@ extension BleService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error { if let error = error {
os_log("特征发现错误: %{public}@", log: logger, type: .error, error.localizedDescription)
return return
} }
guard let characteristics = service.characteristics else { return } guard let characteristics = service.characteristics else { return }
for characteristic in characteristics { for characteristic in characteristics {
os_log("发现特征: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString)
if characteristic.uuid == BleConst.WRITE_CHAR_UUID { if characteristic.uuid == BleConst.WRITE_CHAR_UUID {
writeCharacteristic = characteristic writeCharacteristic = characteristic
} else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID {
@ -504,7 +921,6 @@ extension BleService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { if let error = error {
os_log("接收数据错误: %{public}@", log: logger, type: .error, error.localizedDescription)
return return
} }
@ -515,23 +931,15 @@ extension BleService: CBPeripheralDelegate {
handleResponseData(data) handleResponseData(data)
} else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID {
// 处理音频数据 // 处理音频数据
delegate?.onAudioDataReceived(data: data) processAudioData(data)
} }
} }
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { // 写入完成回调,不需要特殊处理
os_log("写入数据错误: %{public}@", log: logger, type: .error, error.localizedDescription)
}
} }
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { // 通知状态更新回调,不需要特殊处理
os_log("订阅通知错误: %{public}@", log: logger, type: .error, error.localizedDescription)
return
}
os_log("特征 %{public}@ 的通知状态更新为 %{public}d", log: logger, type: .debug,
characteristic.uuid.uuidString, characteristic.isNotifying ? 1 : 0)
} }
} }

128
local_plugins/ble_service/ios/Classes/OpusAudioProcessor.swift

@ -0,0 +1,128 @@
import Foundation
import SpeexKit
import os.log
/// 基于杰理SpeexKit的Opus音频处理器
@available(iOS 13.0, *)
class OpusAudioProcessor: NSObject {
// MARK: - 回调代理协议
protocol AudioDataCallback: AnyObject {
/// 接收到解码后的PCM音频数据
func onAudioDataReceived(data: Data)
}
// MARK: - 属性
private var isDecodingActive = false
private weak var callback: AudioDataCallback?
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "OpusAudioProcessor")
// MARK: - 初始化
override init() {
super.init()
// 注册通知 - 这是SDK设计要求的
NotificationCenter.default.addObserver(
self,
selector: #selector(onOpusDecodedData(_:)),
name: NSNotification.Name(rawValue: kOPUS_DECODE_DATA),
object: nil
)
// 启用日志
OpusUnit.opusIsLog(true)
}
deinit {
stopDecoding()
NotificationCenter.default.removeObserver(self)
}
// MARK: - 通知回调
@objc private func onOpusDecodedData(_ notification: Notification) {
guard let pcmData = notification.object as? Data else {
os_log("解码回调收到无效数据", log: logger, type: .error)
return
}
// 传递给回调
DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: pcmData)
}
}
// MARK: - 公共接口
/// 设置回调
func setCallback(_ callback: AudioDataCallback?) {
self.callback = callback
}
/// 开始Opus解码
func startDecoding(sampleRate: Int32 = 16000, channels: Int32 = 1, frameSize: Int32 = 640) -> Bool {
// 如果已经在解码,直接返回成功
if isDecodingActive {
os_log("Opus解码器已经在运行", log: logger, type: .debug)
return true
}
// 设置音频参数
OpusUnit.opusSetSampleRate(Int32(sampleRate), kbps: 16000, channels: Int32(channels))
// 必须在后台线程启动解码器(SDK要求)
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
// 启动解码器
let result = OpusUnit.opusDecoderRun()
if result == 0 {
os_log("Opus解码器启动成功: 采样率=%d Hz", log: self.logger, type: .info, sampleRate)
self.isDecodingActive = true
} else {
os_log("Opus解码器启动失败: 错误=%d", log: self.logger, type: .error, result)
}
}
return true
}
/// 停止Opus解码
func stopDecoding() -> Bool {
if !isDecodingActive {
return false
}
// 必须在后台线程停止解码器
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
let result = OpusUnit.opusDecoderStop()
os_log("Opus解码器停止: %d", log: self.logger, type: .info, result)
self.isDecodingActive = false
}
return true
}
/// 处理Opus编码数据
func processAudioData(_ data: Data) {
if !isDecodingActive {
os_log("Opus解码器未启动", log: logger, type: .error)
return
}
// 将Opus数据送入解码器
OpusUnit.opusWrite(data)
// 解码结果将通过通知回调给onOpusDecodedData方法
}
/// 判断解码器是否运行中
func isDecoding() -> Bool {
return isDecodingActive
}
/// 清理资源
func cleanup() {
stopDecoding()
}
}

138
local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift

@ -22,7 +22,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { if #available(iOS 18.0, *) {
// iOS 18环境下,先只初始化AccessorySetup // iOS 18环境下,先只初始化AccessorySetup
os_log("iOS 18环境: 延迟BleService初始化,先初始化AccessorySetup", log: logger, type: .info)
let bleAccessorySetup = BleAccessorySetup.shared let bleAccessorySetup = BleAccessorySetup.shared
// 在accessoryAdded回调中延迟设置BleService // 在accessoryAdded回调中延迟设置BleService
@ -48,10 +47,8 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
// 确保所有服务都已初始化 // 确保所有服务都已初始化
private func ensureServicesInitialized() { private func ensureServicesInitialized() {
if !servicesInitialized { if !servicesInitialized {
os_log("按需初始化BleService和相关服务", log: logger, type: .info)
// 设置BLE服务回调 // 设置BLE服务回调
BleService.shared.setDelegate(self) BleService.shared.addCallback(self)
// 设置BleService到AccessorySetup // 设置BleService到AccessorySetup
BleAccessorySetup.shared.setBleService(BleService.shared) BleAccessorySetup.shared.setBleService(BleService.shared)
@ -62,10 +59,8 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
// 初始化所有服务(iOS 17及以下版本的标准行为) // 初始化所有服务(iOS 17及以下版本的标准行为)
private func initializeAllServices() { private func initializeAllServices() {
os_log("标准初始化所有服务", log: logger, type: .info)
// 设置BLE服务回调 // 设置BLE服务回调
BleService.shared.setDelegate(self) BleService.shared.addCallback(self)
// 初始化AccessorySetup // 初始化AccessorySetup
BleAccessorySetup.shared.setBleService(BleService.shared) BleAccessorySetup.shared.setBleService(BleService.shared)
@ -92,24 +87,14 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
} }
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
// 检查是否需要初始化服务
checkAndInitializeServices()
switch call.method { switch call.method {
case "isConnected": case "isConnected":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.isConnected()) result(BleService.shared.isConnected())
case "getBatteryLevel": case "getBatteryLevel":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
// 获取电池信息,通过回调返回结果 // 获取电池信息,通过回调返回结果
if BleService.shared.getBatteryInfo() { if BleService.shared.getBatteryInfo() {
result(0) // 通过回调返回实际值 result(0) // 通过回调返回实际值
@ -120,7 +105,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
} }
case "isCompanionSupported": case "isCompanionSupported":
// iOS 18+ 支持 AccessorySetupKit
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { if #available(iOS 18.0, *) {
result(true) result(true)
@ -135,115 +119,44 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
// iOS上禁用AccessorySetupKit,始终返回无配对设备 // iOS上禁用AccessorySetupKit,始终返回无配对设备
result(nil) result(nil)
case "registerBackgroundScan": case "registerBackgroundScan", "unregisterBackgroundScan", "clearAssociations":
// iOS上禁用,直接返回成功以避免Flutter端报错
result(true)
case "unregisterBackgroundScan":
// iOS上禁用,直接返回成功以避免Flutter端报错
result(true)
case "clearAssociations":
// iOS上禁用,直接返回成功以避免Flutter端报错 // iOS上禁用,直接返回成功以避免Flutter端报错
result(true) result(true)
case "getDeviceVersion": case "getDeviceVersion":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.getDeviceVersion()) result(BleService.shared.getDeviceVersion())
case "getProductId": case "getProductId":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.getProductId()) result(BleService.shared.getProductId())
case "getBatteryInfo": case "getBatteryInfo":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.getBatteryInfo()) result(BleService.shared.getBatteryInfo())
case "openEncoder": case "openEncoder":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.openEncoder()) result(BleService.shared.openEncoder())
case "openDecoder": case "openDecoder":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.openDecoder()) result(BleService.shared.openDecoder())
case "openA2DPDecoder": case "openA2DPDecoder":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.openA2DPDecoder()) result(BleService.shared.openA2DPDecoder())
case "closeCodec": case "closeCodec":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.closeCodec()) result(BleService.shared.closeCodec())
case "playA2DP": case "playA2DP":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.playA2DP()) result(BleService.shared.playA2DP())
// iOS 18+ AccessorySetupKit相关方法 // iOS 18+ AccessorySetupKit相关方法
case "startDeviceAssociation": case "startDeviceAssociation":
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { if #available(iOS 18.0, *) {
// 从主线程调用 showDevicePicker()
DispatchQueue.main.async {
// 获取当前活跃的视图控制器
if let rootVC = UIApplication.shared.windows.first?.rootViewController {
let topVC = self.getTopViewController(rootVC)
os_log("尝试从顶层视图控制器显示设备选择器: %{public}@", log: self.logger, type: .debug, String(describing: topVC))
// 调用带UI上下文的方法
BleAccessorySetup.shared.showDevicePickerFromViewController(topVC)
} else {
os_log("无法获取顶层视图控制器,使用安全方法", log: self.logger, type: .error)
BleAccessorySetup.shared.safeShowDevicePicker()
}
}
result(true) result(true)
} else { } else {
os_log("iOS版本低于18.0,不支持AccessorySetupKit", log: self.logger, type: .info)
result(true) result(true)
} }
#else #else
os_log("环境不支持AccessorySetupKit,模拟成功以继续流程", log: self.logger, type: .info)
result(true) result(true)
#endif #endif
@ -253,9 +166,35 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
} }
// MARK: - 私有方法 // MARK: - 私有方法
// 检查并按需初始化服务
private func checkAndInitializeServices() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
}
// 显示设备选择器
private func showDevicePicker() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
// 从主线程调用
DispatchQueue.main.async {
// 获取当前活跃的视图控制器
if let rootVC = UIApplication.shared.windows.first?.rootViewController {
let topVC = self.getTopViewController(rootVC)
BleAccessorySetup.shared.showDevicePickerFromViewController(topVC)
} else {
BleAccessorySetup.shared.safeShowDevicePicker()
}
}
}
#endif
}
// AccessorySetupKit发现设备的处理 // AccessorySetupKit发现设备的处理
private func handleDiscoveredDevice(uuid: UUID) { private func handleDiscoveredDevice(uuid: UUID) {
// 通过AccessorySetupKit处理发现的设备
BleAccessorySetup.shared.handleDiscoveredAccessory(uuid: uuid) BleAccessorySetup.shared.handleDiscoveredAccessory(uuid: uuid)
} }
@ -371,6 +310,15 @@ extension SwiftBleServicePlugin: BleService.Callback {
sendEvent(dataEventSink, data: dataMap, errorPrefix: "发送音频数据异常") sendEvent(dataEventSink, data: dataMap, errorPrefix: "发送音频数据异常")
} }
func onWakeupSignalReceived() {
let wakeupMap: [String: Any] = [
"type": "wakeupSignal",
"timestamp": Date().timeIntervalSince1970 * 1000
]
sendEvent(statusEventSink, data: wakeupMap, errorPrefix: "发送唤醒信号异常")
}
func onDeviceInfoReceived(infoType: Int, infoData: [String : Any]) { func onDeviceInfoReceived(infoType: Int, infoData: [String : Any]) {
var dataMap: [String: Any] = [ var dataMap: [String: Any] = [
"type": "deviceInfo", "type": "deviceInfo",

92
local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/OpusUnit.h

@ -0,0 +1,92 @@
//
// OpusUnit.h
// QCY_Demo
//
// Created by 杰理科技 on 2021/7/5.
// Copyright © 2021 杰理科技. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* 通知:流式编码回调Opus数据
* 通知数据类型:NSData(Opus)
*/
extern NSString *kOPUS_ENCODE_DATA;
/**
* 通知:流式解码回调PCM数据
* 通知数据类型:NSData(PCM)
*/
extern NSString *kOPUS_DECODE_DATA;
@interface OpusUnit : NSObject
+ (void)opusIsLog:(BOOL)log;
#pragma mark - Opus参数
/**
* 设置参数
* @param rate 采样率 例如:48000、24000、16000、12000、8000(默认 16000)
* @param kbps 比特率 例如:16000、32000、64000(默认 16000)
* @param channel 声道 例如:1、2 (默认 1)
*/
+(void)opusSetSampleRate:(int)rate Kbps:(int)kbps Channels:(int)channel;
#pragma mark - Opus解码
/**
* 直接【opus文件】转换成【pcm文件】
* @param path_opus opus文件路径
* @param path_pcm pcm文件路径
*/
+ (int)opusDecodeOPUS:(NSString *)path_opus PCM:(NSString *)path_pcm;
/**
* 流式解码【开启】
*/
+ (int)opusDecoderRun;
/**
* 输入Opus的数据
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据
* 通知数据类型:NSNotification.object => NSData(PCM格式)
* @param data opus数据流(长度:1024)
*/
+ (void)opusWriteData:(NSData*)data;
/**
* 流式解码【关闭】
*/
+ (int)opusDecoderStop;
#pragma mark - Opus编码
/**
* 【pcm文件】转换成【opus文件】
*/
+ (int)opusEncodePCM:(NSString *)path_pcm OPUS:(NSString *)path_opus;
/**
* 流式编码【开启】
*/
+ (int)opusEecoderRun;
/**
* 输入pcm的数据
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据
* 通知数据类型:NSNotification.object => NSData(opus格式)
* @param data opus数据流(长度:1024)
*/
+ (void)pcmWriteData:(NSData*)data;
/**
* 流式编码【关闭】
*/
+ (int)opusEncoderStop;
@end
NS_ASSUME_NONNULL_END

20
local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexKit.h

@ -0,0 +1,20 @@
//
// SpeexKit.h
// SpeexKit
//
// Created by zhihui liang on 2018/8/20.
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for SpeexKit.
FOUNDATION_EXPORT double SpeexKitVersionNumber;
//! Project version string for SpeexKit.
FOUNDATION_EXPORT const unsigned char SpeexKitVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SpeexKit/PublicHeader.h>
#import <SpeexKit/SpeexUnit.h>
#import <SpeexKit/OpusUnit.h>

40
local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexUnit.h

@ -0,0 +1,40 @@
//
// SpeexUnit.h
// SpeexKit
//
// Created by zhihui liang on 2018/8/20.
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
//
#import <Foundation/Foundation.h>
/**
* 流式解码回调PCM数据
*/
extern NSString *kSPEEX_DECODE_DATA; //speex decode
@interface SpeexUnit : NSObject
+(void)speexIsLog:(BOOL)log;
/**
* 直接【speex文件】转换成【pcm文件】
*/
+(int)speexDecodeSPX:(NSString *)path_spx PCM:(NSString *)path_pcm;
/**
* 流式解码【开启】
*/
+(int)speexDecoderRun;
/**
* 输入Speex的数据
*/
+(void)speexWriteData:(NSData*)data;
/**
* 流式解码【关闭】
*/
+(int)speexDecoderStop;
@end

BIN
local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Info.plist

Binary file not shown.

6
local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Modules/module.modulemap

@ -0,0 +1,6 @@
framework module SpeexKit {
umbrella header "SpeexKit.h"
export *
module * { export * }
}

BIN
local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/SpeexKit

Binary file not shown.

8
local_plugins/ble_service/ios/ble_service.podspec

@ -1,5 +1,6 @@
# #
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint ble_service.podspec` to validate before publishing.
# #
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'ble_service' s.name = 'ble_service'
@ -15,7 +16,14 @@ BLE服务插件,提供蓝牙扫描、连接和通信功能,支持iOS 18+ Acc
s.source_files = 'Classes/**/*' s.source_files = 'Classes/**/*'
s.dependency 'Flutter' s.dependency 'Flutter'
s.platform = :ios, '18.0' s.platform = :ios, '18.0'
# -- ① 把 SpeexKit 声明为“随包带来的二进制框架” --
s.vendored_frameworks = 'Frameworks/SpeexKit.framework' # <- 或 .xcframework
# -- ② SpeexKit 可能依赖到的系统库 --
s.frameworks = 'AVFoundation', 'AudioToolbox'
s.libraries = 'iconv', 'c++' # libc++/iconv多数 Opus/Speex 都要
# Flutter.framework does not contain a i386 slice. # Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0' s.swift_version = '5.0'

58
local_plugins/ble_service/pubspec.yaml

@ -1,26 +1,72 @@
name: ble_service name: ble_service
description: BLE服务插件,提供蓝牙扫描、连接和通信功能 description: 蓝牙低功耗服务插件,提供蓝牙设备连接、通信功能
version: 0.0.1 version: 0.0.1
homepage: homepage:
environment: environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.18.0 <4.0.0"
flutter: ">=2.5.0" flutter: ">=3.3.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# 添加其他依赖项
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^1.0.0 flutter_lints: ^2.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: 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: plugin:
platforms: platforms:
android: android:
package: com.yunqiinnovation.ble_service package: com.yunqiinnovation.ble_service
pluginClass: BleServicePlugin pluginClass: BleServicePlugin
ios: ios:
pluginClass: BleServicePlugin pluginClass: BleServicePlugin
# 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/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# 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/custom-fonts/#from-packages
Loading…
Cancel
Save