Browse Source

上传代码

weicu
liwei1dao 5 months ago
parent
commit
bde962baa7
  1. 6
      lib/modules/agent/controllers/agent_controller.dart
  2. 8
      local_plugins/device_jieli/ios/Classes/Core/JieliHomeServer.swift
  3. 21
      local_plugins/device_jieli/ios/Classes/Feature/ConnectFeature.swift
  4. 165
      local_plugins/device_jieli/ios/Classes/Feature/JieliEventForwarders.swift
  5. 56
      local_plugins/device_jieli/ios/Classes/Feature/ScanFeature.swift

6
lib/modules/agent/controllers/agent_controller.dart

@ -1755,11 +1755,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
return;
}
// 必须在 Android 上且已连接杰理耳机
if (!Platform.isAndroid) {
Get.snackbar('tips'.tr, '该功能仅安卓端支持');
return;
}
// 必须已连接杰理耳机;iOS / Android 都走 device_jieli 的 AI 助理通路
final BleManager bleManager = Get.find<BleManager>();
if (!bleManager.isConnected) {
Get.snackbar('tips'.tr, '请先连接耳机');

8
local_plugins/device_jieli/ios/Classes/Core/JieliHomeServer.swift

@ -66,6 +66,11 @@ class JieliHomeServer {
private(set) lazy var assistantBridge: AssistantBridge = {
AssistantBridge(server: self)
}()
/// 设备主动推送事件转发器(battery / phoneCallStatus / expandFunction),
/// 对齐 Android 的 `DeviceInfoEventForwarder` + `CustomEventForwarder`。
private(set) lazy var eventForwarders: JieliEventForwarders = {
JieliEventForwarders(server: self)
}()
private init() {}
@ -94,6 +99,8 @@ class JieliHomeServer {
// Hook scan into shared BLE central; scan publishes its own events.
scanFeature.attach(server: self)
connectFeature.attach()
// 把 SDK 的 NSNotification(电量 / 通话状态 / 自定义协议)转发到 Dart。
eventForwarders.attach()
initialized = true
dispatcher.send([
@ -110,6 +117,7 @@ class JieliHomeServer {
translationFeature.stop()
speechFeature.detach()
assistantBridge.shutdown()
eventForwarders.detach()
scanFeature.detach()
initialized = false
}

21
local_plugins/device_jieli/ios/Classes/Feature/ConnectFeature.swift

@ -48,6 +48,7 @@ class ConnectFeature: NSObject {
deviceType: Int,
connectWay: Int
) throws {
NSLog("%@", "[JieliConnect] connect() start ble=\(bleAddress) edr=\(edrAddress ?? "nil") type=\(deviceType) way=\(connectWay)")
server?.dispatcher.send([
"type": "connectionState",
"address": bleAddress,
@ -56,26 +57,33 @@ class ConnectFeature: NSObject {
#if canImport(JL_BLEKit)
guard let server = server else { throw PluginError("CONNECT_FAILED", "server not initialized") }
let mult = server.bleMultiple
NSLog("%@", "[JieliConnect] authEnable=\(mult.authEnable) ble_TIMEOUT=\(mult.ble_TIMEOUT) filter=\(mult.ble_FILTER_ENABLE)")
// Build / lookup entity. JL_BLEMultiple keeps a `blePeripheralArr` with
// discovered entities — we resolve by UUID, matching the iOS demo.
let entity: JL_EntityM
if let cached = entityCache[bleAddress] as? JL_EntityM {
entity = cached
NSLog("%@", "[JieliConnect] entity cache hit for \(bleAddress)")
} else if let made = mult.makeEntity(withUUID: bleAddress) {
entity = made
entityCache[bleAddress] = entity
NSLog("%@", "[JieliConnect] entity makeEntity ok for \(bleAddress)")
} else {
NSLog("%@", "[JieliConnect] entity makeEntity FAILED — peripheral not in JL_BLEMultiple cache (\(bleAddress))")
throw PluginError("CONNECT_FAILED", "remote device not found (UUID=\(bleAddress))")
}
mult.connectEntity(entity) { [weak self] status in
guard let self = self else { return }
NSLog("%@", "[JieliConnect] connectEntity callback status=\(status.rawValue) ble=\(bleAddress)")
switch status {
case .paired:
NSLog("%@", "[JieliConnect] PAIRED — issuing cmdTargetFeatureResult (RCSP init handshake) for \(bleAddress)")
self.connectedId = bleAddress
self.connectedName = entity.mPeripheral.name
// RCSP target-feature query — same role as Android's connect-success
// path that issues `cmdTargetFeatureResult` after handshake.
entity.mCmdManager.cmdTargetFeatureResult { _, _, _ in
entity.mCmdManager.cmdTargetFeatureResult { result, response, error in
NSLog("%@", "[JieliConnect] cmdTargetFeatureResult result=\(result) response=\(String(describing: response)) error=\(String(describing: error)) for \(bleAddress)")
self.server?.dispatcher.send([
"type": "connectionState",
"address": bleAddress,
@ -88,8 +96,9 @@ class ConnectFeature: NSObject {
])
}
case .connecting:
break
NSLog("%@", "[JieliConnect] CONNECTING \(bleAddress)")
case .disconnectOk:
NSLog("%@", "[JieliConnect] DISCONNECT_OK \(bleAddress)")
self.server?.dispatcher.send([
"type": "connectionState",
"address": bleAddress,
@ -97,6 +106,9 @@ class ConnectFeature: NSObject {
])
default:
// Any other terminal failure → expose as 0/disconnect with a code.
// 注意:authEnable=true 时,未授权设备会在 RCSP 握手阶段被 SDK 主动断开,
// 通常会以 .disconnect 或 .timeout 等非 .paired 状态走到这里。
NSLog("%@", "[JieliConnect] TERMINAL FAILURE status=\(status.rawValue) (authEnable=\(self.server?.bleMultiple.authEnable ?? false)) — surfacing as disconnect for \(bleAddress)")
self.server?.dispatcher.send([
"type": "connectionState",
"address": bleAddress,
@ -105,6 +117,7 @@ class ConnectFeature: NSObject {
}
}
#else
NSLog("%@", "[JieliConnect] JL_BLEKit not linked — cannot connect \(bleAddress)")
throw PluginError("CONNECT_FAILED", "JL_BLEKit framework not linked")
#endif
}
@ -150,6 +163,7 @@ class ConnectFeature: NSObject {
// BleMultiple flow publishes via the `paired` callback above, so the only
// job here is to ensure consistency for native subscribers.
if let cbp = note.object as? CBPeripheral {
NSLog("%@", "[JieliConnect] notif kJL_BLE_M_ENTITY_CONNECTED id=\(cbp.identifier.uuidString) name=\(cbp.name ?? "<nil>")")
connectedId = cbp.identifier.uuidString
connectedName = cbp.name
}
@ -158,6 +172,7 @@ class ConnectFeature: NSObject {
@objc private func onDisconnected(_ note: Notification) {
if let cbp = note.object as? CBPeripheral {
let id = cbp.identifier.uuidString
NSLog("%@", "[JieliConnect] notif kJL_BLE_M_ENTITY_DISCONNECTED id=\(id) name=\(cbp.name ?? "<nil>")")
server?.dispatcher.send([
"type": "connectionState",
"address": id,
@ -168,9 +183,11 @@ class ConnectFeature: NSObject {
}
@objc private func onAdapterOff(_ note: Notification) {
NSLog("%@", "[JieliConnect] notif kJL_BLE_M_OFF (adapter off)")
server?.dispatcher.send(["type": "adapterStatus", "enabled": false, "hasBle": true])
}
@objc private func onAdapterOn(_ note: Notification) {
NSLog("%@", "[JieliConnect] notif kJL_BLE_M_ON (adapter on)")
server?.dispatcher.send(["type": "adapterStatus", "enabled": true, "hasBle": true])
}
}

165
local_plugins/device_jieli/ios/Classes/Feature/JieliEventForwarders.swift

@ -0,0 +1,165 @@
import Foundation
#if canImport(JL_BLEKit)
import JL_BLEKit
#endif
/// 把 JL_BLEKit 的 NSNotification(设备主动推送)翻译成 EventDispatcher
/// 事件。对齐 Android `event/DeviceInfoEventForwarder` + `CustomEventForwarder`:
///
/// - `kJL_MANAGER_SYSTEM_INFO` / `kJL_MANAGER_TARGET_INFO` → `battery`
/// (耳机端电量变化、放回仓、设备主动同步系统信息时都会触发;这里只在
/// `outputDeviceModel().battery` 真的变化时才往 Dart 抛,避免高频空包)
/// - `kJL_MANAGER_CALL_STATUS` → `phoneCallStatus`
/// (通话状态变化;BleManager._isCall 跟 InternalCleanupListener 都依赖它)
/// - `kJL_MANAGER_CUSTOM_DATA` → `expandFunction`
/// (耳机厂商私有协议主动上报,opCode=0x16/0x17 用来发起/停止通话翻译;
/// Android 的 `CustomEventForwarder.onExpandFunction` 等价路径)
///
/// 通知 payload 格式来自 SDK 头注释:
/// `@{ kJL_MANAGER_KEY_UUID: uuid, kJL_MANAGER_KEY_OBJECT: object }`
/// 实际可能放在 `note.object` 也可能在 `note.userInfo`,所以读取做了兜底。
final class JieliEventForwarders: NSObject {
weak var server: JieliHomeServer?
private var lastBatteryLevel: Int = -1
private var lastCallStatus: Int = -1
init(server: JieliHomeServer) {
self.server = server
super.init()
}
func attach() {
#if canImport(JL_BLEKit)
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(onSystemInfo(_:)),
name: NSNotification.Name(kJL_MANAGER_SYSTEM_INFO), object: nil)
nc.addObserver(self, selector: #selector(onTargetInfo(_:)),
name: NSNotification.Name(kJL_MANAGER_TARGET_INFO), object: nil)
nc.addObserver(self, selector: #selector(onCallStatus(_:)),
name: NSNotification.Name(kJL_MANAGER_CALL_STATUS), object: nil)
nc.addObserver(self, selector: #selector(onCustomData(_:)),
name: NSNotification.Name(kJL_MANAGER_CUSTOM_DATA), object: nil)
// 设备断开时清空去重缓存,避免下一次重连首次电量/通话状态变化被
// "和上次一样" 误判而吞掉。
nc.addObserver(self, selector: #selector(onEntityDisconnected(_:)),
name: NSNotification.Name(kJL_BLE_M_ENTITY_DISCONNECTED), object: nil)
#endif
}
func detach() {
NotificationCenter.default.removeObserver(self)
lastBatteryLevel = -1
lastCallStatus = -1
}
// MARK: - Helpers
/// SDK 把 `@{UUID, OBJECT}` 字典塞在 NSNotification 的哪个字段并不一致
/// (`object` 或 `userInfo`),这里两个位置都查一遍。
private func payloadDict(_ note: Notification) -> [AnyHashable: Any]? {
if let dict = note.object as? [AnyHashable: Any] { return dict }
if let info = note.userInfo, !info.isEmpty { return info }
return nil
}
#if canImport(JL_BLEKit)
private func uuid(from note: Notification) -> String {
let dict = payloadDict(note) ?? [:]
if let s = dict[kJL_MANAGER_KEY_UUID] as? String { return s }
return ""
}
private func obj(from note: Notification) -> Any? {
payloadDict(note)?[kJL_MANAGER_KEY_OBJECT]
}
#endif
// MARK: - Battery
@objc private func onSystemInfo(_ note: Notification) {
publishBatteryIfChanged(note: note)
}
@objc private func onTargetInfo(_ note: Notification) {
publishBatteryIfChanged(note: note)
}
private func publishBatteryIfChanged(note: Notification) {
#if canImport(JL_BLEKit)
guard let server = server, server.initialized else { return }
let address = uuid(from: note)
// 当前 session 只跟踪一个已连接设备;UUID 对不上就放弃,避免抢
// 别的 entity 的状态。
guard let entity = server.connectFeature.currentEntity() as? JL_EntityM,
!address.isEmpty, entity.mUUID == address else { return }
let battery = Int(entity.mCmdManager.outputDeviceModel().battery)
// 0 是 SDK 没拿到信息时的占位;忽略 0 → 非 0 之外的"覆盖性 0",
// 防止短暂的握手抖动把已经显示出来的电量重置回 0。
if battery == 0 && lastBatteryLevel > 0 { return }
if battery == lastBatteryLevel { return }
lastBatteryLevel = battery
server.dispatcher.send([
"type": "battery",
"address": address,
"level": battery,
])
#endif
}
// MARK: - Phone call status
@objc private func onCallStatus(_ note: Notification) {
guard let server = server, server.initialized else { return }
#if canImport(JL_BLEKit)
let address = uuid(from: note)
// 杰理 iOS SDK 的 call status 通过 NSNumber 抛上来;解不出来就当 0
// (= idle),整段链路语义都是 "status != 0 表示通话中"。
let status: Int
if let n = obj(from: note) as? NSNumber {
status = n.intValue
} else if let n = obj(from: note) as? Int {
status = n
} else {
status = 0
}
if status == lastCallStatus { return }
lastCallStatus = status
server.dispatcher.send([
"type": "phoneCallStatus",
"address": address,
"status": status,
])
#endif
}
// MARK: - Disconnect reset
@objc private func onEntityDisconnected(_ note: Notification) {
lastBatteryLevel = -1
lastCallStatus = -1
}
// MARK: - Expand function (vendor custom command, device-initiated)
@objc private func onCustomData(_ note: Notification) {
guard let server = server, server.initialized else { return }
#if canImport(JL_BLEKit)
let address = uuid(from: note)
guard let data = obj(from: note) as? Data, !data.isEmpty else { return }
// Android 的 onExpandFunction(opCode, payload) 把首字节当 opCode,
// 其余作为 payload。iOS `cmdCustomData` 出站方向也是这么拼的
// (见 CustomCmdFeature.send),按对称把入站拆回去。
let opCode = Int(data[0])
let payload = data.count > 1 ? data.subdata(in: 1..<data.count) : Data()
server.dispatcher.send([
"type": "expandFunction",
"address": address,
"opCode": opCode,
"payloadBase64": payload.base64EncodedString(),
])
#endif
}
}

56
local_plugins/device_jieli/ios/Classes/Feature/ScanFeature.swift

@ -143,6 +143,46 @@ class ScanFeature: NSObject, CBCentralManagerDelegate {
let name = peripheral.name
?? (advertisementData[CBAdvertisementDataLocalNameKey] as? String)
// === 调试用:打印每一条广播(过滤前),完整 dump 广播 hex ===
// iOS CoreBluetooth 不暴露原始广播字节,这里把 advertisementData
// 里所有字段按 BLE AD type 拼出 hex 方便核对。
let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
let serviceUuids = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data]
let txPower = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber
let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String
let isConnectable = (advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber)?.boolValue ?? false
let solicited = advertisementData[CBAdvertisementDataSolicitedServiceUUIDsKey] as? [CBUUID]
let overflow = advertisementData[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID]
let mfrHex = manufacturerData?.map { String(format: "%02X", $0) }.joined() ?? "nil"
let svcUuids = serviceUuids?.map { $0.uuidString }.joined(separator: ",") ?? ""
let svcDataHex = serviceData?.map { (k, v) in
"\(k.uuidString)=" + v.map { String(format: "%02X", $0) }.joined()
}.joined(separator: ";") ?? "nil"
let txPowerStr = txPower.map { "\($0.intValue)" } ?? "nil"
let solicitedStr = solicited?.map { $0.uuidString }.joined(separator: ",") ?? ""
let overflowStr = overflow?.map { $0.uuidString }.joined(separator: ",") ?? ""
NSLog("%@", "================ [JieliScan] didDiscover ================")
NSLog("%@", "[JieliScan] name = \(name ?? "<nil>")")
NSLog("%@", "[JieliScan] localName = \(localName ?? "<nil>")")
NSLog("%@", "[JieliScan] id = \(peripheral.identifier.uuidString)")
NSLog("%@", "[JieliScan] rssi = \(RSSI) txPower=\(txPowerStr) connectable=\(isConnectable)")
NSLog("%@", "[JieliScan] mfr(hex) = \(mfrHex)")
if let m = manufacturerData, m.count >= 2 {
let cid = Int(m[0]) | (Int(m[1]) << 8)
let payload = m.count > 2
? m[2..<m.count].map { String(format: "%02X", $0) }.joined()
: ""
NSLog("%@", "[JieliScan] mfr.companyId = 0x\(String(format: "%04X", cid)) payload=\(payload)")
}
NSLog("%@", "[JieliScan] svcUuids = [\(svcUuids)]")
NSLog("%@", "[JieliScan] svcData = \(svcDataHex)")
if !solicitedStr.isEmpty { NSLog("%@", "[JieliScan] solicited = [\(solicitedStr)]") }
if !overflowStr.isEmpty { NSLog("%@", "[JieliScan] overflow = [\(overflowStr)]") }
NSLog("%@", "[JieliScan] =========================================================")
if currentSkipUnnamed && (name?.isEmpty ?? true) { return }
if !currentNameList.isEmpty {
guard let n = name, currentNameList.contains(where: { $0.caseInsensitiveCompare(n) == .orderedSame }) else { return }
@ -151,15 +191,21 @@ class ScanFeature: NSObject, CBCentralManagerDelegate {
let id = peripheral.identifier.uuidString
if !publishedIds.insert(id).inserted { return }
let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
let serviceUuids = (advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID])?
.map { $0.uuidString.uppercased() } ?? []
let serviceUuidsUpper = serviceUuids?.map { $0.uuidString.uppercased() } ?? []
var advFlags: Int? = nil
var mfrCompanyId: Int? = nil
var mfrPayloadHex: String? = nil
var advRecords: [[String: String]] = []
// iOS 拿不到原始扫描记录字节,但 `JieliAdvParser` (Dart 侧) 为 iOS
// 设计的偏移本就是从 manufacturer data(company id 开头)算起的:
// bytes[0..1]=companyId, [4..5]=PID, [7..12]=MAC^0x5A, [13..22]=code
// 所以把 manufacturer 数据完整 hex 喂给 `rawAdv` 即可让绑定流程跑通。
let rawAdvHex: String? = manufacturerData?
.map { String(format: "%02X", $0) }
.joined()
if let m = manufacturerData, m.count >= 2 {
mfrCompanyId = Int(m[0]) | (Int(m[1]) << 8)
if m.count > 2 {
@ -198,12 +244,12 @@ class ScanFeature: NSObject, CBCentralManagerDelegate {
"deviceType": NSNull(), // TODO: parse via JL_AdvParse / JLAdvParse if available
"connectWay": NSNull(), // TODO: parse via JL_AdvParse / JLAdvParse if available
"rssi": RSSI.intValue,
"rawAdv": NSNull(), // CoreBluetooth does not surface raw bytes
"rawAdv": (rawAdvHex as Any?) ?? NSNull(),
"advRecords": advRecords,
"advFlags": advFlags as Any,
"manufacturerCompanyId": mfrCompanyId as Any,
"manufacturerData": mfrPayloadHex as Any,
"serviceUuids": serviceUuids,
"serviceUuids": serviceUuidsUpper,
]
dispatcher?.send(payload)
}

Loading…
Cancel
Save