import Flutter import Foundation import UIKit /// Method-channel router. Maps every Dart call to the matching feature module. /// /// Symmetric with Android `bridge/MethodRouter.kt` — same method names, same /// argument keys. Where the iOS JieLi SDK does not yet have a wired /// implementation, the handler returns a `NOT_IMPLEMENTED_IOS` error so the /// Dart layer can downgrade gracefully (instead of crashing). class MethodRouter { private let server: JieliHomeServer init(server: JieliHomeServer) { self.server = server } func handle(call: FlutterMethodCall, result: @escaping FlutterResult) { let args = call.arguments as? [String: Any] ?? [:] do { switch call.method { // MARK: - lifecycle / platform case "getPlatformVersion": result("iOS \(UIDevice.current.systemVersion)") case "initialize": try server.initialize( multiDevice: (args["multiDevice"] as? Bool) ?? true, skipNoNameDev: (args["skipNoNameDev"] as? Bool) ?? false, enableLog: (args["enableLog"] as? Bool) ?? false ) result(true) // MARK: - scan case "startScan": let timeoutMs = (args["timeoutMs"] as? Int) ?? 30000 let nameList = (args["nameList"] as? [String]) ?? [] let uuidList = (args["uuidList"] as? [String]) ?? [] let skipUnnamed = (args["skipUnnamed"] as? Bool) ?? true try server.scanFeature.startScan( timeoutMs: timeoutMs, nameList: nameList, uuidList: uuidList, skipUnnamed: skipUnnamed ) result(true) case "stopScan": server.scanFeature.stopScan() result(true) case "isScanning": result(server.scanFeature.isScanning()) // MARK: - connect case "connect": guard let address = args["address"] as? String else { result(FlutterError(code: "BAD_ARG", message: "address required", details: nil)); return } try server.connectFeature.connect( bleAddress: address, edrAddress: args["edrAddr"] as? String, deviceType: (args["deviceType"] as? Int) ?? -1, connectWay: (args["connectWay"] as? Int) ?? 0 ) result(true) case "disconnect": guard let address = args["address"] as? String else { result(FlutterError(code: "BAD_ARG", message: "address required", details: nil)); return } try server.connectFeature.disconnect(address: address) result(true) case "isConnected": guard let address = args["address"] as? String else { result(FlutterError(code: "BAD_ARG", message: "address required", details: nil)); return } result(server.connectFeature.isConnected(address: address)) case "connectedDevice": if let info = server.connectFeature.connectedDeviceInfo() { result(info) } else { result(nil) } // MARK: - device info case "deviceSnapshot": guard let address = args["address"] as? String else { result(FlutterError(code: "BAD_ARG", message: "address required", details: nil)); return } result(server.deviceInfoFeature.snapshot(address: address)) case "queryTargetInfo": guard let address = args["address"] as? String else { result(FlutterError(code: "BAD_ARG", message: "address required", details: nil)); return } let mask = (args["mask"] as? Int) ?? 0x0F server.deviceInfoFeature.queryTargetInfo(address: address, mask: mask) { snapshot, error in if let snapshot = snapshot { result(snapshot) } else { result(FlutterError( code: "TARGET_INFO_ERR", message: error?.localizedDescription ?? "queryTargetInfo failed", details: nil )) } } // MARK: - custom RCSP case "sendCustomCmd": guard let address = args["address"] as? String else { result(FlutterError(code: "BAD_ARG", message: "address required", details: nil)); return } guard let opCode = args["opCode"] as? Int else { result(FlutterError(code: "BAD_ARG", message: "opCode required", details: nil)); return } let payloadBytes: [UInt8] if let typed = args["payload"] as? FlutterStandardTypedData { payloadBytes = [UInt8](typed.data) } else if let arr = args["payload"] as? [Int] { payloadBytes = arr.map { UInt8(truncatingIfNeeded: $0) } } else { payloadBytes = [] } server.customCmdFeature.send( address: address, opCode: UInt8(truncatingIfNeeded: opCode), payload: Data(payloadBytes) ) { response, error in if let resp = response { result([UInt8](resp)) } else { result(FlutterError( code: "CUSTOM_CMD_ERR", message: error?.localizedDescription ?? "custom cmd failed", details: nil )) } } // MARK: - translation case "startTranslation": guard let modeId = args["modeId"] as? Int else { result(FlutterError(code: "BAD_ARG", message: "modeId required", details: nil)); return } let extra = (args["args"] as? [String: Any]) ?? [:] if server.deviceRecordFeature.isRecording() { server.deviceRecordFeature.stop() } try server.translationFeature.start(modeId: modeId, args: extra) result(true) case "stopTranslation": server.translationFeature.stop() result(true) case "translationStatus": result([ "working": server.translationFeature.isWorking(), "modeId": server.translationFeature.currentModeId() as Any, "inputStreams": server.translationFeature.currentInputStreams(), "outputStreams": server.translationFeature.currentOutputStreams(), ] as [String: Any]) case "feedTranslatedAudio": guard let streamId = args["streamId"] as? String else { result(FlutterError(code: "BAD_ARG", message: "streamId required", details: nil)); return } let pcm: Data if let typed = args["pcm"] as? FlutterStandardTypedData { pcm = typed.data } else if let arr = args["pcm"] as? [Int] { pcm = Data(arr.map { UInt8(truncatingIfNeeded: $0) }) } else { result(FlutterError(code: "BAD_ARG", message: "pcm required", details: nil)); return } let ok = server.translationFeature.feedTranslatedAudio( streamId: streamId, pcm: pcm, sampleRate: (args["sampleRate"] as? Int) ?? 16000, channels: (args["channels"] as? Int) ?? 1, bitsPerSample: (args["bitsPerSample"] as? Int) ?? 16, isFinal: (args["final"] as? Bool) ?? false ) result(ok) case "feedTranslationResult": server.translationFeature.feedTranslationResult( srcLang: args["srcLang"] as? String, srcText: args["srcText"] as? String, destLang: args["destLang"] as? String, destText: args["destText"] as? String, requestId: args["requestId"] as? String ) result(true) case "isSupportCallTranslationWithStereo": let address = args["address"] as? String result(server.translationFeature.isSupportCallTranslationWithStereo(address: address)) case "feedAudioFilePcm": let pcm: Data if let typed = args["pcm"] as? FlutterStandardTypedData { pcm = typed.data } else if let arr = args["pcm"] as? [Int] { pcm = Data(arr.map { UInt8(truncatingIfNeeded: $0) }) } else { result(FlutterError(code: "BAD_ARG", message: "pcm required", details: nil)); return } let sampleRate = (args["sampleRate"] as? Int) ?? 16000 result(server.translationFeature.feedAudioFilePcm(pcm: pcm, sampleRate: sampleRate)) // MARK: - speech (PTT / wake-word) case "speechIsRecording": result(server.speechFeature.isRecording(address: args["address"] as? String)) case "speechStart": server.speechFeature.start( address: args["address"] as? String, voiceType: (args["voiceType"] as? Int) ?? SpeechFeature.voiceTypeOpus, sampleRate: (args["sampleRate"] as? Int) ?? SpeechFeature.sampleRate16k, vadWay: (args["vadWay"] as? Int) ?? SpeechFeature.vadWayDevice ) { ok, message in if ok { result(true) } else { result(FlutterError(code: "SPEECH_START_ERR", message: message, details: nil)) } } case "speechStop": server.speechFeature.stop( address: args["address"] as? String, reason: (args["reason"] as? Int) ?? SpeechFeature.reasonNormal ) { ok, message in if ok { result(true) } else { result(FlutterError(code: "SPEECH_STOP_ERR", message: message, details: nil)) } } // MARK: - assistant always-recording case "assistantStart": if server.translationFeature.isWorking() { server.translationFeature.stop() } if server.deviceRecordFeature.isRecording() { server.deviceRecordFeature.stop() } let ok = server.assistantBridge.start() if ok { result(true) } else { result(FlutterError(code: "ASSISTANT_START_ERR", message: "assistant bridge start failed", details: nil)) } case "assistantStop": server.assistantBridge.stop() result(true) case "assistantIsRunning": result(server.assistantBridge.isRunning()) // MARK: - device record case "deviceRecordStart": let nested = (args["args"] as? [String: Any]) ?? [:] if server.translationFeature.isWorking() { server.translationFeature.stop() } try server.deviceRecordFeature.start(args: nested) result(true) case "deviceRecordStop": server.deviceRecordFeature.stop() result(true) case "deviceRecordStatus": result(["recording": server.deviceRecordFeature.isRecording()]) // MARK: - OTA case "otaStart": guard let path = args["firmwareFilePath"] as? String else { result(FlutterError(code: "BAD_ARG", message: "firmwareFilePath required", details: nil)); return } let address = args["address"] as? String let blockSize = (args["blockSize"] as? Int) ?? 512 let fileFlag: Data? if let typed = args["fileFlag"] as? FlutterStandardTypedData { fileFlag = typed.data } else if let arr = args["fileFlag"] as? [Int] { fileFlag = Data(arr.map { UInt8(truncatingIfNeeded: $0) }) } else { fileFlag = nil } server.otaFeature.start( address: address, firmwareFilePath: path, blockSize: blockSize, fileFlag: fileFlag ) result(true) case "otaCancel": server.otaFeature.cancel() result(true) case "otaIsRunning": result(server.otaFeature.isRunning()) default: result(FlutterMethodNotImplemented) } } catch let err as PluginError { result(FlutterError(code: err.code, message: err.message, details: nil)) } catch { result(FlutterError(code: "PLUGIN_ERR", message: error.localizedDescription, details: nil)) } } } /// Local plugin error wrapping that maps cleanly onto FlutterError. Used by /// feature classes when they need to surface a typed error code without /// throwing an opaque NSError. struct PluginError: Error { let code: String let message: String init(_ code: String, _ message: String) { self.code = code self.message = message } }