You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1075 lines
41 KiB
1075 lines
41 KiB
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<BleManager>();
|
|
|
|
// ─── 连接信息 ────────────────────────────────────────
|
|
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<DeviceInfo?>(null);
|
|
|
|
// ─── 通话翻译/编解码器(杰理 startTranslation 模式状态)─────
|
|
final _isCodecActive = false.obs;
|
|
bool get isCodecActive => _isCodecActive.value;
|
|
RxBool get isCodecActiveRx => _isCodecActive;
|
|
|
|
// 设备主动上报通话翻译开关信号(耳机端按键)
|
|
final StreamController<bool> _callTranslationRequestController =
|
|
StreamController<bool>.broadcast();
|
|
Stream<bool> get onCallTranslationRequest =>
|
|
_callTranslationRequestController.stream;
|
|
|
|
// 通话翻译模式下,杰理 runtime 解码后推上来的 PCM:
|
|
// - in.uplink = 本机用户麦克风(己方/A 路)
|
|
// - in.downlink = 对端通话音(对方/B 路)
|
|
// 由 translation_controller 转发到 azure_speech 的 AST 流水线。
|
|
final StreamController<Uint8List> _inUplinkPcmController =
|
|
StreamController<Uint8List>.broadcast();
|
|
final StreamController<Uint8List> _inDownlinkPcmController =
|
|
StreamController<Uint8List>.broadcast();
|
|
Stream<Uint8List> get inUplinkPcmStream => _inUplinkPcmController.stream;
|
|
Stream<Uint8List> get inDownlinkPcmStream => _inDownlinkPcmController.stream;
|
|
|
|
// ─── 扫描结果(供设备页扫描列表使用)─────────────────────
|
|
final RxList<JieliDevice> scannedDevices = <JieliDevice>[].obs;
|
|
final RxBool isScanning = false.obs;
|
|
|
|
/// 每个地址最近一次扫到的 advertisement(电量等)。
|
|
/// scannedDevices 只追加不更新,但电量在每次 advertisement 都可能变;
|
|
/// 这里保留最新一次,供连接成功时填到 DeviceInfo。
|
|
final Map<String, JieliDevice> _latestAdvByAddress = {};
|
|
|
|
StreamSubscription<JieliEvent>? _eventSub;
|
|
final GetStorage _storage = GetStorage();
|
|
|
|
Future<void>? _initFuture;
|
|
bool _initSucceeded = false;
|
|
String? _initError;
|
|
String? get initError => _initError;
|
|
bool get isInitialized => _initSucceeded;
|
|
|
|
/// 确保杰理 SDK native 端已 initialize。失败则重试一次。
|
|
/// 失败时抛出 [StateError] —— 调用方负责 try-catch / UI 反馈。
|
|
Future<void> 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<void> 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<bool> 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<void> _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<void> _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<bool> startScan({
|
|
Duration timeout = const Duration(seconds: 30),
|
|
List<String>? uuids,
|
|
List<int>? 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<String> 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 <String>[];
|
|
}
|
|
|
|
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<void> stopScan() async {
|
|
await ready;
|
|
try {
|
|
await Jielihome.instance.stopScan();
|
|
} catch (e) {
|
|
Logger.e('BleManager', '停止扫描失败: $e');
|
|
} finally {
|
|
isScanning.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> 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<void> disconnect() async {
|
|
await ready;
|
|
try {
|
|
final addr = currentDeviceAddress;
|
|
if (addr != null) {
|
|
await Jielihome.instance.disconnect(addr);
|
|
}
|
|
} catch (e) {
|
|
Logger.e('BleManager', '断开连接失败: $e');
|
|
}
|
|
}
|
|
|
|
Future<int> 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<bool> isBleConnected() async {
|
|
final addr = currentDeviceAddress;
|
|
if (addr == null) return false;
|
|
try {
|
|
return await Jielihome.instance.isConnected(addr);
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 探查当前是否已经有杰理设备连着(用于登录/启动后判断是否需要跳转扫描页)。
|
|
/// 通过 [Jielihome.connectedDevice] 同步 SDK 已知的连接状态。
|
|
Future<bool> 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<void> 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<bool> fetchBatteryInfo() async => true;
|
|
Future<bool> fetchDeviceVersion() async => true;
|
|
Future<bool> getProductId() async => true;
|
|
Future<int> getBatteryLevel() async =>
|
|
deviceInfo.value?.batteryLevel ?? 0;
|
|
|
|
void clearDeviceInfo() {
|
|
deviceInfo.value = null;
|
|
currentDeviceAddress = null;
|
|
_currentDeviceName = null;
|
|
_connectedDevice = null;
|
|
}
|
|
|
|
// ─── 编解码器 / 通话翻译模式 ───────────────────────────
|
|
// 杰理:openA2DPDecoder/closeCodec 映射到 startTranslation/stopTranslation。
|
|
// 其余 codec 入口按现有调用语义保留乐观成功,以保持上层兼容。
|
|
|
|
Future<bool> openEncoder() async {
|
|
_isCodecActive.value = true;
|
|
return true;
|
|
}
|
|
|
|
Future<bool> openDecoder() async {
|
|
_isCodecActive.value = true;
|
|
return true;
|
|
}
|
|
|
|
Future<bool> 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<bool> openCallRecordDecoder() async {
|
|
_isCodecActive.value = true;
|
|
return true;
|
|
}
|
|
|
|
Future<bool> closeCodec() async {
|
|
_isCodecActive.value = false;
|
|
try {
|
|
await Jielihome.instance.stopTranslation();
|
|
} catch (_) {
|
|
// 静默失败:未启动时调 stop 是常态
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Future<bool> playA2DP() async => true;
|
|
|
|
// ─── OTA ───────────────────────────────────────────
|
|
/// 杰理 OTA 通过 Jielihome.otaStart 触发,此处保留空实现以兼容旧调用点。
|
|
Future<void> setEnterOta(bool enterOta) async {}
|
|
|
|
// ─── 兼容存根 ───────────────────────────────────────
|
|
Future<bool> requestBluetoothPermission() async => true;
|
|
|
|
/// 旧 Companion 关联 / 后台扫描接口已废弃 — 保留空实现以兼容
|
|
/// 即将被移除的调用点(pairing 模块已删除)。
|
|
Future<bool> clearPairedDevices() async {
|
|
await disconnect();
|
|
return true;
|
|
}
|
|
|
|
// ─── AgentService 启动 / 停止(保持原逻辑) ───────────
|
|
Future<bool> startAgentService() async {
|
|
try {
|
|
var serverurl = DioManager().baseUrl;
|
|
var jsonString = _storage.read<String>("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<String> supportedLanguages = [];
|
|
try {
|
|
final storage = GetStorage();
|
|
final languageManager = Get.find<LanguageManager>();
|
|
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<String>('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<String>('device_sts_code') ?? '';
|
|
final stsDeviceId = _storage.read<String>('sts_device_id') ?? '';
|
|
final stsDeviceSecret = _storage.read<String>('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 = <String, dynamic>{
|
|
'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<bool> 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<bool> _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<BleManager> init() async {
|
|
if (!Get.isRegistered<BleManager>()) {
|
|
Get.put(BleManager(), permanent: true);
|
|
}
|
|
return Future.value(Get.find<BleManager>());
|
|
}
|
|
}
|
|
|