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
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
|
|
}
|
|
}
|
|
|