diff --git a/flutter_02.log b/flutter_02.log new file mode 100644 index 000000000..bdc03f940 --- /dev/null +++ b/flutter_02.log @@ -0,0 +1,379 @@ +Flutter crash report. +Please report a bug at https://github.com/flutter/flutter/issues. + +## command + +flutter build ios + +## exception + +ProcessException: ProcessException: Process exited abnormally with exit code -6: +Command line invocation: + /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -list + +Resolve Package Graph + +Fetching from https://github.com/zhangao0086/DKImagePickerController (cached) + +Fetching from https://github.com/zhangao0086/DKCamera (cached) + +Fetching from https://github.com/zhangao0086/DKPhotoGallery (cached) + +Fetching from https://github.com/apple/swift-nio.git + +Fetching from https://github.com/Alamofire/Alamofire.git + +Fetching from https://github.com/firebase/firebase-ios-sdk (cached) + +Fetching from https://github.com/google/promises.git (cached) + +Fetching from https://github.com/apple/swift-log.git + +Fetching from https://github.com/SDWebImage/SDWebImage (cached) + +Fetching from https://github.com/kirualex/SwiftyGif.git (cached) + +Fetching from https://github.com/loopwork-ai/eventsource.git + +Fetching from https://github.com/google/gtm-session-fetcher.git (cached) + +Fetching from https://github.com/apple/swift-atomics.git + +Fetching from https://github.com/firebase/leveldb.git (cached) + +Fetching from https://github.com/firebase/nanopb.git (cached) + +Fetching from https://github.com/google/interop-ios-for-google-sdks.git (cached) + +Fetching from https://github.com/google/GoogleAppMeasurement.git (cached) + +Fetching from https://github.com/SDWebImage/libwebp-Xcode.git + +Fetching from https://github.com/openid/AppAuth-iOS.git (cached) + +Fetching from https://github.com/MacPaw/OpenAI.git (cached) + +Fetching from https://github.com/apple/swift-protobuf.git (cached) + +Fetching from https://github.com/google/abseil-cpp-binary.git (cached) + +Fetching from https://github.com/apple/swift-system.git + +Fetching from https://github.com/SDWebImage/SDWebImageWebPCoder.git + +Fetching from https://github.com/TimOliver/TOCropViewController (cached) + +Fetching from https://github.com/google/GTMAppAuth.git (cached) + +Fetching from https://github.com/apple/swift-http-types (cached) + +Fetching from https://github.com/google/GoogleSignIn-iOS.git (cached) + +Fetching from https://github.com/google/grpc-binary.git (cached) + +Fetching from https://github.com/googleads/google-ads-on-device-conversion-ios-sdk (cached) + +Fetching from https://github.com/google/GoogleUtilities.git (cached) + +Fetching from https://github.com/google/GoogleDataTransport.git (cached) + +Fetching from https://github.com/google/app-check.git (cached) + +Fetching from https://github.com/apple/swift-collections.git + +Fetching from https://github.com/apple/swift-openapi-runtime (cached) + +Creating working copy of package ‘AppAuth-iOS’ + +Checking out 1.7.6 of package ‘AppAuth-iOS’ + +Creating working copy of package ‘google-ads-on-device-conversion-ios-sdk’ + +Checking out 3.6.0 of package ‘google-ads-on-device-conversion-ios-sdk’ + +Creating working copy of package ‘swift-system’ + +Checking out 1.6.4 of package ‘swift-system’ + +Creating working copy of package ‘leveldb’ + +Checking out 1.22.5 of package ‘leveldb’ + +Creating working copy of package ‘SwiftyGif’ + +Checking out 5.4.5 of package ‘SwiftyGif’ + +Creating working copy of package ‘firebase-ios-sdk’ + +Checking out 12.14.0 of package ‘firebase-ios-sdk’ + +Creating working copy of package ‘grpc-binary’ + +Checking out 1.69.1 of package ‘grpc-binary’ + +Creating working copy of package ‘GoogleSignIn-iOS’ + +Checking out 8.0.0 of package ‘GoogleSignIn-iOS’ + +Creating working copy of package ‘eventsource’ + +Checking out 1.4.1 of package ‘eventsource’ + +Creating working copy of package ‘gtm-session-fetcher’ + +Checking out 3.5.0 of package ‘gtm-session-fetcher’ + +Creating working copy of package ‘OpenAI’ + +Checking out 0.4.3 of package ‘OpenAI’ + +Creating working copy of package ‘swift-openapi-runtime’ + +Checking out 1.8.2 of package ‘swift-openapi-runtime’ + +Creating working copy of package ‘Alamofire’ + +Checking out 5.10.2 of package ‘Alamofire’ + +Creating working copy of package ‘swift-nio’ + +Checking out 2.100.0 of package ‘swift-nio’ + +Creating working copy of package ‘nanopb’ + +Checking out 2.30910.0 of package ‘nanopb’ + +Creating working copy of package ‘promises’ + +Checking out 2.4.0 of package ‘promises’ + +Creating working copy of package ‘swift-protobuf’ + +Checking out 1.29.0 of package ‘swift-protobuf’ + +Creating working copy of package ‘TOCropViewController’ + +Checking out 2.8.0 of package ‘TOCropViewController’ + +Creating working copy of package ‘interop-ios-for-google-sdks’ + +Checking out 101.0.0 of package ‘interop-ios-for-google-sdks’ + +Creating working copy of package ‘swift-log’ + +Checking out 1.12.1 of package ‘swift-log’ + +Creating working copy of package ‘swift-collections’ + +Checking out 1.5.1 of package ‘swift-collections’ + +Creating working copy of package ‘GTMAppAuth’ + +Checking out 4.1.1 of package ‘GTMAppAuth’ + +Creating working copy of package ‘SDWebImage’ + +Checking out 5.21.7 of package ‘SDWebImage’ + +Creating working copy of package ‘GoogleAppMeasurement’ + +Checking out 12.14.0 of package ‘GoogleAppMeasurement’ + +Creating working copy of package ‘DKImagePickerController’ + +Checking out 4.3.9 (0bdfeac) of package ‘DKImagePickerController’ + +Creating working copy of package ‘app-check’ + +Checking out 11.3.0 of package ‘app-check’ + +Creating working copy of package ‘abseil-cpp-binary’ + +Checking out 1.2024072200.0 of package ‘abseil-cpp-binary’ + +Creating working copy of package ‘GoogleUtilities’ + +Checking out 8.1.0 of package ‘GoogleUtilities’ + +Creating working copy of package ‘swift-http-types’ + +Checking out 1.4.0 of package ‘swift-http-types’ + +Creating working copy of package ‘DKPhotoGallery’ + +Checking out master (311c1bc) of package ‘DKPhotoGallery’ + +Creating working copy of package ‘swift-atomics’ + +Checking out 1.3.0 of package ‘swift-atomics’ + +Creating working copy of package ‘GoogleDataTransport’ + +Checking out 10.1.0 of package ‘GoogleDataTransport’ + +Creating working copy of package ‘DKCamera’ + +Checking out master (5c691d1) of package ‘DKCamera’ + +Conflicting identity for test: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/test/ios/test' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/test' both point to the same package identity 'test'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/test/ios/test (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/test. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for ble_service: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/ble_service/ios/ble_service' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/ble_service' both point to the same package identity 'ble_service'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/ble_service/ios/ble_service (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/ble_service. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for speech: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/speech/ios/speech' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/speech' both point to the same package identity 'speech'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/speech/ios/speech (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/speech. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for azure_speech: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/azure_speech/ios/azure_speech' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/azure_speech' both point to the same package identity 'azure_speech'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/azure_speech/ios/azure_speech (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/azure_speech. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for chat_api: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/chat_api/ios/chat_api' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/chat_api' both point to the same package identity 'chat_api'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/chat_api/ios/chat_api (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/chat_api. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for chat_storage: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/chat_storage/ios/chat_storage' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/chat_storage' both point to the same package identity 'chat_storage'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/chat_storage/ios/chat_storage (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/chat_storage. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for location_service: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/location_service/ios/location_service' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/location_service' both point to the same package identity 'location_service'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/location_service/ios/location_service (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/location_service. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for music_service: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/music_service/ios/music_service' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/music_service' both point to the same package identity 'music_service'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/music_service/ios/music_service (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/music_service. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for user_portrait: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/music_service/ios/user_portrait' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/user_portrait' both point to the same package identity 'user_portrait'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/music_service/ios/user_portrait (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/user_portrait. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for qq_music: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/qq_music/ios/qq_music' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/qq_music' both point to the same package identity 'qq_music'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/qq_music/ios/qq_music (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/qq_music. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for speech: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/speech/ios/speech' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/speech' both point to the same package identity 'speech'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/speech/ios/speech (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/speech. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for ble_service: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/ble_service/ios/ble_service' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/ble_service' both point to the same package identity 'ble_service'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/ble_service/ios/ble_service (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/ble_service. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for classic_bluetooth: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/classic_bluetooth/ios/classic_bluetooth' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/classic_bluetooth' both point to the same package identity 'classic_bluetooth'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/azure_speech->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/classic_bluetooth/ios/classic_bluetooth (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/classic_bluetooth. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for location_service: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/location_service/ios/location_service' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/location_service' both point to the same package identity 'location_service'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/location_service/ios/location_service (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/location_service. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for music_service: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/music_service/ios/music_service' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/music_service' both point to the same package identity 'music_service'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/music_service/ios/music_service (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/music_service. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for qq_music: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/qq_music/ios/qq_music' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/qq_music' both point to the same package identity 'qq_music'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/qq_music/ios/qq_music (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/qq_music. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM.Conflicting identity for ble_service: dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/ble_service/ios/ble_service' and dependency '/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/ble_service' both point to the same package identity 'ble_service'. The dependencies are introduced through the following chains: (A) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/agent_service->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/ble_service/ios/ble_service (B) /users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/fluttergeneratedpluginswiftpackage->/users/liwei/work/flutter/deepvoice_client/ios/flutter/ephemeral/packages/.packages/ble_service. If there are multiple chains that lead to the same dependency, only the first chain is shown here. To see all chains use debug output option. To resolve the conflict, coordinate with the maintainer of the package that introduces the conflicting dependency. This will be escalated to an error in future versions of SwiftPM. +2026-08-11 14:07:31.830 xcodebuild[43679:20351941] DVTDeviceOperation: Encountered a build number "" that is incompatible with DVTBuildVersion. +2026-08-11 14:07:31.831 xcodebuild[43679:20351792] [MT] DVTDeviceOperation: Encountered a build number "" that is incompatible with DVTBuildVersion. +*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSMutableArray insertObjects:atIndexes:]: count of array (70) differs from count of index set (68)' +*** First throw call stack: +( + 0 CoreFoundation 0x000000018b8e51c0 __exceptionPreprocess + 176 + 1 libobjc.A.dylib 0x000000018b36e91c objc_exception_throw + 88 + 2 CoreFoundation 0x000000018b9e2a84 mutateError + 0 + 3 CoreFoundation 0x000000018b8cba78 -[NSMutableArray insertObjects:atIndexes:] + 1444 + 4 IDEFoundation 0x00000001146b81c0 __50-[IDEGroup insertGroupSubitems:atIndexes:context:]_block_invoke + 1072 + 5 DVTFoundation 0x000000011033a240 -[DVTModelObjectGraph performBlockCoalescingModelChanges:] + 160 + 6 IDEFoundation 0x00000001146b7d14 -[IDEGroup insertGroupSubitems:atIndexes:context:] + 232 + 7 DVTFoundation 0x00000001102fc80c -[NSMutableArray(DVTFoundationClassAdditions) dvt_sortedInsertOfObjects:withComparator:] + 180 + 8 IDESwiftPackageCore 0x000000011d6dc830 $s19IDESwiftPackageCore0A9WorkspaceC23DependencyPackagesGroupC12sortedInsert2ofySayAA0abE13FileReferenceOG_tF + 308 + 9 IDESwiftPackageCore 0x000000011d628b60 $s19IDESwiftPackageCore23IDESPMWorkspaceDelegateC32registerDependencyFileReferences33_033DB97E8C2A552F3158921835EE9F98LL_27modelGraphSynchronizerTokenySayAA0abgH9ReferenceO010dependencyU0_AC16Reg 10 IDESwiftPackageCore 0x000000011d62c2e4 $s19IDESwiftPackageCore23IDESPMWorkspaceDelegateC27dependencyPackagesDidUpdate33_033DB97E8C2A552F3158921835EE9F98LL8packages14graphHasErrors25revalidatedFilePathCaches27modelGraphSynchronizerToke 11 IDESwiftPackageCore 0x000000011d6244ac $s19IDESwiftPackageCore23IDESPMWorkspaceDelegateC27packageGraphDidFinishAction_8duration6resulty7SwiftPM12SPMWorkspaceC0bgJ0O_SdAG0oebgJ16ResolutionResultOtYaFyAC05ModelG17SynchronizerToken33_033 12 IDESwiftPackageCore 0x000000011d62832c $s19IDESwiftPackageCore23IDESPMWorkspaceDelegateC38disableWorkspaceContentSynchronization33_033DB97E8C2A552F3158921835EE9F98LL6duringyyAC27ModelGraphSynchronizerTokenAELLVXE_tFyyXEfU_TA + 20 + 13 DVTFoundation 0x0000000110511b0c $sSo19DVTModelObjectGraphC13DVTFoundationE34performBlockCoalescingModelChangesyxxyXElFyyXEfU_ + 104 + 14 DVTFoundation 0x00000001104c7904 $s2os18OSLogInterpolationV06appendC0_6format5align7privacyySdyXA_AA0B15FloatFormattingVAA0B15StringAlignmentVAA0B7PrivacyVtFSdycfu_ + 20 + 15 DVTFoundation 0x0000000110511828 $sIeg_IyB_TR + 20 + 16 DVTFoundation 0x000000011033a2c0 __58-[DVTModelObjectGraph performBlockCoalescingModelChanges:]_block_invoke + 104 + 17 DVTFoundation 0x00000001103343e4 -[DVTModelGraphTransactionScope performTransaction:] + 60 + 18 DVTFoundation 0x000000011033a228 -[DVTModelObjectGraph performBlockCoalescingModelChanges:] + 136 + 19 DVTFoundation 0x0000000110512330 $sSo19DVTModelObjectGraphC13DVTFoundationE34performBlockCoalescingModelChangesyxxyXElF + 336 + 20 IDESwiftPackageCore 0x000000011d616624 $s19IDESwiftPackageCore23IDESPMWorkspaceDelegateC38disableWorkspaceContentSynchronization33_033DB97E8C2A552F3158921835EE9F98LL6duringyyAC27ModelGraphSynchronizerTokenAELLVXE_tF + 496 + 21 IDESwiftPackageCore 0x000000011d6232c8 $s19IDESwiftPackageCore23IDESPMWorkspaceDelegateC27packageGraphDidFinishAction_8duration6resulty7SwiftPM12SPMWorkspaceC0bgJ0O_SdAG0oebgJ16ResolutionResultOtYaFTY8_ + 176 + 22 IDESwiftPackageCore 0x000000011d62d449 $s19IDESwiftPackageCore23IDESPMWorkspaceDelegateC27packageGraphWillStartActionyy7SwiftPM12SPMWorkspaceC0bgJ0OFy13DVTFoundation24DVTAsyncStreamControllerCyAE0mB15ResolutionEventOs5NeverOGSgzYbXEfU 23 SwiftPM 0x0000000125b2f201 $s7SwiftPM12SPMWorkspaceC26packageGraphActionFinished33_918CA8CFEB8D3E06D2EEA2771EA5EA00LLyyAC07PackageE16ResolutionResultAELLVYaFTQ1_ + 1 + 24 SwiftPM 0x0000000125b2eb09 $s7SwiftPM12SPMWorkspaceC46processPackageGraphActionsInBackgroundIfNeeded33_918CA8CFEB8D3E06D2EEA2771EA5EA00LL010canProcessefG0yAC11AffirmativeO_tFyyYaYbcfU1_yyYacfU0_TQ1_ + 1 + 25 SwiftPM 0x0000000125b0ed75 $s7SwiftPM21SPMRepositoryProviderC10UnderlyingC13SourceControl010RepositoryD0AafGP4open10repository2atAF0H0_pAF0H9SpecifierV_6Basics12AbsolutePathVtYaKFTWTQ0_ + 1 + 26 SwiftPM 0x0000000125b6aa7d $sxIeAgHr_xs5Error_pIegHrzo_s8SendableRzs5NeverORs_r0_lTRyt_Tg5TQ0_ + 1 + 27 SwiftPM 0x0000000125b0ed75 $s7SwiftPM21SPMRepositoryProviderC10UnderlyingC13SourceControl010RepositoryD0AafGP4open10repository2atAF0H0_pAF0H9SpecifierV_6Basics12AbsolutePathVtYaKFTWTQ0_ + 1 + 28 libswift_Concurrency.dylib 0x0000000290147ec5 _ZL23completeTaskWithClosurePN5swift12AsyncContextEPNS_10SwiftErrorE + 1 +) +** INTERNAL ERROR: Uncaught exception ** +Uncaught Exception: *** -[NSMutableArray insertObjects:atIndexes:]: count of array (70) differs from count of index set (68) +Stack: + 0 __exceptionPreprocess (in CoreFoundation) + 1 objc_exception_throw (in libobjc.A.dylib) + 2 mutateError (in CoreFoundation) + 3 -[NSMutableArray insertObjects:atIndexes:] (in CoreFoundation) + 4 __50-[IDEGroup insertGroupSubitems:atIndexes:context:]_block_invoke (in IDEFoundation) + 5 -[DVTModelObjectGraph performBlockCoalescingModelChanges:] (in DVTFoundation) + 6 -[IDEGroup insertGroupSubitems:atIndexes:context:] (in IDEFoundation) + 7 -[NSMutableArray(DVTFoundationClassAdditions) dvt_sortedInsertOfObjects:withComparator:] (in DVTFoundation) + 8 IDESwiftWorkspace.DependencyPackagesGroup.sortedInsert(of:) (in IDESwiftPackageCore) + 9 specialized IDESPMWorkspaceDelegate.registerDependencyFileReferences(_:modelGraphSynchronizerToken:) (in IDESwiftPackageCore) + 10 specialized IDESPMWorkspaceDelegate.dependencyPackagesDidUpdate(packages:graphHasErrors:revalidatedFilePathCaches:modelGraphSynchronizerToken:) (in IDESwiftPackageCore) + 11 closure #1 in IDESPMWorkspaceDelegate.packageGraphDidFinishAction(_:duration:result:) (in IDESwiftPackageCore) + 12 partial apply for closure #1 in IDESPMWorkspaceDelegate.disableWorkspaceContentSynchronization(during:) (in IDESwiftPackageCore) + 13 (in DVTFoundation) + 14 (in DVTFoundation) + 15 thunk for @escaping @callee_guaranteed () -> () (in DVTFoundation) + 16 __58-[DVTModelObjectGraph performBlockCoalescingModelChanges:]_block_invoke (in DVTFoundation) + 17 -[DVTModelGraphTransactionScope performTransaction:] (in DVTFoundation) + 18 -[DVTModelObjectGraph performBlockCoalescingModelChanges:] (in DVTFoundation) + 19 DVTModelObjectGraph.performBlockCoalescingModelChanges(_:) (in DVTFoundation) + 20 IDESPMWorkspaceDelegate.disableWorkspaceContentSynchronization(during:) (in IDESwiftPackageCore) + 21 IDESPMWorkspaceDelegate.packageGraphDidFinishAction(_:duration:result:) (in IDESwiftPackageCore) + 22 (in IDESwiftPackageCore) + 23 SPMWorkspace.packageGraphActionFinished(_:) (in SwiftPM) + 24 closure #2 in closure #3 in SPMWorkspace.processPackageGraphActionsInBackgroundIfNeeded(canProcessPackageGraphActions:) (in SwiftPM) + 25 (in SwiftPM) + 26 specialized thunk for @escaping @isolated(any) @callee_guaranteed @async () -> (@out A) (in SwiftPM) + 27 (in SwiftPM) + 28 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) (in libswift_Concurrency.dylib) + Command: /usr/bin/arch -arm64e xcrun xcodebuild -list + +``` +#0 RunResult.throwException (package:flutter_tools/src/base/process.dart:153:5) +#1 _DefaultProcessUtils.run (package:flutter_tools/src/base/process.dart:379:19) + +#2 XcodeProjectInterpreter.getInfo (package:flutter_tools/src/ios/xcodeproj.dart:328:30) + +#3 XcodeBasedProject.projectInfo (package:flutter_tools/src/xcode_project.dart:211:29) + +#4 XcodeBasedProject.buildSettingsForBuildInfo (package:flutter_tools/src/xcode_project.dart:247:36) + +#5 IosProject._parseProductName (package:flutter_tools/src/xcode_project.dart:852:55) + +#6 IosProject.productName (package:flutter_tools/src/xcode_project.dart:840:29) + +#7 BuildableIOSApp.fromProject (package:flutter_tools/src/ios/application_package.dart:118:33) + +#8 FlutterApplicationPackageFactory.getPackageForPlatform (package:flutter_tools/src/flutter_application_package.dart:79:15) + +#9 _BuildIOSSubCommand.buildableIOSApp. (package:flutter_tools/src/commands/build_ios.dart:938:9) + +#10 _BuildIOSSubCommand.runCommand (package:flutter_tools/src/commands/build_ios.dart:978:33) + +#11 FlutterCommand.run. (package:flutter_tools/src/runner/flutter_command.dart:1590:27) + +#12 AppContext.run. (package:flutter_tools/src/base/context.dart:154:19) + +#13 CommandRunner.runCommand (package:args/command_runner.dart:212:13) + +#14 FlutterCommandRunner.runCommand. (package:flutter_tools/src/runner/flutter_command_runner.dart:496:9) + +#15 AppContext.run. (package:flutter_tools/src/base/context.dart:154:19) + +#16 FlutterCommandRunner.runCommand (package:flutter_tools/src/runner/flutter_command_runner.dart:431:5) + +#17 FlutterCommandRunner.run. (package:flutter_tools/src/runner/flutter_command_runner.dart:307:33) + +#18 run.. (package:flutter_tools/runner.dart:104:11) + +#19 AppContext.run. (package:flutter_tools/src/base/context.dart:154:19) + +#20 main (package:flutter_tools/executable.dart:103:3) + +``` + +## flutter doctor + +``` +[✓] Flutter (Channel stable, 3.41.9, on macOS 26.5 25F71 darwin-arm64, locale zh-Hans-CN) [204ms] + • Flutter version 3.41.9 on channel stable at /Users/liwei/flutter + • Upstream repository https://github.com/flutter/flutter.git + • Framework revision 00b0c91f06 (3 months ago), 2026-04-29 10:03:19 -0700 + • Engine revision 42d3d75a56 + • Dart version 3.11.5 + • DevTools version 2.54.2 + • Pub download mirror https://pub.flutter-io.cn + • Flutter download mirror https://storage.flutter-io.cn + • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, + cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, + enable-uiscene-migration + +[!] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [4.3s] + • Android SDK at /Users/liwei/Library/Android/sdk + • Emulator version unknown + • Platform .android-Baklava.bak, build-tools 36.0.0 + • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java + This is the JDK bundled with the latest Android Studio installation on this machine. + To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`. + • Java version OpenJDK Runtime Environment (build 21.0.4+-12422083-b607.1) + ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses + +[✓] Xcode - develop for iOS and macOS (Xcode 26.5) [1,622ms] + • Xcode at /Applications/Xcode.app/Contents/Developer + • Build 17F42 + • CocoaPods version 1.16.2 + +[✓] Chrome - develop for the web [48ms] + • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome + +[✓] Connected device (3 available) [6.3s] + • Weiyu (wireless) (mobile) • 00008120-000E54A02631A01E • ios • iOS 18.7.8 22H352 + • macOS (desktop) • macos • darwin-arm64 • macOS 26.5 25F71 darwin-arm64 + • Chrome (web) • chrome • web-javascript • Google Chrome 151.0.7922.76 + ! Error: Browsing on the local area network for liwei. Ensure the device is unlocked and attached with a cable or associated with + the same local area network as this Mac. + The device must be opted into Developer Mode to connect wirelessly. (code -27) + ! Error: Browsing on the local area network for Voitrans的iPhoneXr. Ensure the device is unlocked and attached with a cable or + associated with the same local area network as this Mac. + The device must be opted into Developer Mode to connect wirelessly. (code -27) + ! Error: Browsing on the local area network for 李伟的Apple Watch. Ensure the device is unlocked and discoverable via Bluetooth. + (code -27) + +[✓] Network resources [1,227ms] + • All expected network resources are available. + +! Doctor found issues in 1 category. +``` diff --git a/lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart b/lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart index b0a696b89..bc0618928 100644 --- a/lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart +++ b/lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart @@ -12,17 +12,21 @@ class CallTranslationDebugController extends GetxController { final GetStorage _storage = GetStorage(); // 持久化键 - static const String _kSendInterval = 'call_trans_debug_send_interval'; + // 码率默认值 16→8kbps、发送间隔默认值 100→40ms,键名升版丢弃旧偏好,确保新默认生效 + static const String _kSendInterval = 'call_trans_debug_send_interval_v2'; static const String _kBundleFrameCount = 'call_trans_debug_bundle_frames'; - static const String _kUplinkBitrate = 'call_trans_debug_uplink_bitrate'; - static const String _kDownlinkBitrate = 'call_trans_debug_downlink_bitrate'; - - // 默认值(与原生默认保持一致) - static const int defaultSendIntervalMs = 100; + static const String _kUplinkBitrate = 'call_trans_debug_uplink_bitrate_v2'; + static const String _kDownlinkBitrate = 'call_trans_debug_downlink_bitrate_v2'; + + // 默认值(与原生默认保持一致,见 BleService.kt DEFAULT_AUDIO_SEND_INTERVAL_NORMAL = 40L) + // 注意:发送间隔必须 ≤ 合包产生周期。左右各每 20ms 出 1 帧、每 bundleFrameCount 帧合 1 包, + // 双声道合计每 (20 × bundleFrameCount ÷ 2) ms 产生 1 包 —— 默认 4 帧即 40ms/包。 + // 间隔一旦大于这个值,发送速率追不上产生速率,队列会持续积压、时延无上限增长。 + static const int defaultSendIntervalMs = 40; static const int defaultBundleFrameCount = 4; - // 上行/下行默认每声道码率(kbps):对应上行 packetSize=40、下行 packetSize=80 - static const int defaultUplinkBitrateKbps = 16; - static const int defaultDownlinkBitrateKbps = 16; + // 上行/下行默认每声道码率(kbps):对应上行 packetSize=20、下行 packetSize=40 + static const int defaultUplinkBitrateKbps = 8; + static const int defaultDownlinkBitrateKbps = 8; // 取值范围 static const int minSendIntervalMs = 20; diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt index 11f6a7c10..100b1719a 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt @@ -32,7 +32,15 @@ class BleCommandSender { /** 指令队列 */ private val commandQueue: Queue = LinkedList() - + + /** 通话翻译心跳包(内容固定,预先成帧,避免每 50ms 重复建包与算CRC) */ + private val heartbeatPacket: ByteArray by lazy { + createCommandPacket( + BleConst.CMD_CALL_TRANSLATION_HEARTBEAT.toByte(), + byteArrayOf(BleConst.CALL_TRANSLATION_HEARTBEAT_PAYLOAD.toByte()) + ) + } + /** 是否正在等待回复 */ var isWaitingReply = true private set @@ -128,6 +136,7 @@ class BleCommandSender { * @param cmdData 命令数据 * @return 是否成功发送 */ + @Synchronized @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { val callback = this.callback ?: run { @@ -181,6 +190,39 @@ class BleCommandSender { } } + /** + * 立即发送一包通话翻译心跳(cmd=0xFF, data=[0xFF]),不入队列、不等待设备回复。 + * + * 心跳按 50ms 一拍持续下发,设备侧不回 ACK:若走 sendCommand 的常规路径, + * isWaitingReply 会一直为 false,心跳只能靠 1s 超时逐个排空,队列会以 20 包/秒的速度积压, + * 并把正常指令挤在后面。因此心跳单独走直写路径,且不参与 isWaitingReply 状态机。 + * + * 若此刻有普通指令在途(isWaitingReply == false),本拍心跳跳过: + * 同一时刻只允许一次 GATT 写在途,抢写只会失败,下一拍(50ms 后)再发即可。 + * + * @return 是否成功写入本拍心跳 + */ + // 与 sendCommand/processNextCommand 互斥:writeChar.value 是同一个共享缓冲, + // 心跳线程若在主线程"填包 → 写入"之间插入,会把待发的普通指令内容覆盖掉 + @Synchronized + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + fun sendCallTranslationHeartbeat(): Boolean { + val callback = this.callback ?: return false + if (!callback.isConnected()) return false + // 普通指令在途,让位给它,本拍不发 + if (!isWaitingReply) return false + + return try { + val writeChar = callback.getWriteCharacteristic() ?: return false + val gatt = callback.getBluetoothGatt() ?: return false + writeChar.value = heartbeatPacket + gatt.writeCharacteristic(writeChar) == true + } catch (e: Exception) { + Log.e(TAG, "发送通话翻译心跳异常: ${e.message}", e) + false + } + } + /** * 处理命令响应,重置等待状态并处理下一个命令 * @param responseCommandType 响应的命令类型 @@ -217,7 +259,14 @@ class BleCommandSender { val cmdType = data[1].toInt() and 0xFF val lenByte = data[2].toInt() and 0xFF - + // 心跳(0xFF)不参与命令状态机:心跳本身就是直写下发的, + // 若在此推进 isWaitingReply/队列,会把真正在途指令的等待状态提前放开 + if (cmdType == BleConst.CMD_CALL_TRANSLATION_HEARTBEAT) { + Log.d(TAG, "[CALL_TRANS_HB] 收到心跳响应,忽略") + return + } + + // 正常响应处理:尽量进行 CRC 校验 var crcOk = true @@ -766,6 +815,7 @@ class BleCommandSender { /** * 处理队列中的下一个命令 */ + @Synchronized private fun processNextCommand() { if (commandQueue.isEmpty()) { return diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index 0106599e5..9fbd33650 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -105,6 +105,12 @@ object BleConst { const val CMD_CALL_TRANSLATION_ON = 0x16 /** 设备上报关闭通话翻译 0x17 (帧头0xCC, cmdLen=0x01, status=0xFF) */ const val CMD_CALL_TRANSLATION_OFF = 0x17 + /** 通话翻译心跳 0xFF (帧头0xAA, cmdLen=0x01, data=0xFF),无业务内容,仅用于维持链路数据频率 */ + const val CMD_CALL_TRANSLATION_HEARTBEAT = 0xFF + /** 通话翻译心跳数据位(占位,无业务含义) */ + const val CALL_TRANSLATION_HEARTBEAT_PAYLOAD = 0xFF + /** 通话翻译心跳发送间隔(ms) */ + const val CALL_TRANSLATION_HEARTBEAT_INTERVAL_MS = 50L // ================== 语音唤醒状态值 ================== /** 语音唤醒关闭 */ diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index dd9ceda0f..261a48f32 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -160,15 +160,15 @@ object BleService { @Volatile private var audioSendIntervalNormal = DEFAULT_AUDIO_SEND_INTERVAL_NORMAL - // 上行编码 packetSize(字节):每声道独立 mono 流每帧字节数,默认 40 = 每声道 16kbps@20ms。 + // 上行编码 packetSize(字节):每声道独立 mono 流每帧字节数,默认 20 = 每声道 8kbps@20ms。 // 调试界面可修改;因编码器创建时才读取,需重新启动通话翻译才生效。 @Volatile - private var uplinkPacketSize = 40 + private var uplinkPacketSize = 20 - // 下行解码 packetSize(字节):立体声合并流每帧字节数,默认 80 = 每声道 16kbps@20ms。 + // 下行解码 packetSize(字节):立体声合并流每帧字节数,默认 40 = 每声道 8kbps@20ms。 // 需与设备固件编码 packetSize 一致,否则解码错位;同样需重新启动通话翻译才生效。 @Volatile - private var downlinkPacketSize = 80 + private var downlinkPacketSize = 40 // 左/右声道休眠标记(true 表示该声道处于休眠,暂停下发)。 // 收到 F0(左)/F1(右) 时置 true,收到 F2(左)/F3(右) 恢复指令时置 false。 // 休眠期间该声道的包暂不发送(转入积压缓存),仅传输另一声道,恢复后按序补发,不丢包。 @@ -210,7 +210,34 @@ object BleService { // 初始化状态 private var isInitialized = false - + + // 通话翻译心跳相关:独立 HandlerThread 起拍,避免 50ms 的定时任务压在主线程上 + @Volatile + private var isHeartbeatRunning = false + private var heartbeatThread: HandlerThread? = null + private var heartbeatHandler: Handler? = null + // 心跳发送失败(链路忙/普通指令在途)的连续计数,仅用于节流日志,成功即清零 + private var heartbeatFailCount = 0 + private val heartbeatRunnable = object : Runnable { + override fun run() { + if (!isHeartbeatRunning) return + try { + if (commandSender.sendCallTranslationHeartbeat()) { + heartbeatFailCount = 0 + } else { + heartbeatFailCount++ + // 每积累 20 次(约1秒)失败告警一次,避免刷屏 + if (heartbeatFailCount % 20 == 0) { + Log.w(TAG, "[CALL_TRANS_HB] 心跳连续未发出 ${heartbeatFailCount} 次(链路忙或未连接)") + } + } + } catch (e: Exception) { + Log.e(TAG, "[CALL_TRANS_HB] 心跳发送异常: ${e.message}", e) + } + heartbeatHandler?.postDelayed(this, BleConst.CALL_TRANSLATION_HEARTBEAT_INTERVAL_MS) + } + } + // ====================================================================================================== // 指令发送和管理相关变量(整合自BleCommandSender) // ====================================================================================================== @@ -864,6 +891,8 @@ object BleService { } isReply = true; commandSender.clearCommandQueue() + // 连接已断开,心跳无处可发,停掉定时器 + stopCallTranslationHeartbeat() bluetoothGatt = null notifyChar = null writeChar = null @@ -1394,7 +1423,8 @@ object BleService { /** * 开始Opus数据流编码(通话翻译模式:左右声道独立编码) - * 输出合包格式:[4B 序号(大端 uint32)] + [1B 声道(0=左/1=右)] + [5 × opus(40B)] = 205B + * 输出合包格式:[4B 序号(大端 uint32)] + [1B 声道(0=左/1=右)] + [N × opus(uplinkPacketSize 字节)] + * 默认 N=4、uplinkPacketSize=20(8kbps@20ms),即每包 85B * @return 是否成功开始编码 */ private fun startOpusEncodeStream(): Boolean { @@ -1417,7 +1447,7 @@ object BleService { /** * 将一包已成型的音频合帧加入下行发送队列 - * 包格式:[4B 序号(大端 uint32)] + [1B 声道(0=左/1=右)] + [5 × opus(40B)] + * 包格式:[4B 序号(大端 uint32)] + [1B 声道(0=左/1=右)] + [N × opus(uplinkPacketSize 字节)] * 双声道独立编码模式下,每次回调送来的就是一整包,直接入队,不再二次切片 * @param data 待发送的合帧包 */ @@ -1762,6 +1792,47 @@ object BleService { Log.e(TAG, failureReason.toString()) } + // ====================================================================================================== + // 通话翻译心跳 - 50ms 一拍,维持链路数据频率 + // ====================================================================================================== + + /** + * 启动通话翻译心跳:按 CALL_TRANSLATION_HEARTBEAT_INTERVAL_MS 固定节拍下发 0xFF 指令。 + * 通话翻译期间链路一旦出现空闲,连接事件会被降频,导致上下行音频抖动; + * 心跳保证链路上始终有数据在跑。 + * 心跳走直写路径,不占用指令队列与等待回复状态机,详见 BleCommandSender.sendCallTranslationHeartbeat()。 + * 可重复调用(幂等),A2DP 解码器重试多轮时不会起多份定时器。 + */ + fun startCallTranslationHeartbeat() { + if (isHeartbeatRunning) { + Log.d(TAG, "[CALL_TRANS_HB] 心跳已在运行,跳过重复启动") + return + } + val thread = HandlerThread("ble-call-heartbeat").apply { start() } + heartbeatThread = thread + heartbeatHandler = Handler(thread.looper) + isHeartbeatRunning = true + heartbeatHandler?.post(heartbeatRunnable) + Log.i( + TAG, + "[CALL_TRANS_HB] 通话翻译心跳已启动 cmd=0x${"%02X".format(BleConst.CMD_CALL_TRANSLATION_HEARTBEAT)} " + + "间隔=${BleConst.CALL_TRANSLATION_HEARTBEAT_INTERVAL_MS}ms" + ) + } + + /** + * 停止通话翻译心跳并释放心跳线程。可重复调用(幂等)。 + */ + fun stopCallTranslationHeartbeat() { + if (!isHeartbeatRunning && heartbeatThread == null) return + isHeartbeatRunning = false + heartbeatHandler?.removeCallbacks(heartbeatRunnable) + heartbeatThread?.quitSafely() + heartbeatHandler = null + heartbeatThread = null + Log.i(TAG, "[CALL_TRANS_HB] 通话翻译心跳已停止") + } + // ====================================================================================================== // 通话翻译调试参数 - 运行时可调 // ====================================================================================================== @@ -1858,6 +1929,8 @@ object BleService { */ fun closeCodec(): Boolean { Log.i(TAG, "关闭编解码...") + // 退出通话翻译,停止心跳 + stopCallTranslationHeartbeat() recordfile?.closeFile() recordfile1?.closeFile() val cmdData = byteArrayOf( @@ -1899,6 +1972,8 @@ object BleService { ) val ok = commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) Log.i(TAG, "[CALL_TRANS_CMD] 通话翻译指令下发 cmd=0x05 sub=0xA2 result=$ok bytes=${cmdData.joinToString(",") { "0x%02X".format(it) }}") + // 进入通话翻译,开始下发心跳维持链路数据频率 + startCallTranslationHeartbeat() return ok } @@ -2166,7 +2241,10 @@ object BleService { */ fun cleanup() { Log.i(TAG, "清理BLE服务资源...") - + + // 停止通话翻译心跳 + stopCallTranslationHeartbeat() + // 重置指令发送器 commandSender.reset() diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift index 9f7549e89..1f98ba124 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift @@ -100,6 +100,12 @@ class BleConst { static let CMD_CALL_TRANSLATION_ON: UInt8 = 0x16 /** 设备上报关闭通话翻译 0x17 (帧头0xCC, cmdLen=0x01, status=0xFF) */ static let CMD_CALL_TRANSLATION_OFF: UInt8 = 0x17 + /** 通话翻译心跳 0xFF (帧头0xAA, cmdLen=0x01, data=0xFF),无业务内容,仅用于维持链路数据频率 */ + static let CMD_CALL_TRANSLATION_HEARTBEAT: UInt8 = 0xFF + /** 通话翻译心跳数据位(占位,无业务含义) */ + static let CALL_TRANSLATION_HEARTBEAT_PAYLOAD: UInt8 = 0xFF + /** 通话翻译心跳发送间隔(秒),即 50ms */ + static let CALL_TRANSLATION_HEARTBEAT_INTERVAL: TimeInterval = 0.05 // ================== 语音唤醒状态值 ================== /** 语音唤醒关闭 */ diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index 3a91d0e7e..ed01ab137 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -104,6 +104,8 @@ public class BleService: NSObject { private var sampleRate: Int32 = 16000 private var channels: Int32 = 1 private var frameSize: Int32 = 640 + // 通话翻译上行编码码率(每声道 kbps):8kbps@20ms → 20 字节/帧,与 Android 端 uplinkPacketSize=20 保持一致 + private let CALL_TRANS_UPLINK_BITRATE_KBPS: Int32 = 8 // MARK: - 音频数据发送相关属性 private var audioBuffer = [UInt8]() // 音频数据缓冲区 @@ -122,6 +124,22 @@ public class BleService: NSObject { private let nextSendEarliestLock = NSLock() private let audioQueueLock = NSLock() // 队列操作锁 + + // MARK: - 通话翻译心跳相关属性 + // 心跳独立起拍队列,不占用音频发送队列,也不压主线程 + private let heartbeatQueue = DispatchQueue(label: "com.yunqiinnovation.ble_service.call_heartbeat", qos: .userInitiated) + private var heartbeatTimer: DispatchSourceTimer? + private var isHeartbeatRunning = false + // 心跳包内容固定,预先成帧,避免每 50ms 重复建包与算CRC + private lazy var heartbeatPacket: Data = { + var packet = Data() + packet.append(BleConst.APP_REQUEST_HEADER) // 帧头 0xAA + packet.append(BleConst.CMD_CALL_TRANSLATION_HEARTBEAT) // 指令 0xFF + packet.append(UInt8(1)) // 长度 0x01 + packet.append(BleConst.CALL_TRANSLATION_HEARTBEAT_PAYLOAD) // 数据 0xFF + packet.append(self.calculateCRC8(data: packet)) // CRC8/MAXIM + return packet + }() //private var currentCommand: Command? // 命令结构体 @@ -480,7 +498,10 @@ public class BleService: NSObject { if centralManager != nil { isManualDisconnect = true // 标记为主动断开 - + + // 连接即将断开,心跳无处可发,停掉定时器 + stopCallTranslationHeartbeat() + // 停止音频发送定时器 stopAudioSendTimer() stopOpusRecording() @@ -753,8 +774,13 @@ public class BleService: NSObject { // 确保Opus解码器处于活跃状态(如果不是则启动) startOpusDecoding(sampleRate: 16000, channels: 2, frameSize: 1280) - opusProcessor?.startEncoding(sampleRate: 16000, channels: 1, frameRate: 20) - + // 通话翻译上行编码:8kbps/声道 @20ms = 20 字节/帧,与 Android 端 uplinkPacketSize=20 对齐 + opusProcessor?.startEncoding( + sampleRate: 16000, + channels: 1, + frameRate: 20, + bitrateKbps: CALL_TRANS_UPLINK_BITRATE_KBPS + ) os_log("[CALL_TRANS_CMD] 打开A2DP解码并开始录制(通话翻译)...", log: logger, type: .info) let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_STEREO]) @@ -762,6 +788,8 @@ public class BleService: NSObject { os_log("[CALL_TRANS_CMD] 通话翻译指令下发 cmd=0x05 sub=0xA2 result=%{public}@ bytes=[0x%02X,0x%02X]", log: logger, type: .info, ok ? "成功" : "失败", BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_STEREO) + // 进入通话翻译,开始下发心跳维持链路数据频率 + startCallTranslationHeartbeat() return ok } /// 打开通话记录解码器并开始录制 @@ -776,9 +804,10 @@ public class BleService: NSObject { /// - Returns: 操作是否成功 public func closeCodec() -> Bool { os_log("关闭编解码并停止录制...", log: logger, type: .info) - - - + + // 退出通话翻译,停止心跳 + stopCallTranslationHeartbeat() + // 停止音频发送定时器 stopAudioSendTimer() @@ -949,6 +978,67 @@ public class BleService: NSObject { processNextCommand() } + // MARK: - 通话翻译心跳 + + /// 启动通话翻译心跳:按 CALL_TRANSLATION_HEARTBEAT_INTERVAL(50ms) 固定节拍下发 0xFF 指令。 + /// 通话翻译期间链路一旦出现空闲,连接事件会被降频,导致上下行音频抖动;心跳保证链路上始终有数据在跑。 + /// 心跳直写特征值,不进命令队列、不等设备回复,详见 sendCallTranslationHeartbeat()。 + /// 可重复调用(幂等),A2DP 解码器重试多轮时不会起多份定时器。 + func startCallTranslationHeartbeat() { + heartbeatQueue.async { [weak self] in + guard let self = self else { return } + guard !self.isHeartbeatRunning else { + os_log("[CALL_TRANS_HB] 心跳已在运行,跳过重复启动", log: self.logger, type: .debug) + return + } + + self.isHeartbeatRunning = true + let timer = DispatchSource.makeTimerSource(queue: self.heartbeatQueue) + timer.schedule(deadline: .now() + BleConst.CALL_TRANSLATION_HEARTBEAT_INTERVAL, + repeating: BleConst.CALL_TRANSLATION_HEARTBEAT_INTERVAL, + leeway: .milliseconds(5)) + timer.setEventHandler { [weak self] in + self?.sendCallTranslationHeartbeat() + } + self.heartbeatTimer = timer + timer.resume() + + os_log("[CALL_TRANS_HB] 通话翻译心跳已启动 cmd=0x%02X 间隔=%.0fms", + log: self.logger, type: .info, + BleConst.CMD_CALL_TRANSLATION_HEARTBEAT, + BleConst.CALL_TRANSLATION_HEARTBEAT_INTERVAL * 1000) + } + } + + /// 停止通话翻译心跳。可重复调用(幂等)。 + func stopCallTranslationHeartbeat() { + heartbeatQueue.async { [weak self] in + guard let self = self else { return } + guard self.isHeartbeatRunning || self.heartbeatTimer != nil else { return } + + self.isHeartbeatRunning = false + self.heartbeatTimer?.cancel() + self.heartbeatTimer = nil + os_log("[CALL_TRANS_HB] 通话翻译心跳已停止", log: self.logger, type: .info) + } + } + + /// 发送一拍心跳:直接写指令特征值,不入队列、不等待回复。 + /// 心跳 50ms 一拍且设备不回 ACK,若走 sendCommand 常规路径,isReply 会一直为 false, + /// 心跳只能靠 1s 超时逐个排空,队列会以 20 包/秒积压并把正常指令挤在后面。 + /// 普通指令在途(isReply == false)时本拍让位,50ms 后再发。 + private func sendCallTranslationHeartbeat() { + guard let characteristic = writeCharacteristic, + let peripheral = peripheralDevice, + connectionState == BleConst.STATE_CONNECTED else { + return + } + // 普通指令在途,让位给它,本拍不发(与 Android 对齐) + guard isReply else { return } + + peripheral.writeValue(heartbeatPacket, for: characteristic, type: .withoutResponse) + } + /// 计算CRC8/MAXIM校验值(与Android端保持一致) private func calculateCRC8(data: Data) -> UInt8 { var crc: Int = 0 @@ -1069,6 +1159,13 @@ public class BleService: NSObject { return } + // 心跳(0xFF)不参与命令状态机:心跳本身就是直写下发的, + // 若在此推进 isReply/队列,会把真正在途指令的等待状态提前放开 + guard cmd != BleConst.CMD_CALL_TRANSLATION_HEARTBEAT else { + os_log("[CALL_TRANS_HB] 收到心跳响应,忽略", log: logger, type: .debug) + return + } + // 记录响应详情 var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") // os_log("liwei---- 收到设备响应: %@", log: logger, type: .debug, hexString) @@ -1472,6 +1569,7 @@ public class BleService: NSObject { /// 清理所有资源 func cleanup() { + stopCallTranslationHeartbeat() stopScan() disconnect() @@ -1999,7 +2097,9 @@ extension BleService: CBCentralManagerDelegate { updateConnectionState(BleConst.STATE_DISCONNECTED) stopOpusRecording() - + // 连接已断开,心跳无处可发,停掉定时器 + stopCallTranslationHeartbeat() + // 只有在非主动断开的情况下才重新连接 if !isManualDisconnect { if central.state == .poweredOn { diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift index 3c84f0a62..b6fae68b6 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift @@ -372,32 +372,32 @@ class SwiftOpusAudioProcessor: NSObject { /// - sampleRate: 采样率(默认16000Hz) /// - channels: 声道数(默认1) /// - frameRate: 帧长度(默认20ms) + /// - bitrateKbps: 每声道码率(kbps,默认 8),决定每帧字节数 = 码率 × 帧长 ÷ 8 /// - isHeader: 是否包含头部信息 /// - Returns: 是否启动成功 - func startEncoding(sampleRate: Int32 = 16000, - channels: Int32 = 1, + func startEncoding(sampleRate: Int32 = 16000, + channels: Int32 = 1, frameRate: Int32 = 20, + bitrateKbps: Int32 = 8, isHeader: Bool = false) -> Bool { if isEncodingActive { return true } - - print("启动JLOpusEncoder编码 - 采样率: \(sampleRate)Hz, 声道: \(channels), 帧长度: \(frameRate)ms, 头部: \(isHeader)") - + + print("启动JLOpusEncoder编码 - 采样率: \(sampleRate)Hz, 声道: \(channels), 帧长度: \(frameRate)ms, 码率: \(bitrateKbps)kbps/声道, 头部: \(isHeader)") + // 创建JLOpusFormat配置 encoderFormat = JLOpusFormat.defaultFormats() encoderFormat?.sampleRate = Int32(sampleRate) encoderFormat?.channels = Int32(channels) encoderFormat?.frameDuration = Int32(frameRate) + encoderFormat?.bitRate = bitrateKbps * 1000 * channels encoderFormat?.hasDataHeader = isHeader - - // 根据配置计算数据大小 - if channels == 1 { - encoderFormat?.dataSize = frameRate == 20 ? 40 : 80 // 20ms=40bytes, 40ms=80bytes - } else { - encoderFormat?.dataSize = frameRate == 20 ? 80 : 160 // 双声道翻倍 - } - + + // 每帧字节数 = 每声道码率(kbps) × 帧长(ms) ÷ 8,双声道翻倍 + // 默认 8kbps@20ms 单声道 = 20 字节/帧 + encoderFormat?.dataSize = bitrateKbps * frameRate * channels / 8 + guard let format = encoderFormat else { print("编码器格式配置失败") return false diff --git a/tools/ble_log.sh b/tools/ble_log.sh new file mode 100755 index 000000000..b5ce437b4 --- /dev/null +++ b/tools/ble_log.sh @@ -0,0 +1,289 @@ +#!/usr/bin/env bash +# +# BLE 通话翻译日志抓取 / 过滤工具 +# +# 用途:排查"通话中 BLE buff 溢出且不恢复"问题,把 Android / iOS / Dart 三处日志 +# 按同一套关键词过滤出来,边看边落盘,方便和设备端同事对时间轴。 +# +# 常用: +# ./tools/ble_log.sh android # 实时抓 Android BLE 日志(同时写文件) +# ./tools/ble_log.sh android -k # 只看通话翻译相关行(强过滤) +# ./tools/ble_log.sh ios # 实时抓 iOS 真机日志 +# ./tools/ble_log.sh pull-android # 拉 app 内的 Dart 日志文件 app_*.log +# ./tools/ble_log.sh pull-ios # 同上(iOS,需 devicectl) +# ./tools/ble_log.sh filter <日志文件> # 从已有日志里筛通话翻译相关行 +# ./tools/ble_log.sh stat <日志文件> # 快速统计:拥塞/休眠/发送节拍 +# +set -uo pipefail + +APP_ID="com.saitong.voitrans" +IOS_SUBSYSTEM="com.yunqiinnovation.ble_service" + +# Android 需要抓的 TAG(flutter 用于捞 Dart 侧 Logger 输出) +ANDROID_TAGS=( + BleService + BleCommandSender + BleServicePlugin + BleCompanionManager + OpusAudioManager + flutter +) + +# --------------------------------------------------------------------------- +# 通话翻译相关关键词。 +# 前半段是代码里已有的日志标记,后半段 CT_* 是排查期建议补充的聚合日志标记 +# (每秒吞吐 CT_RATE / 设备流控 CT_FLOW / 写入背压 CT_TX / 心跳 CT_HB)。 +# 新增日志时统一用 [CT_xxx] 前缀,这里就不用再改。 +# --------------------------------------------------------------------------- +CT_PATTERN='CT_RATE|CT_FLOW|CT_TX|CT_HB|CALL_TRANS_CMD|CALL_TRANS_HB|CALL_TRANS_DEBUG' +CT_PATTERN="$CT_PATTERN"'|音频下行发送|下行写入拥塞|下行写入已恢复|音频发送队列已满|音频发送线程|声道积压缓存' +CT_PATTERN="$CT_PATTERN"'|休眠|补发积压|恢复左声道|恢复右声道|通话翻译|编解码|心跳' +CT_PATTERN="$CT_PATTERN"'|连接参数|连接优先级|onConnectionUpdated|MTU|mtu' +CT_PATTERN="$CT_PATTERN"'|已连接|已断开|连接断开|重连|GATT' + +OUT_DIR="${BLE_LOG_DIR:-./ble_logs}" + +usage() { + # 打印文件头部的注释块(到第一个非注释行为止) + awk 'NR==1 {next} /^#/ {sub(/^# ?/, ""); print; next} {exit}' "$0" + exit "${1:-0}" +} + +die() { echo "错误: $*" >&2; exit 1; } + +ts() { date +%Y%m%d_%H%M%S; } + +need() { command -v "$1" >/dev/null 2>&1 || die "未找到命令 $1,请先安装"; } + +# 生成输出文件路径 +out_file() { + mkdir -p "$OUT_DIR" + echo "$OUT_DIR/$1_$(ts).log" +} + +# --------------------------------------------------------------------------- +# Android 实时抓取 +# --------------------------------------------------------------------------- +cmd_android() { + local keyword_only=0 all_history=0 outfile="" + while [[ $# -gt 0 ]]; do + case "$1" in + -k|--keyword) keyword_only=1; shift ;; + -a|--all) all_history=1; shift ;; + -o|--out) outfile="$2"; shift 2 ;; + *) die "未知参数 $1" ;; + esac + done + + need adb + adb get-state >/dev/null 2>&1 || die "adb 没有连接到设备(adb devices 检查一下)" + + [[ -z "$outfile" ]] && outfile="$(out_file android)" + + # 只保留关心的 TAG,其余全部静音(*:S) + local specs=() + for t in "${ANDROID_TAGS[@]}"; do specs+=("$t:V"); done + specs+=("AndroidRuntime:E" "*:S") + + echo "==> 抓取 Android 日志,落盘: $outfile" + echo " TAG: ${ANDROID_TAGS[*]}" + [[ $keyword_only -eq 1 ]] && echo " 模式: 仅通话翻译关键词" + echo " Ctrl+C 结束" + echo + + # -T 1 表示从当前时刻开始,避免历史日志刷屏;-a 则抓取缓冲区里的全部历史 + local head_args=(-v threadtime) + [[ $all_history -eq 0 ]] && head_args+=(-T 1) + + adb logcat -c 2>/dev/null || true + if [[ $keyword_only -eq 1 ]]; then + adb logcat "${head_args[@]}" "${specs[@]}" \ + | grep --line-buffered -E "$CT_PATTERN" \ + | tee "$outfile" + else + adb logcat "${head_args[@]}" "${specs[@]}" | tee "$outfile" + fi +} + +# --------------------------------------------------------------------------- +# iOS 实时抓取 +# 真机优先用 idevicesyslog(libimobiledevice,brew install libimobiledevice) +# 模拟器 / 本机用 log stream +# --------------------------------------------------------------------------- +cmd_ios() { + local keyword_only=0 use_sim=0 outfile="" + while [[ $# -gt 0 ]]; do + case "$1" in + -k|--keyword) keyword_only=1; shift ;; + --sim) use_sim=1; shift ;; + -o|--out) outfile="$2"; shift 2 ;; + *) die "未知参数 $1" ;; + esac + done + + [[ -z "$outfile" ]] && outfile="$(out_file ios)" + echo "==> 抓取 iOS 日志,落盘: $outfile" + echo " Ctrl+C 结束" + echo + + local filter_cmd=(cat) + [[ $keyword_only -eq 1 ]] && filter_cmd=(grep --line-buffered -E "$CT_PATTERN") + + if [[ $use_sim -eq 1 ]]; then + # 模拟器 / 本机:按 subsystem + 进程名过滤 + log stream --style syslog --level debug \ + --predicate "subsystem == \"$IOS_SUBSYSTEM\" OR process == \"Runner\"" \ + | "${filter_cmd[@]}" | tee "$outfile" + elif command -v idevicesyslog >/dev/null 2>&1; then + # 真机:idevicesyslog 最稳,能同时拿到 os_log 和 Dart print + idevicesyslog -m "" 2>/dev/null \ + | grep --line-buffered -E "ble_service|BleService|Runner|flutter|$CT_PATTERN" \ + | "${filter_cmd[@]}" | tee "$outfile" + else + echo "提示: 未安装 idevicesyslog,真机日志建议 brew install libimobiledevice" >&2 + echo " 现在退回 log stream(新版 macOS 对真机支持受限,若无输出请用 Console.app)" >&2 + echo >&2 + log stream --style syslog --level debug \ + --predicate "subsystem == \"$IOS_SUBSYSTEM\" OR process == \"Runner\"" \ + | "${filter_cmd[@]}" | tee "$outfile" + fi +} + +# --------------------------------------------------------------------------- +# 拉取 app 内的 Dart 日志文件(Logger 写的 app_YYYY-MM-DD*.log) +# --------------------------------------------------------------------------- +cmd_pull_android() { + need adb + adb get-state >/dev/null 2>&1 || die "adb 没有连接到设备" + + local dest="$OUT_DIR/dart_android_$(ts)" + mkdir -p "$dest" + + # getApplicationDocumentsDirectory() → /data/data//app_flutter + local remote_dir="app_flutter/logs" + echo "==> 从 $APP_ID 拉取 $remote_dir → $dest" + + local files + files="$(adb exec-out run-as "$APP_ID" ls "$remote_dir" 2>/dev/null)" || { + die "run-as 失败。release 包不可调试,请改用 debug/profile 包,或让 app 提供日志导出入口" + } + [[ -z "$files" ]] && die "设备上没有日志文件(app 可能还没初始化 Logger)" + + while IFS= read -r f; do + [[ -z "$f" ]] && continue + echo " - $f" + adb exec-out run-as "$APP_ID" cat "$remote_dir/$f" > "$dest/$f" + done <<< "$files" + + echo + echo "完成: $dest" + ls -lh "$dest" +} + +cmd_pull_ios() { + need xcrun + local dest="$OUT_DIR/dart_ios_$(ts)" + mkdir -p "$dest" + + local udid + udid="$(xcrun devicectl list devices 2>/dev/null | awk '/connected/ {print $(NF-1); exit}')" + [[ -z "$udid" ]] && die "未找到已连接的 iOS 设备(xcrun devicectl list devices 检查一下)" + + echo "==> 从设备 $udid 拉取 Documents/logs → $dest" + xcrun devicectl device copy from \ + --device "$udid" \ + --domain-type appDataContainer \ + --domain-identifier "$APP_ID" \ + --source "Documents/logs" \ + --destination "$dest" \ + || die "拉取失败。备选方案:Xcode → Window → Devices and Simulators → 选中 app → Download Container" + + echo "完成: $dest" + ls -lh "$dest" +} + +# --------------------------------------------------------------------------- +# 离线过滤 / 统计 +# --------------------------------------------------------------------------- +cmd_filter() { + local f="${1:-}" + [[ -f "$f" ]] || die "用法: $0 filter <日志文件>" + grep -E "$CT_PATTERN" "$f" +} + +cmd_stat() { + local f="${1:-}" + [[ -f "$f" ]] || die "用法: $0 stat <日志文件>" + + echo "=========== 通话翻译日志速览: $f ===========" + echo + echo "-- 关键事件计数 --" + local -a keys=( + '通话翻译指令下发' + '音频下行发送' + '下行写入拥塞' + '下行写入已恢复' + '音频发送队列已满' + '声道积压缓存超上限' + '收到左声道恢复指令' + '收到右声道恢复指令' + '心跳连续未发出' + '连接参数' + '连接断开' + ) + for k in "${keys[@]}"; do + printf ' %-24s %s\n' "$k" "$(grep -c -- "$k" "$f" 2>/dev/null || true)" + done + + echo + echo "-- 设备流控(休眠/恢复配对)--" + local sleep_n resume_n + sleep_n="$(grep -cE '休眠|SLEEP_LEFT|SLEEP_RIGHT|0xF0|0xF1' "$f" 2>/dev/null || true)" + resume_n="$(grep -cE '恢复左声道|恢复右声道|0xF2|0xF3' "$f" 2>/dev/null || true)" + echo " 休眠(F0/F1) = $sleep_n 恢复(F2/F3) = $resume_n" + if [[ "$sleep_n" -gt "$resume_n" ]]; then + echo " ⚠️ 休眠多于恢复 $((sleep_n - resume_n)) 次 —— resume 指令可能在拥塞时丢了," + echo " 声道会一直卡在休眠态,这正是「怎么都不恢复」的典型形态,重点看最后一次休眠的时刻。" + fi + + echo + echo "-- 下行发送节拍 Δ 分布(正常应贴近配置的发送间隔)--" + grep -o 'Δ=[0-9]\+ms' "$f" 2>/dev/null | grep -o '[0-9]\+' | awk ' + { n++; s+=$1; if($1>max) max=$1; + if($1<=50) b1++; else if($1<=100) b2++; else if($1<=300) b3++; + else if($1<=1000) b4++; else b5++ } + END { + if (n==0) { print " (没有匹配到「音频下行发送」行,先确认日志已打开)"; exit } + printf " 样本=%d 均值=%.1fms 最大=%dms\n", n, s/n, max + printf " <=50ms: %d 51-100ms: %d 101-300ms: %d 301-1000ms: %d >1s: %d\n", + b1+0, b2+0, b3+0, b4+0, b5+0 + if (b4+b5 > 0) print " ⚠️ 存在 >300ms 的发送空档,对照同一时刻的拥塞/休眠日志看是哪一类原因" + }' + + echo + echo "-- 积压水位变化(hold(L=..,R=..),持续上涨即为发不出去)--" + grep -o 'hold(L=[0-9]\+,R=[0-9]\+)' "$f" 2>/dev/null | tail -20 || true + + echo + echo "-- 最后 15 条拥塞/丢包告警 --" + grep -E '下行写入拥塞|音频发送队列已满|声道积压缓存超上限|心跳连续未发出' "$f" 2>/dev/null | tail -15 || true + echo +} + +# --------------------------------------------------------------------------- +main() { + local sub="${1:-}" + [[ $# -gt 0 ]] && shift + case "$sub" in + android) cmd_android "$@" ;; + ios) cmd_ios "$@" ;; + pull-android) cmd_pull_android "$@" ;; + pull-ios) cmd_pull_ios "$@" ;; + filter) cmd_filter "$@" ;; + stat) cmd_stat "$@" ;; + -h|--help|help|"") usage 0 ;; + *) echo "未知子命令: $sub" >&2; usage 1 ;; + esac +} + +main "$@"