import 'dart:async'; import 'package:flutter/services.dart'; // device_plugin_interface 适配器(让 device_manager 以厂商无关方式驱动 jieli)。 export 'src/jieli_device_plugin.dart'; export 'src/jieli_device_session.dart'; const _methodChannel = MethodChannel('device_jieli/method'); const _eventChannel = EventChannel('device_jieli/event'); class JieliDevice { JieliDevice({ required this.name, required this.address, this.edrAddr, this.deviceType, this.connectWay, this.rssi, this.rawAdv, this.advFlags, this.manufacturerCompanyId, this.manufacturerData, this.serviceUuids = const [], this.advRecords = const [], }); final String name; final String address; final String? edrAddr; final int? deviceType; final int? connectWay; final int? rssi; /// 原始广播包 hex(不含 "0x" 前缀)。 final String? rawAdv; /// AD Type 0x01 Flags 字节值。 final int? advFlags; /// AD Type 0xFF 厂商数据的前两字节(小端)解析出的 Company ID。 final int? manufacturerCompanyId; /// AD Type 0xFF 厂商数据去掉 Company ID 后剩余字节的 hex(不含 "0x")。 final String? manufacturerData; /// ScanRecord.serviceUuids 列表(大写字符串)。 final List serviceUuids; /// 按 BLE Core Spec §11 解析的每条 AD 记录:{len, type, data}。 final List> advRecords; factory JieliDevice.fromMap(Map m) => JieliDevice( name: (m['name'] as String?) ?? '', address: m['address'] as String, edrAddr: m['edrAddr'] as String?, deviceType: m['deviceType'] as int?, connectWay: m['connectWay'] as int?, rssi: m['rssi'] as int?, rawAdv: m['rawAdv'] as String?, advFlags: m['advFlags'] as int?, manufacturerCompanyId: m['manufacturerCompanyId'] as int?, manufacturerData: m['manufacturerData'] as String?, serviceUuids: (m['serviceUuids'] as List?)?.cast() ?? const [], advRecords: (m['advRecords'] as List?) ?.whereType() .map((e) => e.map( (k, v) => MapEntry(k.toString(), v?.toString() ?? ''), )) .toList() ?? const [], ); @override String toString() => 'JieliDevice($name, $address, rssi=$rssi)'; } abstract class JieliEvent { const JieliEvent(); } class AdapterStatusEvent extends JieliEvent { final bool enabled; final bool hasBle; const AdapterStatusEvent(this.enabled, this.hasBle); } class ScanStatusEvent extends JieliEvent { final bool ble; final bool started; const ScanStatusEvent(this.ble, this.started); } class DeviceFoundEvent extends JieliEvent { final JieliDevice device; const DeviceFoundEvent(this.device); } class BondStatusEvent extends JieliEvent { final String address; final int status; const BondStatusEvent(this.address, this.status); } class RcspInitEvent extends JieliEvent { final String address; final int code; const RcspInitEvent(this.address, this.code); bool get success => code == 0; } class ConnectionStateEvent extends JieliEvent { final String address; final int state; const ConnectionStateEvent(this.address, this.state); static const int connectionOk = 1; static const int connectionConnecting = 2; static const int connectionDisconnect = 0; } class UnknownJieliEvent extends JieliEvent { final Map raw; const UnknownJieliEvent(this.raw); } // ───── 设备级事件 ───── class BatteryEvent extends JieliEvent { final String? address; final int? level; const BatteryEvent(this.address, this.level); } class PhoneCallStatusEvent extends JieliEvent { final String? address; final int status; const PhoneCallStatusEvent(this.address, this.status); } class ExpandFunctionEvent extends JieliEvent { final String? address; final int opCode; final String? payloadBase64; const ExpandFunctionEvent(this.address, this.opCode, this.payloadBase64); } // ───── 翻译事件 ───── class TranslationModeIds { static const int idle = 0; static const int record = 1; static const int recordingTranslation = 2; static const int callTranslation = 3; static const int audioTranslation = 4; static const int faceToFaceTranslation = 5; static const int callTranslationWithStereo = 6; } class TranslationStreams { static const String inMic = 'in.mic'; static const String inUplink = 'in.uplink'; static const String inDownlink = 'in.downlink'; static const String inAudioFile = 'in.audioFile'; static const String outSpeaker = 'out.speaker'; static const String outUplink = 'out.uplink'; static const String outDownlink = 'out.downlink'; static const String outLocalPlayback = 'out.localPlayback'; } class TranslationAudioEvent extends JieliEvent { final int modeId; final String streamId; final int sampleRate; final int channels; final int bitsPerSample; final int seq; final int tsMs; final bool isFinal; /// 16bit signed PCM 字节流(小端,interleaved 当 channels>1) final Uint8List pcm; const TranslationAudioEvent({ required this.modeId, required this.streamId, required this.sampleRate, required this.channels, required this.bitsPerSample, required this.seq, required this.tsMs, required this.isFinal, required this.pcm, }); } class TranslationLogEvent extends JieliEvent { final int modeId; final String content; const TranslationLogEvent(this.modeId, this.content); } class TranslationResultEvent extends JieliEvent { final int modeId; final String? srcLang; final String? srcText; final String? destLang; final String? destText; /// 由调用方贯穿同一段翻译,便于字幕聚合到具体段落 final String? requestId; const TranslationResultEvent({ required this.modeId, this.srcLang, this.srcText, this.destLang, this.destText, this.requestId, }); } class TranslationErrorEvent extends JieliEvent { final int modeId; final int code; final String? message; const TranslationErrorEvent(this.modeId, this.code, this.message); } // ───── 语音助手 / 唤醒事件 ───── class SpeechStartEvent extends JieliEvent { final String? address; /// RecordParam.VOICE_TYPE_PCM=0 / SPEEX=1 / OPUS=2 final int? voiceType; /// 已转换为 Hz(如 16000) final int? sampleRate; /// VAD_WAY_DEVICE=0 / SDK=1 final int? vadWay; final int tsMs; const SpeechStartEvent({ this.address, this.voiceType, this.sampleRate, this.vadWay, required this.tsMs, }); } class SpeechAudioEvent extends JieliEvent { final String? address; /// 'pcm16' / 'speex'(speex 时未解码) final String encoding; final int sampleRate; final int channels; final int bitsPerSample; final int tsMs; /// 16bit signed PCM(encoding=='pcm16');speex 时为原始帧 final Uint8List pcm; const SpeechAudioEvent({ this.address, required this.encoding, required this.sampleRate, required this.channels, required this.bitsPerSample, required this.tsMs, required this.pcm, }); } class SpeechEndEvent extends JieliEvent { final String? address; final int? reason; final String? message; final int tsMs; const SpeechEndEvent({this.address, this.reason, this.message, required this.tsMs}); } class SpeechErrorEvent extends JieliEvent { final int code; final String? message; const SpeechErrorEvent(this.code, this.message); } class SpeechVoiceType { static const int pcm = 0; static const int speex = 1; static const int opus = 2; } class SpeechSampleRate { static const int khz8 = 8; static const int khz16 = 16; } class SpeechVadWay { static const int device = 0; static const int sdk = 1; } class SpeechStopReason { static const int normal = 0; static const int stop = 1; } // ───── 设备录音事件 ───── /// 上行/下行通道 ID 常量(与 Kotlin DeviceRecordFeature 保持一致) class DeviceRecordStreams { static const String inUplink = 'in.uplink'; static const String inDownlink = 'in.downlink'; } class DeviceRecordStartEvent extends JieliEvent { final String? address; final int sampleRate; final int tsMs; const DeviceRecordStartEvent({ this.address, required this.sampleRate, required this.tsMs, }); } class DeviceRecordAudioEvent extends JieliEvent { final String? address; /// [DeviceRecordStreams.inUplink] 或 [DeviceRecordStreams.inDownlink] final String streamId; final int sampleRate; final int channels; final int bitsPerSample; final int tsMs; /// 16bit signed PCM 字节流(小端) final Uint8List pcm; const DeviceRecordAudioEvent({ this.address, required this.streamId, required this.sampleRate, required this.channels, required this.bitsPerSample, required this.tsMs, required this.pcm, }); } class DeviceRecordStopEvent extends JieliEvent { final String? address; final int tsMs; const DeviceRecordStopEvent({this.address, required this.tsMs}); } class DeviceRecordErrorEvent extends JieliEvent { final String? address; final int code; final String? message; const DeviceRecordErrorEvent({this.address, required this.code, this.message}); } // ───── AI 助理通路事件(JieliAssistantPort:MODE_RECORD + DEVICE_ALWAYS_RECORDING)───── // 与 SpeechXxxEvent 互斥;走 RCSP 翻译模式状态机,耳机端 OPUS → 16k PCM 自动持续上推。 class AssistantStartEvent extends JieliEvent { final int sampleRate; final int tsMs; const AssistantStartEvent({required this.sampleRate, required this.tsMs}); } class AssistantAudioEvent extends JieliEvent { /// 'pcm16' final String encoding; final int sampleRate; final int channels; final int bitsPerSample; final int sequence; final int tsMs; /// 16-bit signed PCM, mono, 16kHz, 20ms 帧 final Uint8List pcm; const AssistantAudioEvent({ required this.encoding, required this.sampleRate, required this.channels, required this.bitsPerSample, required this.sequence, required this.tsMs, required this.pcm, }); } class AssistantEndEvent extends JieliEvent { final int tsMs; const AssistantEndEvent({required this.tsMs}); } class AssistantErrorEvent extends JieliEvent { final String code; final String? message; const AssistantErrorEvent(this.code, this.message); } // ───── OTA 事件 ───── enum OtaState { idle, inquiring, notifyingSize, entering, transferring, verifying, rebooting, done, failed, cancelled, } class OtaStateEvent extends JieliEvent { final OtaState state; final int sent; final int total; /// -1 表示未知(idle/cancelled 等) final int percent; final int tsMs; const OtaStateEvent({ required this.state, required this.sent, required this.total, required this.percent, required this.tsMs, }); } class OtaErrorEvent extends JieliEvent { final int code; final String? message; const OtaErrorEvent(this.code, this.message); } class Jielihome { Jielihome._(); static final Jielihome instance = Jielihome._(); Stream? _events; Stream get events { return _events ??= _eventChannel .receiveBroadcastStream() .map((raw) => _parseEvent(raw as Map)); } JieliEvent _parseEvent(Map raw) { switch (raw['type']) { case 'adapterStatus': return AdapterStatusEvent(raw['enabled'] == true, raw['hasBle'] == true); case 'scanStatus': return ScanStatusEvent(raw['ble'] == true, raw['started'] == true); case 'deviceFound': return DeviceFoundEvent(JieliDevice.fromMap(raw)); case 'bondStatus': return BondStatusEvent(raw['address'] as String, raw['status'] as int); case 'rcspInit': return RcspInitEvent(raw['address'] as String, raw['code'] as int); case 'connectionState': return ConnectionStateEvent(raw['address'] as String, raw['state'] as int); case 'battery': return BatteryEvent(raw['address'] as String?, raw['level'] as int?); case 'phoneCallStatus': return PhoneCallStatusEvent(raw['address'] as String?, (raw['status'] as int?) ?? 0); case 'expandFunction': return ExpandFunctionEvent( raw['address'] as String?, (raw['opCode'] as int?) ?? 0, raw['payloadBase64'] as String?, ); case 'translationAudio': return TranslationAudioEvent( modeId: (raw['modeId'] as int?) ?? 0, streamId: raw['streamId'] as String, sampleRate: (raw['sampleRate'] as int?) ?? 16000, channels: (raw['channels'] as int?) ?? 1, bitsPerSample: (raw['bitsPerSample'] as int?) ?? 16, seq: (raw['seq'] as num?)?.toInt() ?? 0, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, isFinal: raw['final'] == true, pcm: raw['pcm'] as Uint8List, ); case 'translationLog': return TranslationLogEvent( (raw['modeId'] as int?) ?? 0, raw['content'] as String? ?? '', ); case 'translationResult': return TranslationResultEvent( modeId: (raw['modeId'] as int?) ?? 0, srcLang: raw['srcLang'] as String?, srcText: raw['srcText'] as String?, destLang: raw['destLang'] as String?, destText: raw['destText'] as String?, requestId: raw['requestId'] as String?, ); case 'translationError': return TranslationErrorEvent( (raw['modeId'] as int?) ?? 0, (raw['code'] as int?) ?? 0, raw['message'] as String?, ); case 'speechStart': return SpeechStartEvent( address: raw['address'] as String?, voiceType: raw['voiceType'] as int?, sampleRate: raw['sampleRate'] as int?, vadWay: raw['vadWay'] as int?, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, ); case 'speechAudio': return SpeechAudioEvent( address: raw['address'] as String?, encoding: (raw['encoding'] as String?) ?? 'pcm16', sampleRate: (raw['sampleRate'] as int?) ?? 16000, channels: (raw['channels'] as int?) ?? 1, bitsPerSample: (raw['bitsPerSample'] as int?) ?? 16, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, pcm: raw['pcm'] as Uint8List, ); case 'speechEnd': return SpeechEndEvent( address: raw['address'] as String?, reason: raw['reason'] as int?, message: raw['message'] as String?, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, ); case 'speechError': return SpeechErrorEvent( (raw['code'] as int?) ?? 0, raw['message'] as String?, ); case 'assistantStart': return AssistantStartEvent( sampleRate: (raw['sampleRate'] as int?) ?? 16000, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, ); case 'assistantAudio': return AssistantAudioEvent( encoding: (raw['encoding'] as String?) ?? 'pcm16', sampleRate: (raw['sampleRate'] as int?) ?? 16000, channels: (raw['channels'] as int?) ?? 1, bitsPerSample: (raw['bitsPerSample'] as int?) ?? 16, sequence: (raw['sequence'] as num?)?.toInt() ?? 0, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, pcm: raw['pcm'] as Uint8List, ); case 'assistantEnd': return AssistantEndEvent( tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, ); case 'assistantError': return AssistantErrorEvent( (raw['code'] as String?) ?? 'device.assistant.unknown', raw['message'] as String?, ); case 'deviceRecordStart': return DeviceRecordStartEvent( address: raw['address'] as String?, sampleRate: (raw['sampleRate'] as int?) ?? 16000, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, ); case 'deviceRecordAudio': return DeviceRecordAudioEvent( address: raw['address'] as String?, streamId: raw['streamId'] as String, sampleRate: (raw['sampleRate'] as int?) ?? 16000, channels: (raw['channels'] as int?) ?? 1, bitsPerSample: (raw['bitsPerSample'] as int?) ?? 16, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, pcm: raw['pcm'] as Uint8List, ); case 'deviceRecordStop': return DeviceRecordStopEvent( address: raw['address'] as String?, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, ); case 'deviceRecordError': return DeviceRecordErrorEvent( address: raw['address'] as String?, code: (raw['code'] as int?) ?? 0, message: raw['message'] as String?, ); case 'otaState': return OtaStateEvent( state: _parseOtaState(raw['state'] as String?), sent: (raw['sent'] as num?)?.toInt() ?? -1, total: (raw['total'] as num?)?.toInt() ?? 0, percent: (raw['percent'] as int?) ?? -1, tsMs: (raw['tsMs'] as num?)?.toInt() ?? 0, ); case 'otaError': return OtaErrorEvent( (raw['code'] as int?) ?? 0, raw['message'] as String?, ); default: return UnknownJieliEvent(raw); } } OtaState _parseOtaState(String? s) { switch (s) { case 'INQUIRING': return OtaState.inquiring; case 'NOTIFYING_SIZE': return OtaState.notifyingSize; case 'ENTERING': return OtaState.entering; case 'TRANSFERRING': return OtaState.transferring; case 'VERIFYING': return OtaState.verifying; case 'REBOOTING': return OtaState.rebooting; case 'DONE': return OtaState.done; case 'FAILED': return OtaState.failed; case 'CANCELLED': return OtaState.cancelled; default: return OtaState.idle; } } Future getPlatformVersion() async { return await _methodChannel.invokeMethod('getPlatformVersion'); } Future initialize({ bool multiDevice = true, bool skipNoNameDev = false, bool enableLog = false, }) async { await _methodChannel.invokeMethod('initialize', { 'multiDevice': multiDevice, 'skipNoNameDev': skipNoNameDev, 'enableLog': enableLog, }); } /// 启动扫描,可选复合过滤。 /// /// - [nameList]:设备名命中列表(精确匹配,忽略大小写);空 = 不按名过滤 /// - [uuidList]:UUID 命中列表 → `ScanFilter.setServiceUuid`;空 = 不按 UUID 过滤 /// - [skipUnnamed]:跳过没有 name 的广播(环境噪声),默认 true Future startScan({ Duration timeout = const Duration(seconds: 30), List nameList = const [], List uuidList = const [], bool skipUnnamed = true, }) async { await _methodChannel.invokeMethod('startScan', { 'timeoutMs': timeout.inMilliseconds, 'nameList': nameList, 'uuidList': uuidList, 'skipUnnamed': skipUnnamed, }); } Future stopScan() => _methodChannel.invokeMethod('stopScan'); Future isScanning() async => (await _methodChannel.invokeMethod('isScanning')) ?? false; Future connect(JieliDevice device) async { await _methodChannel.invokeMethod('connect', { 'address': device.address, 'edrAddr': device.edrAddr, 'connectWay': device.connectWay, 'deviceType': device.deviceType, }); } Future disconnect(String address) async { await _methodChannel.invokeMethod('disconnect', {'address': address}); } Future isConnected(String address) async => (await _methodChannel.invokeMethod('isConnected', {'address': address})) ?? false; Future connectedDevice() async { final raw = await _methodChannel.invokeMethod('connectedDevice'); if (raw == null) return null; return JieliDevice( name: (raw['name'] as String?) ?? '', address: raw['address'] as String, ); } // ───── 设备信息查询 ───── Future?> deviceSnapshot(String address) async { final raw = await _methodChannel.invokeMethod( 'deviceSnapshot', {'address': address}); return raw?.cast(); } Future?> queryTargetInfo(String address, {int mask = 0x0F}) async { final raw = await _methodChannel.invokeMethod( 'queryTargetInfo', {'address': address, 'mask': mask}); return raw?.cast(); } // ───── 自定义指令 ───── Future?> sendCustomCmd(String address, int opCode, List payload) async { final raw = await _methodChannel.invokeMethod?>( 'sendCustomCmd', {'address': address, 'opCode': opCode, 'payload': payload}, ); return raw?.cast(); } // ───── 翻译 ───── Future startTranslation({ required int modeId, Map args = const {}, }) async { await _methodChannel.invokeMethod('startTranslation', { 'modeId': modeId, 'args': args, }); } Future stopTranslation() => _methodChannel.invokeMethod('stopTranslation'); Future?> translationStatus() async { final raw = await _methodChannel.invokeMethod('translationStatus'); return raw?.cast(); } /// 把外部翻译服务返回的 TTS PCM 喂回插件,由插件根据当前模式回送给耳机或本地播放。 /// [pcm] 必须是 16bit signed PCM(小端、interleaved when channels>1)。 /// 推荐传 [Uint8List];也接受 List<int> 但会触发一次拷贝。 Future feedTranslatedAudio({ required String streamId, required List pcm, int sampleRate = 16000, int channels = 1, int bitsPerSample = 16, bool isFinal = false, }) async { final bytes = pcm is Uint8List ? pcm : Uint8List.fromList(pcm); return (await _methodChannel.invokeMethod('feedTranslatedAudio', { 'streamId': streamId, 'pcm': bytes, 'sampleRate': sampleRate, 'channels': channels, 'bitsPerSample': bitsPerSample, 'final': isFinal, })) ?? false; } /// 透传翻译文本(字幕)到 EventChannel,供 UI 订阅 [TranslationResultEvent]。 /// [requestId] 用于把同一段翻译的多次回调串起来。 Future feedTranslationResult({ String? srcLang, String? srcText, String? destLang, String? destText, String? requestId, }) async { await _methodChannel.invokeMethod('feedTranslationResult', { 'srcLang': srcLang, 'srcText': srcText, 'destLang': destLang, 'destText': destText, 'requestId': requestId, }); } /// 探测当前已连耳机是否支持立体声通话翻译方案。 /// [address] 不传则用当前已连设备。 Future isSupportCallTranslationWithStereo({String? address}) async { return (await _methodChannel.invokeMethod( 'isSupportCallTranslationWithStereo', {'address': address}, )) ?? false; } /// 把外部音频文件解码后的 PCM 灌入「音视频翻译」模式(仅 MODE_AUDIO_TRANSLATION 生效)。 Future feedAudioFilePcm({ required List pcm, int sampleRate = 16000, }) async { final bytes = pcm is Uint8List ? pcm : Uint8List.fromList(pcm); return (await _methodChannel.invokeMethod('feedAudioFilePcm', { 'pcm': bytes, 'sampleRate': sampleRate, })) ?? false; } // ───── 语音助手 / 唤醒 ───── // 提示:插件初始化时已经常驻订阅了耳机 RECORD 状态回调; // - 耳机本地检测到唤醒词或按键 → 自动收到 SpeechStart/SpeechAudio/SpeechEnd 事件 // - 业务层不需要先调任何方法 // 下面的 speechStart/Stop 仅用于「APP 主动触发/取消」语音助手的场景。 Future speechIsRecording({String? address}) async => (await _methodChannel.invokeMethod('speechIsRecording', {'address': address})) ?? false; Future speechStart({ String? address, int voiceType = SpeechVoiceType.opus, int sampleRate = SpeechSampleRate.khz16, int vadWay = SpeechVadWay.device, }) async { await _methodChannel.invokeMethod('speechStart', { 'address': address, 'voiceType': voiceType, 'sampleRate': sampleRate, 'vadWay': vadWay, }); } Future speechStop({ String? address, int reason = SpeechStopReason.normal, }) async { await _methodChannel.invokeMethod('speechStop', { 'address': address, 'reason': reason, }); } // ───── AI 助理通路(JieliAssistantPort)───── // 与 [speechStart] 不同:本通路走 RCSP `MODE_RECORD` + // `STRATEGY_DEVICE_ALWAYS_RECORDING`,耳机自主持续推 OPUS,APP 不需要唤醒词 // 也不需要按键。Port 内部以 `packetSize=40` 解码 16k OPUS 帧,避免 SpeechFeature // 路径下因 packetSize=200 导致的 80% 帧丢失。 // // 订阅 [AssistantAudioEvent] 拿 16k/16bit/mono/20ms PCM 帧; // [AssistantErrorEvent] / [AssistantEndEvent] 用于错误和退出通知。 Future assistantStart() async => (await _methodChannel.invokeMethod('assistantStart')) ?? false; Future assistantStop() async { await _methodChannel.invokeMethod('assistantStop'); } Future assistantIsRunning() async => (await _methodChannel.invokeMethod('assistantIsRunning')) ?? false; // ───── 设备录音 ───── // 订阅 [DeviceRecordStartEvent] / [DeviceRecordAudioEvent] / // [DeviceRecordStopEvent] / [DeviceRecordErrorEvent] 接收录音状态和 PCM 数据。 /// 启动设备录音上行。 /// [address] 省略则取当前已连设备。 Future deviceRecordStart({ String? address, int sampleRate = 16000, }) async { await _methodChannel.invokeMethod('deviceRecordStart', { 'args': { if (address != null) 'address': address, 'sampleRate': sampleRate, }, }); } Future deviceRecordStop() => _methodChannel.invokeMethod('deviceRecordStop'); Future?> deviceRecordStatus() async { final raw = await _methodChannel.invokeMethod('deviceRecordStatus'); return raw?.cast(); } // ───── OTA ───── /// 启动 OTA 升级。订阅 [OtaStateEvent] / [OtaErrorEvent] 看进度。 /// [firmwareFilePath] 必须是手机本地完整路径(你提前下好的固件文件)。 Future otaStart({ String? address, required String firmwareFilePath, int blockSize = 512, List? fileFlag, }) async { await _methodChannel.invokeMethod('otaStart', { 'address': address, 'firmwareFilePath': firmwareFilePath, 'blockSize': blockSize, if (fileFlag != null) 'fileFlag': fileFlag is Uint8List ? fileFlag : Uint8List.fromList(fileFlag), }); } Future otaCancel() => _methodChannel.invokeMethod('otaCancel'); Future otaIsRunning() async => (await _methodChannel.invokeMethod('otaIsRunning')) ?? false; }