import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:device_jieli/device_jieli.dart'; import 'package:agent_service/agent_service.dart'; import '/data/models/appconfig_model.dart'; import '../models/user_Info.dart' as userInfo; import '../../data/models/appconfig.dart'; import '../../core/translations/app_translations.dart'; import '../../core/utils/logger.dart'; import 'language_manager.dart'; import 'network/dio_manager.dart'; import '../../routes/app_routes.dart'; /// BLE 连接状态常量(保持外部 API 兼容) class BleConnectionState { static const int disconnected = 0; static const int connecting = 1; static const int connected = 2; static const int disconnecting = 3; static const int error = -1; } /// 设备信息缓存(精简后版本,去掉唤醒词字段) class DeviceInfo { final String address; final String name; final int batteryLevel; final int leftEarBattery; final int rightEarBattery; final int caseBattery; final bool leftEarCharging; final bool rightEarCharging; final bool caseCharging; final String firmwareVersion; final String firmwareProductId; final String firmwareProductColor; final DateTime lastUpdated; DeviceInfo({ required this.address, required this.name, this.batteryLevel = 0, this.leftEarBattery = 0, this.rightEarBattery = 0, this.caseBattery = 0, this.leftEarCharging = false, this.rightEarCharging = false, this.caseCharging = false, this.firmwareVersion = '', this.firmwareProductId = '', this.firmwareProductColor = '', DateTime? lastUpdated, }) : lastUpdated = lastUpdated ?? DateTime.now(); DeviceInfo copyWith({ String? address, String? name, int? batteryLevel, int? leftEarBattery, int? rightEarBattery, int? caseBattery, bool? leftEarCharging, bool? rightEarCharging, bool? caseCharging, String? firmwareVersion, String? firmwareProductId, String? firmwareProductColor, }) { return DeviceInfo( address: address ?? this.address, name: name ?? this.name, batteryLevel: batteryLevel ?? this.batteryLevel, leftEarBattery: leftEarBattery ?? this.leftEarBattery, rightEarBattery: rightEarBattery ?? this.rightEarBattery, caseBattery: caseBattery ?? this.caseBattery, leftEarCharging: leftEarCharging ?? this.leftEarCharging, rightEarCharging: rightEarCharging ?? this.rightEarCharging, caseCharging: caseCharging ?? this.caseCharging, firmwareVersion: firmwareVersion ?? this.firmwareVersion, firmwareProductId: firmwareProductId ?? this.firmwareProductId, firmwareProductColor: firmwareProductColor ?? this.firmwareProductColor, lastUpdated: DateTime.now(), ); } @override String toString() => 'DeviceInfo(addr: $address, name: $name, battery: $batteryLevel%, fw: $firmwareVersion)'; } /// 杰理蓝牙管理器(替换旧的 ble_service 实现,对外保留 BleManager 名字) class BleManager extends GetxService { static BleManager get to => Get.find(); // ─── 连接信息 ──────────────────────────────────────── String? currentDeviceAddress; String? _currentDeviceName; JieliDevice? _connectedDevice; JieliDevice? get connectedDevice => _connectedDevice; final _connectionState = BleConnectionState.disconnected.obs; int get connectionState => _connectionState.value; RxInt get connectionStateRx => _connectionState; final _isCall = false.obs; final _isConnected = false.obs; bool get isConnected => _isConnected.value; bool get isCall => _isCall.value; final deviceInfo = Rx(null); // ─── 通话翻译/编解码器(杰理 startTranslation 模式状态)───── final _isCodecActive = false.obs; bool get isCodecActive => _isCodecActive.value; RxBool get isCodecActiveRx => _isCodecActive; // 设备主动上报通话翻译开关信号(耳机端按键) final StreamController _callTranslationRequestController = StreamController.broadcast(); Stream get onCallTranslationRequest => _callTranslationRequestController.stream; // 通话翻译模式下,杰理 runtime 解码后推上来的 PCM: // - in.uplink = 本机用户麦克风(己方/A 路) // - in.downlink = 对端通话音(对方/B 路) // 由 translation_controller 转发到 azure_speech 的 AST 流水线。 final StreamController _inUplinkPcmController = StreamController.broadcast(); final StreamController _inDownlinkPcmController = StreamController.broadcast(); Stream get inUplinkPcmStream => _inUplinkPcmController.stream; Stream get inDownlinkPcmStream => _inDownlinkPcmController.stream; // ─── 扫描结果(供设备页扫描列表使用)───────────────────── final RxList scannedDevices = [].obs; final RxBool isScanning = false.obs; /// 每个地址最近一次扫到的 advertisement(电量等)。 /// scannedDevices 只追加不更新,但电量在每次 advertisement 都可能变; /// 这里保留最新一次,供连接成功时填到 DeviceInfo。 final Map _latestAdvByAddress = {}; StreamSubscription? _eventSub; final GetStorage _storage = GetStorage(); Future? _initFuture; bool _initSucceeded = false; String? _initError; String? get initError => _initError; bool get isInitialized => _initSucceeded; /// 确保杰理 SDK native 端已 initialize。失败则重试一次。 /// 失败时抛出 [StateError] —— 调用方负责 try-catch / UI 反馈。 Future ensureInitialized() async { if (_initSucceeded) return; // 上一次 future 还在 in-flight 或失败 → 重新启动一次 if (_initFuture == null) { _initFuture = _initializeJieli(); } try { await _initFuture!.timeout(const Duration(seconds: 8)); } catch (e) { _initError = e.toString(); _initFuture = null; // 允许下次重试 throw StateError('杰理 SDK 初始化失败: $_initError'); } if (!_initSucceeded) { _initFuture = null; throw StateError('杰理 SDK 初始化失败: ${_initError ?? "unknown"}'); } } /// 兼容旧 API(部分调用点不关心初始化结果) Future get ready async { try { await ensureInitialized(); } catch (_) { // 静默:让后续 native 调用自身报错 } } BleManager() { print('[BLE] BleManager 构造,立即启动杰理 SDK 初始化'); _initFuture = _initializeJieli(); _setupEventListener(); _startConnectionWatchdog(); } @override void onInit() { super.onInit(); print('[BLE] BleManager.onInit (init 已在构造时启动)'); } @override void onClose() { _eventSub?.cancel(); _stopBatteryWatchdog(); _stopConnectionWatchdog(); _callTranslationRequestController.close(); _inUplinkPcmController.close(); _inDownlinkPcmController.close(); super.onClose(); } /// 把外部 AST 服务返回的 TTS PCM 灌回耳机。 /// - leg=='A' / 'uplink' → 对端听(SOURCE_E_SCO_UP_LINK) /// - leg=='B' / 'downlink' → 本机用户听(SOURCE_E_SCO_DOWN_LINK) /// /// 拿到多少就送多少,不做节流也不补静音;底层 SDK / RCSP runtime 自己管 /// 整段 OPUS 编码 + 串行 writeAudioData 节奏。 Future feedAstTtsPcm({ required String leg, required Uint8List pcm, int sampleRate = 16000, int channels = 1, bool isFinal = false, }) async { // 段尾帧(pcm 为空 + isFinal=true)必须放行,让 native 触发立即整段下发; // 仅对“普通空帧(非段尾)”短路。 if (pcm.isEmpty && !isFinal) return true; final upper = leg.toUpperCase(); final streamId = (upper == 'A' || upper == 'UPLINK') ? 'out.uplink' : 'out.downlink'; final isFlush = isFinal; try { final ok = await Jielihome.instance.feedTranslatedAudio( streamId: streamId, pcm: pcm, sampleRate: sampleRate, channels: channels, isFinal: isFinal, ); if (isFlush || !ok) { // 只在段尾 flush 或 native 返回 false 时打,避免刷屏。 Logger.info( '[BleManager] feedAstTtsPcm leg=$leg streamId=$streamId pcm=${pcm.length}B isFinal=$isFinal nativeOk=$ok', ); } return ok; } catch (e) { Logger.w('BleManager', 'feedAstTtsPcm leg=$leg failed: $e'); return false; } } Future _initializeJieli() async { try { print('[BLE] [INIT] 调用 Jielihome.initialize ...'); // 与参考项目 ai-agent-client 保持一致:使用 SDK 默认配置(multiDevice=true, skipNoNameDev=false) // 不要清空 BluetoothOption.scanFilterData —— 那是杰理协议广播标识,清空反而扫不到 await Jielihome.instance.initialize(); _initSucceeded = true; _initError = null; print('[BLE] [INIT] 杰理 SDK 初始化完成'); } catch (e, s) { _initSucceeded = false; _initError = e.toString(); print('[BLE] [INIT] 杰理 SDK 初始化失败: $e\n$s'); rethrow; } } void _setupEventListener() { print('[BLE] [EVT] 准备订阅 Jielihome.events ...'); _eventSub?.cancel(); _eventSub = Jielihome.instance.events.listen( (event) { // TranslationAudioEvent 频率极高(10ms/帧),最先拦截并单独走节流日志处理器, // 避免进入下面的 print / if‑链路经引发刷屏。 if (event is TranslationAudioEvent) { _handleTranslationAudio(event); return; } // 总开关日志:每一个事件都打一次,便于诊断 native → dart 通道是否畅通 print('[BLE] [EVT] ${event.runtimeType}'); if (event is AdapterStatusEvent) { print('[BLE] [EVT] 适配器状态 enabled=${event.enabled} hasBle=${event.hasBle}'); } else if (event is ScanStatusEvent) { print('[BLE] [EVT] 扫描状态变化 ble=${event.ble} started=${event.started}'); isScanning.value = event.started; } else if (event is DeviceFoundEvent) { final found = event.device; final exists = scannedDevices.any((d) => d.address == found.address); // 每次广播都用最新数据覆盖(后续若 SDK 扩展了广播字段,仍可从这里拿到) _latestAdvByAddress[found.address] = found; print('[BLE] [EVT] deviceFound name="${found.name}" addr=${found.address} ' 'rssi=${found.rssi} ' '(已存在=$exists, 当前列表 size=${scannedDevices.length})'); if (!exists) { scannedDevices.add(found); print('[BLE] [EVT] 新设备已加入列表,size=${scannedDevices.length}'); } } else if (event is ConnectionStateEvent) { _handleConnectionState(event); } else if (event is RcspInitEvent) { print('[BLE] [EVT] RCSP 初始化: ${event.address} success=${event.success}'); if (event.success) { // RCSP init 完成后 SDK 才开始往缓存里填电量;这里启动轮询直到拿到非 0 _startBatteryWatchdog(); // 同时启动 AgentService(azureAsrHelper / TTS / chatApi 在这里建出来)。 // native initialize 自带幂等保护,重连重复触发是 no-op; // 老的 PairingController 删除时这一调用丢了,不补回来 AI 助理 / PTT // 等所有 ASR 路径都会因为 azureAsrHelper=null 静默失败。 startAgentService().then((ok) { Logger.i('BleManager', 'startAgentService on RCSP init: ok=$ok'); }).catchError((e) { Logger.e('BleManager', 'startAgentService on RCSP init 失败: $e'); }); } } else if (event is BatteryEvent) { Logger.i('BleManager', '[EVT] BatteryEvent addr=${event.address} level=${event.level}'); _handleBattery(event); } else if (event is PhoneCallStatusEvent) { _isCall.value = event.status != 0; } else if (event is ExpandFunctionEvent) { _handleExpandFunction(event); } else if (event is TranslationLogEvent) { print('[BLE] [EVT] TranslationLog modeId=${event.modeId} ${event.content}'); } else if (event is TranslationErrorEvent) { print('[BLE] [EVT] TranslationError modeId=${event.modeId} ' 'code=${event.code} msg=${event.message}'); } else if (event is UnknownJieliEvent) { print('[BLE] [EVT] 未识别事件: ${event.raw}'); } }, onError: (e) => print('[BLE] [EVT] 杰理事件流错误: $e'), onDone: () => print('[BLE] [EVT] 杰理事件流被关闭 (onDone)'), ); print('[BLE] [EVT] 订阅完成 sub=$_eventSub'); } void _handleConnectionState(ConnectionStateEvent e) { // 关键诊断日志:原生 → Dart 链路上的最终一站。如果这里没打印,意味着 // 原生侧 `connectionState` 事件根本没到达 Dart(看 iOS `[JieliDispatcher] // send #N type=connectionState` 是否成功 sink-ok)。 print('[BLE] [EVT] >>> _handleConnectionState addr=${e.address} state=${e.state} ' '(0=disconnect 1=ok 2=connecting)'); Logger.i('BleManager', 'JL 连接状态: ${e.address} → ${e.state}'); if (e.state == ConnectionStateEvent.connectionOk) { currentDeviceAddress = e.address; _connectionState.value = BleConnectionState.connected; _isConnected.value = true; print('[BLE] [EVT] >>> isConnected 翻成 true, currentDeviceAddress=${e.address}'); _ensureDeviceInfoStub(); // 用扫描期间最新一条 advertisement 立刻填上电量(SDK BleScanMessage 里就有 // leftDeviceQuantity / rightDeviceQuantity / chargingBinQuantity / 各 charging 位) _fillBatteryFromAdvertisement(e.address); // 不在这里 fetchDeviceInfo —— RCSP 还没 init,SDK 缓存里的电量是 0; // 改到 RcspInitEvent 成功时再拉一次 + 之后再轮询几次保险。 } else if (e.state == ConnectionStateEvent.connectionConnecting) { _connectionState.value = BleConnectionState.connecting; print('[BLE] [EVT] >>> 连接中 (state=2)'); } else { _connectionState.value = BleConnectionState.disconnected; _isConnected.value = false; _isCodecActive.value = false; _stopBatteryWatchdog(); print('[BLE] [EVT] >>> isConnected 翻成 false (state=${e.state})'); } } // 连接 + RCSP init 之后 SDK 缓存才开始填电量;BatteryEvent 在某些设备上 // 又只在变化时才推。所以连上后主动轮询几次拉快照,拿到非 0 就停。 Timer? _batteryWatchdog; int _batteryWatchdogTicks = 0; static const int _batteryWatchdogMaxTicks = 12; // 12 × 2.5s = 30s static const Duration _batteryWatchdogInterval = Duration(milliseconds: 2500); void _startBatteryWatchdog() { _stopBatteryWatchdog(); _batteryWatchdogTicks = 0; Logger.i('BleManager', '[BAT] 启动电量轮询'); // 立刻先拉一次,再启周期任务 Future.microtask(fetchDeviceInfo); _batteryWatchdog = Timer.periodic(_batteryWatchdogInterval, (t) async { _batteryWatchdogTicks++; final lvl = deviceInfo.value?.batteryLevel ?? 0; if (lvl > 0) { Logger.i('BleManager', '[BAT] 拿到电量=$lvl%,停止轮询 (tick=$_batteryWatchdogTicks)'); _stopBatteryWatchdog(); return; } if (_batteryWatchdogTicks >= _batteryWatchdogMaxTicks) { Logger.w('BleManager', '[BAT] 30s 内未拿到电量,停止轮询 (tick=$_batteryWatchdogTicks)'); _stopBatteryWatchdog(); return; } Logger.d('BleManager', '[BAT] 轮询拉快照 tick=$_batteryWatchdogTicks'); await fetchDeviceInfo(); }); } void _stopBatteryWatchdog() { _batteryWatchdog?.cancel(); _batteryWatchdog = null; } // 连接状态 watchdog:杰理 SDK 在某些时序下不会回调 ConnectionStateEvent // (冷启时设备已经连着、事件订阅 race、原生侧自动重连等),导致本地 Rx // 状态停在 disconnected 但 SDK 内部已经有连接。这里定期直接问 SDK。 // // 重要:只做"单向"同步(disconnected → connected),**不**把已连状态翻回 // disconnected。原因:multi-device 模式下 connectedDevice() 偶发会返回 null, // 上一版把它当真翻成 disconnected 反而把已连接 UI 抹掉了。断开仍走 // ConnectionStateEvent.connectionDisconnect 那条事件路径。 Timer? _connectionWatchdog; static const Duration _connectionWatchdogInterval = Duration(seconds: 5); void _startConnectionWatchdog() { _connectionWatchdog?.cancel(); _connectionWatchdog = Timer.periodic(_connectionWatchdogInterval, (_) { _reconcileConnectionFromSdk(); }); } void _stopConnectionWatchdog() { _connectionWatchdog?.cancel(); _connectionWatchdog = null; } Future _reconcileConnectionFromSdk() async { if (!_initSucceeded) return; try { // 步骤 1:尽量找到一个真实在连的 address。 // 优先:本地已有 currentDeviceAddress → 直接 isConnected(addr) 校验。 // 兜底:connectedDevice() 看 SDK 是否能给出当前已连设备。 String? sdkConnectedAddr; String? sdkConnectedName; final localAddr = currentDeviceAddress; if (localAddr != null && localAddr.isNotEmpty) { if (await Jielihome.instance.isConnected(localAddr)) { sdkConnectedAddr = localAddr; sdkConnectedName = _currentDeviceName; } } if (sdkConnectedAddr == null) { final dev = await Jielihome.instance.connectedDevice(); if (dev != null && dev.address.isNotEmpty) { sdkConnectedAddr = dev.address; sdkConnectedName = dev.name; } } if (sdkConnectedAddr == null) return; // 没拿到,本轮放弃,**不**翻 _isConnected // 步骤 2:本地 Rx 与 SDK 不一致 → 强制同步到「已连」。 final wasConnected = _isConnected.value; final addressChanged = currentDeviceAddress != sdkConnectedAddr; if (!wasConnected || addressChanged) { Logger.w('BleManager', '[WATCHDOG] 本地 isConnected=$wasConnected addr=$currentDeviceAddress' ' / SDK 在连 $sdkConnectedAddr → 同步为 connected'); currentDeviceAddress = sdkConnectedAddr; if (sdkConnectedName != null && sdkConnectedName.isNotEmpty) { _currentDeviceName = sdkConnectedName; } _isConnected.value = true; _connectionState.value = BleConnectionState.connected; _ensureDeviceInfoStub(); } // 步骤 3:兜底电量。冷启场景下 RcspInitEvent 不会再来, // _startBatteryWatchdog 永远不被触发,电量 UI 一直 0。 // 这里发现 SDK 在连且本地电量为 0 时,主动启动一次电量轮询。 final lvl = deviceInfo.value?.batteryLevel ?? 0; if (lvl == 0 && _batteryWatchdog == null) { Logger.i('BleManager', '[WATCHDOG] 已连但电量=0,启动电量轮询补抓'); _startBatteryWatchdog(); } } catch (e) { Logger.w('BleManager', '[WATCHDOG] 查询 SDK 连接状态失败: $e'); } } void _handleBattery(BatteryEvent e) { final level = e.level ?? 0; _ensureDeviceInfoStub(); // 杰理 SDK 的 BatteryInfo 只有总电量;左右耳/盒子的精细数据来自 BleScanMessage(见 _fillBatteryFromAdvertisement)。 // 这里更新总电量;如果当前左右耳还是 0,也镜像一下作为兜底。 final cur = deviceInfo.value; if (cur == null) return; deviceInfo.value = cur.copyWith( batteryLevel: level, leftEarBattery: cur.leftEarBattery > 0 ? cur.leftEarBattery : level, rightEarBattery: cur.rightEarBattery > 0 ? cur.rightEarBattery : level, ); } /// 杰理同步后的 `JieliDevice` 仅暴露 name / address / rssi 等基础字段, /// 不再提供左右耳/仓的电量与充电状态。电量改由以下两路拿: /// 1. 连接成功后 RCSP 初始化完成 → `_startBatteryWatchdog` 轮询 /// `fetchDeviceInfo` 拉 `deviceSnapshot` / `queryTargetInfo`; /// 2. SDK 主动推送的 [BatteryEvent](见 `_handleBattery`)。 /// 所以这里不再从扫描广播里预填电量,保留空实现以兼容既有调用点。 void _fillBatteryFromAdvertisement(String address) { final adv = _latestAdvByAddress[address]; if (adv == null) { Logger.w('BleManager', '[BAT] 没有 $address 的 advertisement 数据可用'); return; } Logger.i('BleManager', '[BAT] 广播不再携带电量字段,连接后将通过 RCSP/BatteryEvent 补齐'); } int _uplinkRxCount = 0; int _downlinkRxCount = 0; void _handleTranslationAudio(TranslationAudioEvent e) { // streamId 来自 native TranslationStreams:in.uplink / in.downlink / in.mic ... // 仅转发通话翻译两条输入流;out.* 由这里产生回灌指令而不是接收。 switch (e.streamId) { case 'in.uplink': _uplinkRxCount++; if (_uplinkRxCount <= 5 || _uplinkRxCount % 100 == 1) { Logger.i('CALL-RX', '上行 #$_uplinkRxCount len=${e.pcm.length} sr=${e.sampleRate} ch=${e.channels} bits=${e.bitsPerSample} seq=${e.seq}'); } if (!_inUplinkPcmController.isClosed) { _inUplinkPcmController.add(e.pcm); } break; case 'in.downlink': _downlinkRxCount++; if (_downlinkRxCount <= 5 || _downlinkRxCount % 100 == 1) { Logger.i('CALL-RX', '下行 #$_downlinkRxCount len=${e.pcm.length} sr=${e.sampleRate} ch=${e.channels} bits=${e.bitsPerSample} seq=${e.seq}'); } if (!_inDownlinkPcmController.isClosed) { _inDownlinkPcmController.add(e.pcm); } break; } } void _handleExpandFunction(ExpandFunctionEvent e) { if (e.opCode == 0x16) { _emitCallTranslationRequest(true); } else if (e.opCode == 0x17) { _emitCallTranslationRequest(false); } } void _emitCallTranslationRequest(bool enable) { Logger.i('BleManager', '[CALL_TRANS_CMD] 设备上报${enable ? "开启" : "关闭"}通话翻译'); if (!_callTranslationRequestController.isClosed) { _callTranslationRequestController.add(enable); } if (enable) { final currentRoute = Get.currentRoute; if (currentRoute != Routes.translation) { Get.toNamed( Routes.translation, arguments: {'translationId': 'call', 'autoStart': true}, ); } } } void _ensureDeviceInfoStub() { if (deviceInfo.value == null && currentDeviceAddress != null) { deviceInfo.value = DeviceInfo( address: currentDeviceAddress!, name: _currentDeviceName ?? '', ); } } // ─── 扫描 / 连接 ──────────────────────────────────── /// 启动扫描。 /// /// 扫描的 service UUID 白名单来源: /// 1) 调用方显式传入 [uuids](通常用于调试或定向扫描); /// 2) 否则根据 [productIds] 从 [AppConfig.config.products] 映射出每个产品 /// 的 `scanuuid`(服务端下发的产品配置),在 Flutter 侧拼接成 uuids 列表 /// 再通过平台通道下发给底层,由 Android `ScanFilter.setServiceUuid` /// 进行 OS 级过滤; /// 3) 如果上面两项都没给,就默认使用 [AppConfig.allproductUuids()],等价 /// 于「下发所有已配置产品」的 uuid 白名单。 /// /// 空列表 / AppConfig 未初始化时,底层不做 uuid 过滤,全部放行。 Future startScan({ Duration timeout = const Duration(seconds: 30), List? uuids, List? productIds, }) async { print('[BLE] [SCAN] BleManager.startScan 入口 ' 'productIds=$productIds uuids=$uuids'); _lastScanError = null; try { await ensureInitialized(); } catch (e) { print('[BLE] [SCAN] 杰理 SDK 未初始化: $e'); _lastScanError = e.toString(); isScanning.value = false; return false; } final List resolvedUuids; if (uuids != null && uuids.isNotEmpty) { resolvedUuids = uuids.where((u) => u.isNotEmpty).toList(growable: false); } else if (productIds != null && productIds.isNotEmpty && AppConfig.isInitialized()) { final ids = productIds.toSet(); resolvedUuids = AppConfig.config.products .where((p) => ids.contains(p.id)) .map((p) => p.scanuuid) .where((u) => u.isNotEmpty) .toList(growable: false); } else if (AppConfig.isInitialized()) { resolvedUuids = AppConfig.allproductUuids() .where((u) => u.isNotEmpty) .toList(growable: false); } else { resolvedUuids = const []; } print('[BLE] [SCAN] init 已通过,UUID 白名单=$resolvedUuids'); scannedDevices.clear(); isScanning.value = true; try { await Jielihome.instance.startScan( timeout: timeout, uuidList: resolvedUuids, skipUnnamed: true, ); print('[BLE] [SCAN] native startScan 返回成功,等待 deviceFound 事件'); return true; } catch (e, s) { print('[BLE] [SCAN] 启动扫描失败: $e\n$s'); isScanning.value = false; _lastScanError = e.toString(); return false; } } String? _lastScanError; String? get lastScanError => _lastScanError; Future stopScan() async { await ready; try { await Jielihome.instance.stopScan(); } catch (e) { Logger.e('BleManager', '停止扫描失败: $e'); } finally { isScanning.value = false; } } Future connectDevice(JieliDevice device) async { await ready; _currentDeviceName = device.name; currentDeviceAddress = device.address; _connectedDevice = device; _connectionState.value = BleConnectionState.connecting; try { await Jielihome.instance.connect(device); } catch (e) { Logger.e('BleManager', '连接设备失败: $e'); _connectionState.value = BleConnectionState.error; } } Future disconnect() async { await ready; try { final addr = currentDeviceAddress; if (addr != null) { await Jielihome.instance.disconnect(addr); } } catch (e) { Logger.e('BleManager', '断开连接失败: $e'); } } Future getConnectionState() async { final addr = currentDeviceAddress; if (addr == null) return BleConnectionState.disconnected; try { final connected = await Jielihome.instance.isConnected(addr); _connectionState.value = connected ? BleConnectionState.connected : BleConnectionState.disconnected; _isConnected.value = connected; return _connectionState.value; } catch (e) { Logger.e('BleManager', '查询连接状态失败: $e'); return BleConnectionState.error; } } Future isBleConnected() async { final addr = currentDeviceAddress; if (addr == null) return false; try { return await Jielihome.instance.isConnected(addr); } catch (_) { return false; } } /// 探查当前是否已经有杰理设备连着(用于登录/启动后判断是否需要跳转扫描页)。 /// 通过 [Jielihome.connectedDevice] 同步 SDK 已知的连接状态。 Future hasActiveConnection() async { await ready; try { final dev = await Jielihome.instance.connectedDevice(); if (dev != null && dev.address.isNotEmpty) { currentDeviceAddress = dev.address; _currentDeviceName = dev.name; _connectedDevice = dev; _isConnected.value = true; _connectionState.value = BleConnectionState.connected; _ensureDeviceInfoStub(); return true; } } catch (e) { Logger.w('BleManager', 'hasActiveConnection 探查失败: $e'); } return false; } // ─── 设备信息 ─────────────────────────────────────── String getDeviceName() { return deviceInfo.value?.name ?? _currentDeviceName ?? ''; } Future fetchDeviceInfo() async { final addr = currentDeviceAddress; if (addr == null) return; try { final snap = await Jielihome.instance.deviceSnapshot(addr) ?? {}; final name = (snap['name'] as String?) ?? _currentDeviceName ?? ''; final battery = (snap['battery'] as num?)?.toInt() ?? (snap['batteryLevel'] as num?)?.toInt() ?? 0; final fw = (snap['firmwareVersion'] as String?) ?? (snap['version'] as String?) ?? ''; final pid = (snap['productId'] as String?) ?? ''; _ensureDeviceInfoStub(); // 同 _handleBattery:杰理 SDK 只暴露单个总电量,把它同时镜像到左右耳, // home_view 才能从 leftEarBattery / rightEarBattery 读到非零值。 deviceInfo.value = (deviceInfo.value ?? DeviceInfo(address: addr, name: name)) .copyWith( name: name, batteryLevel: battery, leftEarBattery: battery, rightEarBattery: battery, firmwareVersion: fw, firmwareProductId: pid, ); Logger.i('BleManager', '设备快照: name=$name battery=$battery fw=$fw pid=$pid'); } catch (e) { Logger.w('BleManager', '获取设备快照失败: $e'); } } Future fetchBatteryInfo() async => true; Future fetchDeviceVersion() async => true; Future getProductId() async => true; Future getBatteryLevel() async => deviceInfo.value?.batteryLevel ?? 0; void clearDeviceInfo() { deviceInfo.value = null; currentDeviceAddress = null; _currentDeviceName = null; _connectedDevice = null; } // ─── 编解码器 / 通话翻译模式 ─────────────────────────── // 杰理:openA2DPDecoder/closeCodec 映射到 startTranslation/stopTranslation。 // 其余 codec 入口按现有调用语义保留乐观成功,以保持上层兼容。 Future openEncoder() async { _isCodecActive.value = true; return true; } Future openDecoder() async { _isCodecActive.value = true; return true; } Future openA2DPDecoder() async { Logger.i('CALL_TRANS_CMD', '【启动】通话翻译模式 (杰理 startTranslation modeId=3)'); try { await Jielihome.instance.startTranslation( modeId: TranslationModeIds.callTranslation, ); _isCodecActive.value = true; Logger.i('CALL_TRANS_CMD', '✅【启动成功】通话翻译模式已下发 — 接下来等耳机回上行音频'); return true; } catch (e) { Logger.e('CALL_TRANS_CMD', '❌【启动失败】通话翻译模式: $e'); _isCodecActive.value = false; return false; } } Future openCallRecordDecoder() async { _isCodecActive.value = true; return true; } Future closeCodec() async { _isCodecActive.value = false; try { await Jielihome.instance.stopTranslation(); } catch (_) { // 静默失败:未启动时调 stop 是常态 } return true; } Future playA2DP() async => true; // ─── OTA ─────────────────────────────────────────── /// 杰理 OTA 通过 Jielihome.otaStart 触发,此处保留空实现以兼容旧调用点。 Future setEnterOta(bool enterOta) async {} // ─── 兼容存根 ─────────────────────────────────────── Future requestBluetoothPermission() async => true; /// 旧 Companion 关联 / 后台扫描接口已废弃 — 保留空实现以兼容 /// 即将被移除的调用点(pairing 模块已删除)。 Future clearPairedDevices() async { await disconnect(); return true; } // ─── AgentService 启动 / 停止(保持原逻辑) ─────────── Future startAgentService() async { try { var serverurl = DioManager().baseUrl; var jsonString = _storage.read("logintoken"); final azureSpeechKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; final azureSpeechRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? 'eastasia'; final openaiApiKey = AppConfig.env('OPENAI_API_KEY') ?? ''; final openaiBaseUrl = AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1'; final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo'; final openaiModelMedia = AppConfig.env('OPENAI_API_VISION_MODEL') ?? 'doubao-1-5-vision-pro-32k-250115'; final openaiagentid = AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant'; final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid); var musicServiceType = AppConfig.env('APP_ANDROID_MUSICSERVICE_TYPE') ?? 'AUTO'; if (Platform.isIOS) { musicServiceType = AppConfig.env('APP_IOS_MUSICSERVICE_TYPE') ?? 'AUTO'; } final navigationMode = AppConfig.env('APP_NAVIGATION_MODE') ?? 'external_auto'; final xunfeiAppId = AppConfig.env('XUNFEI_ASR_APP_ID') ?? ''; final xunfeiAccessKeyId = AppConfig.env('XUNFEI_ASR_ACCESS_KEY_ID') ?? ''; final xunfeiAccessKeySecret = AppConfig.env('XUNFEI_ASR_ACCESS_KEY_SECRET') ?? ''; final jsonData = AppConfig.mcpConfig(); Logger.info('启动 AgentService,musicServiceType: $musicServiceType'); final isTtsInterruptEnabled = await _loadTtsInterrupe(); final vipExpiryTime = userInfo.User.instance.vipexptime; List supportedLanguages = []; try { final storage = GetStorage(); final languageManager = Get.find(); String? savedLanguage = storage.read('language'); if (savedLanguage != null && savedLanguage != 'system') { final parts = savedLanguage.split('_'); if (parts.length == 2) { final lang = parts[0].toLowerCase(); final country = parts[1].toUpperCase(); if (lang == 'zh' && (country == 'HK' || country == 'TW' || country == 'CN')) { supportedLanguages.add('zh-$country'); } else if (lang == 'tl' && country == 'PH') { supportedLanguages.add('fil-PH'); } else { supportedLanguages.add('$lang-$country'); } } } else { final appLocale = Get.locale ?? AppTranslations.deviceLocale(); if (appLocale != null) { final lang = appLocale.languageCode.toLowerCase(); final country = appLocale.countryCode; if (lang.toLowerCase() == 'zh' && country != null && country.isNotEmpty && (country.toUpperCase() == 'HK' || country.toUpperCase() == 'TW' || country.toUpperCase() == 'CN')) { supportedLanguages.add('zh-${country.toUpperCase()}'); } else { if (country != null && country.isNotEmpty) { final normalizedCountry = country.toUpperCase(); if (lang == 'tl' && normalizedCountry == 'PH') { supportedLanguages.add('fil-PH'); } else { supportedLanguages.add('$lang-$normalizedCountry'); } } else { final asrCode = languageManager.getAsrCodeByShortCode(lang); if (asrCode != null) { supportedLanguages.add(asrCode); } else { supportedLanguages.add('en-US'); } } } } } if (supportedLanguages.isEmpty) { supportedLanguages.add('en-US'); } } catch (e) { Logger.error('获取APP语言配置失败,使用默认语言: $e'); supportedLanguages = ['zh-CN', 'en-US']; } final agentType = _storage.read('device_agent_type') ?? AppConfig.env('AGENT_TYPE') ?? 'stt_llm_tts'; final agentMode = (agentType == 'yidong') ? 'sts' : 'stt_llm_tts'; final agentId = (agentType == 'yidong') ? (AppConfig.env('MOBILE_ELF_AGENT_ID') ?? 'mobile_elf') : 'default_agent'; final stsProductId = AppConfig.env('MOBILE_ELF_PRODUCT_ID') ?? ''; final stsProductKey = AppConfig.env('MOBILE_ELF_PRODUCT_KEY') ?? ''; final stsDeviceNo = _storage.read('device_sts_code') ?? ''; final stsDeviceId = _storage.read('sts_device_id') ?? ''; final stsDeviceSecret = _storage.read('sts_device_secret') ?? ''; final mac = currentDeviceAddress?.toUpperCase() ?? ''; String stsCmei = ''; try { final matchedDevice = userInfo.User.instance.devices.firstWhere( (d) => d.devicemac.toUpperCase() == mac, ); stsCmei = matchedDevice.cmei; } catch (_) {} final config = { 'agentMode': agentMode, 'agentId': agentId, 'stsProductId': stsProductId, 'stsProductKey': stsProductKey, 'stsDeviceNo': stsDeviceNo, 'stsDeviceId': stsDeviceId, 'stsDeviceSecret': stsDeviceSecret, 'stsCmei': stsCmei, 'azureSpeechKey': azureSpeechKey, 'azureSpeechRegion': azureSpeechRegion, 'xunfeiAppId': xunfeiAppId, 'xunfeiAccessKeyId': xunfeiAccessKeyId, 'xunfeiAccessKeySecret': xunfeiAccessKeySecret, 'openaiApiKey': openaiApiKey, 'openaiBaseUrl': openaiBaseUrl, 'openaiModel': openaiModel, 'openaivisionModel': openaiModelMedia, 'systemPrompt': systemPrompt, 'mcpServer': jsonEncode(jsonData), 'volcanoAppId': '2316081933', 'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En', 'serverurl': serverurl, 'usertoken': jsonString, 'supportedLanguages': supportedLanguages, 'musicServiceType': musicServiceType, 'navigationMode': navigationMode, 'isttsinterrupt': isTtsInterruptEnabled ? 'true' : 'false', 'vipexptime': vipExpiryTime, 'callingToolText': 'callingTool'.tr, 'insufficientIntegralText': 'insufficientIntegral'.tr, }; AgentService.nativeLogCallback ??= (level, message) { switch (level) { case 'ERROR': Logger.e('NativeAgent', message); break; case 'WARNING': Logger.w('NativeAgent', message); break; case 'DEBUG': Logger.d('NativeAgent', message); break; default: Logger.i('NativeAgent', message); } }; if (agentMode == 'sts') { AgentService.events .where((e) => e.type == AgentServiceEventType.stsCredentials) .listen((e) { final deviceId = e.data['deviceId']?.toString() ?? ''; final deviceSecret = e.data['deviceSecret']?.toString() ?? ''; final deviceNo = e.data['deviceNo']?.toString() ?? ''; if (deviceId.isNotEmpty) _storage.write('sts_device_id', deviceId); if (deviceSecret.isNotEmpty) { _storage.write('sts_device_secret', deviceSecret); } if (deviceNo.isNotEmpty) _storage.write('sts_device_no', deviceNo); }); } final result = await AgentService.startAgentService(config); Logger.i('BleManager', 'AgentService.startAgentService 结果: $result'); return result; } catch (e) { Logger.error('启动 AgentService 失败: $e'); return false; } } Future stopAgentService() async { try { final result = await AgentService.stopAgentService(); if (result) { Logger.info('AgentService 停止成功'); clearDeviceInfo(); } return result; } catch (e) { Logger.error('停止 AgentService 失败: $e'); return false; } } Future _loadTtsInterrupe() async { try { if (!_storage.hasData('agent_tts_interrupt')) { String deviceName = getDeviceName(); DBProduct? product = AppConfig.getproduct(deviceName); if (deviceName != '' && product != null) { return !product.iscontinuouschat!; } return false; } return _storage.read('agent_tts_interrupt') ?? false; } catch (_) { return false; } } static Future init() async { if (!Get.isRegistered()) { Get.put(BleManager(), permanent: true); } return Future.value(Get.find()); } }