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.

327 lines
12 KiB

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).
public class MethodRouter {
private let server: JieliHomeServer
public init(server: JieliHomeServer) {
self.server = server
}
public 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.
public struct PluginError: Error {
public let code: String
public let message: String
public init(_ code: String, _ message: String) {
self.code = code
self.message = message
}
}