diff --git a/entry/src/main/ets/components/test/UsbControllerTestView.ets b/entry/src/main/ets/components/test/UsbControllerTestView.ets index 505f93a1..ee24b8e9 100644 --- a/entry/src/main/ets/components/test/UsbControllerTestView.ets +++ b/entry/src/main/ets/components/test/UsbControllerTestView.ets @@ -15,6 +15,7 @@ import { usbManager } from '@kit.BasicServicesKit'; import { UsbDriverService, UsbDriverListener, ControllerType, AbstractController, ButtonFlags } from '../../service/usbdriver'; +import { HidDdkController } from '../../service/usbdriver/HidDdkController'; import { GamepadManager } from '../../service/input/GamepadManager'; import { AXIS_MAX } from '../../service/input/GamepadTypes'; import { AppColors } from '../../common/Theme'; @@ -56,6 +57,7 @@ class DeviceInfo { vid: number = 0; pid: number = 0; type: number = 0; + transport: string = ''; } // USB 驱动监听器实现类 @@ -127,6 +129,7 @@ export struct UsbControllerTestView { @State logMessages: string[] = []; @State pollRate: number = 0; @State pollCount: number = 0; + @State hidChannelInfo: string = ''; private usbDriverService: UsbDriverService | null = null; private listener: TestViewDriverListener | null = null; @@ -134,6 +137,7 @@ export struct UsbControllerTestView { private lastPollStatTime: number = 0; private pollRateTimer: number = -1; private uiRefreshTimer: number = -1; + private hidStatsTimer: number = -1; // 内部缓冲区:USB 轮询回调(高频,125-1000Hz)写入此变量, // 由 UI 刷新定时器同步到 @State 以正确触发 UI 重渲染 private internalLastInputState: ControllerInputState | null = null; @@ -143,13 +147,44 @@ export struct UsbControllerTestView { this.checkUsbDevices(); this.startPollRateTimer(); this.startUiRefreshTimer(); + this.startHidStatsTimer(); } aboutToDisappear(): void { this.stopUiRefreshTimer(); this.stopPollRateTimer(); + this.stopHidStatsTimer(); this.stopDriver(); } + + // 1 秒刷新 HID DDK 通道状态(iface/描述符/报文率/最近错误) + private startHidStatsTimer(): void { + this.hidStatsTimer = setInterval(() => { + try { + const controllers = UsbDriverService.getInstance().getControllers(); + const hidController = controllers.find(c => c instanceof HidDdkController); + if (hidController instanceof HidDdkController) { + const stats = hidController.getReaderStats(); + this.hidChannelInfo = + `接口 ${stats.iface} · 描述符 ${stats.descriptorLength}B · ` + + `${stats.reportsPerSec.toFixed(1)} Hz · 累计 ${stats.totalReports} 报文` + + (stats.lastError !== 0 ? ` · 错误 ${stats.lastError}` : '') + + (stats.running ? '' : ' · 已停止'); + } else { + this.hidChannelInfo = ''; + } + } catch (err) { + this.hidChannelInfo = ''; + } + }, 1000); + } + + private stopHidStatsTimer(): void { + if (this.hidStatsTimer !== -1) { + clearInterval(this.hidStatsTimer); + this.hidStatsTimer = -1; + } + } private startPollRateTimer(): void { this.lastPollStatTime = Date.now(); @@ -228,6 +263,7 @@ export struct UsbControllerTestView { deviceInfo.vid = controller.getVendorId(); deviceInfo.pid = controller.getProductId(); deviceInfo.type = controller.getControllerType(); + deviceInfo.transport = controller.getTransportInfo(); this.connectedDevices = [...this.connectedDevices, deviceInfo]; } @@ -486,7 +522,7 @@ export struct UsbControllerTestView { .fontSize(13) .fontWeight(FontWeight.Medium) .fontColor(AppColors.TextPrimary) - Text(`${this.getControllerTypeName(device.type)} · VID:0x${device.vid.toString(16).toUpperCase()} PID:0x${device.pid.toString(16).toUpperCase()}`) + Text(`${this.getControllerTypeName(device.type)}${device.transport !== '' ? ` · ${device.transport}` : ''} · VID:0x${device.vid.toString(16).toUpperCase()} PID:0x${device.pid.toString(16).toUpperCase()}`) .fontSize(10) .fontColor(AppColors.TextTertiary) } @@ -506,6 +542,29 @@ export struct UsbControllerTestView { .borderRadius(12) .border({ width: 1, color: AppColors.CardBorder }) + // ═══ HID DDK 通道状态卡(实验通道工作时显示) ═══ + if (this.hidChannelInfo !== '') { + Row({ space: 8 }) { + Text('HID-DDK') + .fontSize(10) + .fontWeight(FontWeight.Bold) + .fontColor(AppColors.Success) + .padding({ left: 6, right: 6, top: 2, bottom: 2 }) + .backgroundColor('#1A4CAF50') + .borderRadius(4) + Text(this.hidChannelInfo) + .fontSize(11) + .fontColor(AppColors.TextSecondary) + .fontFamily('monospace') + .layoutWeight(1) + } + .width('100%') + .padding({ left: 12, right: 12, top: 8, bottom: 8 }) + .backgroundColor(AppColors.CardBackground) + .borderRadius(8) + .border({ width: 1, color: AppColors.CardBorder }) + } + // ═══ 输入可视化卡片 ═══ if (this.hasInput) { Column({ space: 10 }) { diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 34e57179..3d4d52f2 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -16,6 +16,7 @@ import { AppStateService } from '../service/AppStateService'; import { UiSettings } from '../service/SettingsService'; import { RcpSessionPool } from '../utils/RcpSessionPool'; import { NetworkErrorClassifierSelfCheck } from '../utils/NetworkErrorClassifierSelfCheck'; +import { HidDdkProbe } from '../service/usbdriver/HidDdkProbe'; const TAG = 'EntryAbility'; const DOMAIN = 0x0000; @@ -45,6 +46,11 @@ export default class EntryAbility extends UIAbility { // 注册应用前后台状态变化监听 this.registerAppStateCallback(); + // HID DDK 主进程可用性自检(无 USB 设备时用哑 deviceId 验证 Init 权限) + HidDdkProbe.startupCheck().catch(() => { + // 自检只打日志,失败无需处理 + }); + // 网络契约自检(纯函数,零开销,仅日志) NetworkErrorClassifierSelfCheck.run(); } diff --git a/entry/src/main/ets/pages/SettingsPageV2.ets b/entry/src/main/ets/pages/SettingsPageV2.ets index 32603d4b..6c09ca5d 100644 --- a/entry/src/main/ets/pages/SettingsPageV2.ets +++ b/entry/src/main/ets/pages/SettingsPageV2.ets @@ -207,6 +207,7 @@ struct SettingsPageV2 { @State usbDriverEnabled: boolean = false; // 默认关闭 USB 手柄驱动 @State forceUsbDriverOnly: boolean = false; // 强制纯 USB 驱动模式 @State ddkHighSpeedPolling: boolean = false; // USB 驱动高速轮询 + @State hidDdkInputChannel: boolean = false; // HID DDK 输入通道(实验) // 输入 - 屏幕控制器 @State enableOnscreenControls: boolean = false; @@ -563,6 +564,7 @@ struct SettingsPageV2 { this.usbDriverEnabled = await this.loadBoolean(SettingsKeys.USB_DRIVER_ENABLED, false); this.forceUsbDriverOnly = await this.loadBoolean(SettingsKeys.FORCE_USB_DRIVER_ONLY, false); this.ddkHighSpeedPolling = await this.loadBoolean(SettingsKeys.DDK_HIGH_SPEED_POLLING, false); + this.hidDdkInputChannel = await this.loadBoolean(SettingsKeys.HID_DDK_INPUT_CHANNEL, false); // 输入 - 屏幕控制器 this.enableOnscreenControls = await this.loadBoolean(SettingsKeys.ENABLE_ONSCREEN_CONTROLS, false); @@ -1727,6 +1729,21 @@ struct SettingsPageV2 { }); } }, + { + title: 'HID DDK 输入通道(实验)', + subtitle: '通过内核 HID 接口直读手柄原始报文,免 USB 接口声明与内核驱动重绑定。需配合"强制 USB 驱动接管输入"使用,不可用时自动回退', + type: 'toggle', + value: this.hidDdkInputChannel, + visible: this.usbDriverEnabled, + action: () => { + this.hidDdkInputChannel = !this.hidDdkInputChannel; + this.saveSetting(SettingsKeys.HID_DDK_INPUT_CHANNEL, this.hidDdkInputChannel); + ToastQueue.show({ + message: '下次串流时生效', + duration: 2500 + }); + } + }, { title: '手柄测试', subtitle: '测试 USB 和蓝牙/系统手柄', diff --git a/entry/src/main/ets/service/SettingsService.ets b/entry/src/main/ets/service/SettingsService.ets index 6379ca6e..f3b41914 100644 --- a/entry/src/main/ets/service/SettingsService.ets +++ b/entry/src/main/ets/service/SettingsService.ets @@ -99,6 +99,7 @@ export class SettingsKeys { static readonly USB_DRIVER_ENABLED: string = 'settings_usb_driver_enabled'; // 默认启用 USB 手柄驱动 static readonly FORCE_USB_DRIVER_ONLY: string = 'settings_force_usb_driver_only'; // 强制纯 USB 驱动模式(禁用 GCK) static readonly DDK_HIGH_SPEED_POLLING: string = 'settings_ddk_high_speed_polling'; // USB 驱动高速轮询(DDK) + static readonly HID_DDK_INPUT_CHANNEL: string = 'settings_hid_ddk_input_channel'; // HID DDK 输入通道(实验,hidraw 直读) // 输入 - 屏幕控制器 static readonly ENABLE_ONSCREEN_CONTROLS: string = 'settings_enable_onscreen_controls'; @@ -209,6 +210,7 @@ export interface InputSettings { usbDriverEnabled: boolean; // 默认启用 USB 手柄驱动 forceUsbDriverOnly: boolean; // 强制纯 USB 驱动模式 ddkHighSpeedPolling: boolean; // USB 驱动高速轮询(DDK) + hidDdkInputChannel: boolean; // HID DDK 输入通道(实验) // 体感助手 gyroAssistEnabled: boolean; @@ -795,6 +797,7 @@ export class SettingsService { usbDriverEnabled: await this.getBoolean(SettingsKeys.USB_DRIVER_ENABLED, false), forceUsbDriverOnly: await this.getBoolean(SettingsKeys.FORCE_USB_DRIVER_ONLY, false), ddkHighSpeedPolling: await this.getBoolean(SettingsKeys.DDK_HIGH_SPEED_POLLING, false), + hidDdkInputChannel: await this.getBoolean(SettingsKeys.HID_DDK_INPUT_CHANNEL, false), // 体感助手 gyroAssistEnabled: await this.getBoolean(SettingsKeys.GYRO_ASSIST_ENABLED, false), diff --git a/entry/src/main/ets/service/usbdriver/AbstractController.ets b/entry/src/main/ets/service/usbdriver/AbstractController.ets index 07797dc0..9c653252 100644 --- a/entry/src/main/ets/service/usbdriver/AbstractController.ets +++ b/entry/src/main/ets/service/usbdriver/AbstractController.ets @@ -319,6 +319,11 @@ export abstract class AbstractController { } } + /** 传输通道标识(供测试页/诊断展示),子类可覆写 */ + getTransportInfo(): string { + return 'USB'; + } + /** * 启动控制器 * @returns 是否启动成功 diff --git a/entry/src/main/ets/service/usbdriver/HidDdkController.ets b/entry/src/main/ets/service/usbdriver/HidDdkController.ets new file mode 100644 index 00000000..97097ea9 --- /dev/null +++ b/entry/src/main/ets/service/usbdriver/HidDdkController.ets @@ -0,0 +1,228 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2024-2025 Moonlight/AlkaidLab + * + * 本程序基于 GPL-3.0 发布,详见 LICENSE + */ + +/** + * 基于 HID DDK (hidraw) 的 USB 手柄控制器 + * + * 与 usbManager bulkTransfer / USB DDK 轮询通道的区别: + * - 不 open pipe、不 claim 接口、不 detach 内核 HID 驱动 + * - 输入:OH_Hid_ReadTimeout 阻塞读(内核中断驱动) + * - 输出:OH_Hid_Write 输出报告(震动) + * - 解析:与 NativeHidController 共享 HidReportParserUtil + * + * 前置条件:start() 前须已 setDeviceLocation()(用于 busNum/devAddress → DDK deviceId) + */ + +import { usbManager } from '@kit.BasicServicesKit'; +import { AbstractController } from './AbstractController'; +import { UsbDriverListener } from './UsbDriverListener'; +import { ButtonFlags, ControllerType, ControllerCapabilities } from './ControllerConstants'; +import { NativeHidParserService, NativeGamepadType } from './NativeHidParserService'; +import { HidReportParserUtil } from './HidReportParserUtil'; +import { HidDdkReader, HidReaderStats } from './HidDdkReader'; +import { DdkUsbPoller } from './DdkUsbPoller'; + +const TAG = '[USB-HidDdk]'; + +export class HidDdkController extends AbstractController { + private device: usbManager.USBDevice; + private reader: HidDdkReader = new HidDdkReader(); + private nativeParser: NativeHidParserService; + private running: boolean = false; + private forceProtocolType: number = 0; // 0=自动 + private lastActivityTime: number = 0; + + constructor( + device: usbManager.USBDevice, + controllerId: number, + listener: UsbDriverListener + ) { + super(controllerId, listener, device.vendorId, device.productId); + this.device = device; + this.nativeParser = NativeHidParserService.getInstance(); + this.nativeParser.init(); + + const nativeType = this.nativeParser.getGamepadType(device.vendorId, device.productId); + this.deviceName = this.nativeParser.getGamepadName(device.vendorId, device.productId); + + switch (nativeType) { + case NativeGamepadType.XBOX: + this.type = ControllerType.XBOX; + break; + case NativeGamepadType.PLAYSTATION: + this.type = ControllerType.PS; + break; + case NativeGamepadType.SWITCH: + this.type = ControllerType.NINTENDO; + break; + default: + this.type = ControllerType.XBOX; + } + + this.capabilities = ControllerCapabilities.ANALOG_TRIGGERS | ControllerCapabilities.RUMBLE; + + this.supportedButtonFlags = + ButtonFlags.A_FLAG | ButtonFlags.B_FLAG | ButtonFlags.X_FLAG | ButtonFlags.Y_FLAG | + ButtonFlags.UP_FLAG | ButtonFlags.DOWN_FLAG | ButtonFlags.LEFT_FLAG | ButtonFlags.RIGHT_FLAG | + ButtonFlags.LB_FLAG | ButtonFlags.RB_FLAG | + ButtonFlags.LS_CLK_FLAG | ButtonFlags.RS_CLK_FLAG | + ButtonFlags.BACK_FLAG | ButtonFlags.PLAY_FLAG | ButtonFlags.SPECIAL_BUTTON_FLAG; + + console.info(`${TAG} 创建控制器: ${this.deviceName} (type=${nativeType})`); + } + + setForceProtocolType(protocolType: number): void { + this.forceProtocolType = protocolType; + console.info(`${TAG} 设置强制协议类型: ${protocolType}`); + } + + getForceProtocolType(): number { + return this.forceProtocolType; + } + + start(): boolean { + if (this.running) { + return true; + } + this.markInitializing(); + + // 解析 DDK deviceId(与 DdkUsbPoller 相同的 busNum/devAddress 编码) + const initResult = DdkUsbPoller.init(); + if (initResult.code !== 0) { + console.error(`${TAG} USB DDK 初始化失败 (${initResult.error}),无法确定 deviceId`); + return false; + } + const idResult = DdkUsbPoller.makeDeviceId(this.busNum, this.devAddress); + if (!idResult.found || idResult.deviceId === undefined) { + console.error(`${TAG} deviceId 解析失败 (${idResult.error})`); + return false; + } + + const ok = this.reader.start( + idResult.deviceId, + (data: Uint8Array) => this.processInputReport(data), + (errorCode: number) => this.handleReadError(errorCode), + 100 + ); + + if (!ok) { + console.error(`${TAG} hidraw 通道启动失败: code=${this.reader.lastStartError},上层将回退旧通道`); + return false; + } + + this.running = true; + this.lastActivityTime = Date.now(); + this.notifyDeviceAdded(); + + console.info(`${TAG} 控制器已启动 (hidraw): ${this.deviceName}`); + return true; + } + + stop(): void { + if (!this.running) { + return; + } + this.running = false; + this.markStopped(); + // 先停 reader(join 读线程),再通知移除——避免 tsfn 队列中的残留报文 + // 在设备已通知移除后仍触发 processInputReport 上报 + this.reader.stop(); + this.notifyDeviceRemoved(); + console.info(`${TAG} 控制器已停止: ${this.deviceName}`); + } + + isConnected(): boolean { + return this.running; + } + + getLastActivityTime(): number { + return this.lastActivityTime; + } + + mayBeSilentlyDisconnected(inactivityThresholdMs: number = 30000): boolean { + if (!this.running) { + return true; + } + return (Date.now() - this.lastActivityTime) > inactivityThresholdMs; + } + + rumble(lowFreqMotor: number, highFreqMotor: number): void { + const clamp = (value: number): number => { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.min(65535, Math.round(value))); + }; + + const cmd = this.nativeParser.createRumbleCommand( + this.vendorId, + this.productId, + clamp(lowFreqMotor), + clamp(highFreqMotor) + ); + + if (cmd) { + const ret = this.reader.sendOutput(cmd); + if (ret < 0) { + console.warn(`${TAG} rumble 输出失败: ret=${ret}`); + } + } + } + + rumbleTriggers(_leftTrigger: number, _rightTrigger: number): void { + // 大多数手柄不支持扳机震动 + } + + /** 通道标识(供测试页/诊断展示) */ + getTransportInfo(): string { + return 'HID-DDK'; + } + + getReaderStats(): HidReaderStats { + return this.reader.getStats(); + } + + private processInputReport(data: Uint8Array): void { + // stop() 后 tsfn 队列可能仍投递残留报文,直接丢弃 + if (!this.running) { + return; + } + const state = HidReportParserUtil.parseToState( + this.vendorId, + this.productId, + data, + this.forceProtocolType + ); + + if (!state) { + return; + } + + this.lastActivityTime = Date.now(); + this.buttonFlags = state.buttonFlags; + this.leftStickX = state.leftStickX; + this.leftStickY = state.leftStickY; + this.rightStickX = state.rightStickX; + this.rightStickY = state.rightStickY; + this.leftTrigger = state.leftTrigger; + this.rightTrigger = state.rightTrigger; + + this.reportInput(); + } + + private handleReadError(errorCode: number): void { + console.error(`${TAG} hidraw 读错误: code=${errorCode},停止控制器`); + // 归零输入,确保按键释放 + this.buttonFlags = 0; + this.leftStickX = 0; + this.leftStickY = 0; + this.rightStickX = 0; + this.rightStickY = 0; + this.leftTrigger = 0; + this.rightTrigger = 0; + this.reportInput(); + this.stop(); + } +} diff --git a/entry/src/main/ets/service/usbdriver/HidDdkProbe.ets b/entry/src/main/ets/service/usbdriver/HidDdkProbe.ets new file mode 100644 index 00000000..6998c46b --- /dev/null +++ b/entry/src/main/ets/service/usbdriver/HidDdkProbe.ets @@ -0,0 +1,126 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2024-2025 Moonlight/AlkaidLab + * + * HID DDK Probe - 验证 HID DDK 主进程可用性的 ArkTS 封装 + * + * 只做被动探测(打开 hidraw → 读描述符 → 限时读报文 → 关闭), + * 不接管输入、不干扰现有 USB 驱动流程,结果仅用于日志与后续决策。 + */ + +import nativeLib from 'libmoonlight_nativelib.so'; +import { usbManager } from '@kit.BasicServicesKit'; + +const TAG = '[HID-DDK-Probe]'; + +interface HidProbeNativeInterface { + isAvailable(): boolean; + probe(deviceId: number, readMs: number, onResult: (result: HidProbeResult) => void): void; +} + +interface NativeLibWithHidProbe { + HidDdkProbe?: HidProbeNativeInterface; +} + +export interface HidProbeResult { + available: boolean; + opened: boolean; + initCode: number; + openCode: number; + descCode: number; + initError?: string; + openError?: string; + interfaceIndex?: number; + vid?: number; + pid?: number; + busType?: number; + name?: string; + descriptorLength?: number; + descriptor?: Uint8Array; + reportCount?: number; + reportsPerSec?: number; + sampleReports?: Uint8Array[]; +} + +const hidProbeNative = (nativeLib as NativeLibWithHidProbe).HidDdkProbe; + +// 启动自检只跑一次 +let probedStartup = false; + +export class HidDdkProbe { + + /** + * native 模块与 libhid.z.so 是否可用 + */ + static isAvailable(): boolean { + if (!hidProbeNative) { + return false; + } + try { + return hidProbeNative.isAvailable(); + } catch (err) { + console.error(`${TAG} isAvailable 异常:`, err); + return false; + } + } + + /** + * 异步探测指定 DDK deviceId + */ + static probe(deviceId: number, readMs: number = 2000): Promise { + return new Promise((resolve) => { + if (!hidProbeNative) { + resolve({ available: false, opened: false, initCode: -1, openCode: -1, descCode: -1 }); + return; + } + try { + hidProbeNative.probe(deviceId, readMs, (result: HidProbeResult) => { + resolve(result); + }); + } catch (err) { + console.error(`${TAG} probe 异常:`, err); + resolve({ available: false, opened: false, initCode: -2, openCode: -2, descCode: -2 }); + } + }); + } + + /** + * 启动期环境自检(模拟器友好) + * + * 不依赖真实 USB 手柄:用无效 deviceId 跑一遍探测, + * 核心结论是 OH_Hid_Init 的权限/服务检查(HID DDK 主进程可用性)。 + * Open 返回 DEVICE_NOT_FOUND 属预期。接入真手柄时走 probeOnceForDevice 全链路探测。 + */ + static async startupCheck(): Promise { + if (probedStartup) { + return; + } + probedStartup = true; + + // 真机上若有 USB 设备接入,交给 handleUsbDevice 的全链路探测 + try { + if (usbManager.getDevices().length > 0) { + return; + } + } catch (err) { + console.warn(`${TAG} 枚举 USB 设备异常,继续启动自检:`, err); + } + + if (!HidDdkProbe.isAvailable()) { + console.warn(`${TAG} 启动自检: libhid.z.so 不可用 (系统低于 API 18 或符号缺失)`); + return; + } + + console.info(`${TAG} 启动自检: 无 USB 设备,用哑 deviceId 验证 Init 权限`); + const result = await HidDdkProbe.probe(1, 300); + if (result.initCode === 0) { + console.info(`${TAG} 启动自检结论: OH_Hid_Init=0 主进程可用` + + `${result.openCode === 27300009 ? ' (Open=DEVICE_NOT_FOUND 属预期,无设备)' : ''}`); + } else if (result.initCode === 201) { + console.error(`${TAG} 启动自检结论: OH_Hid_Init=201 NO_PERM — 主进程使用 HID DDK 被拒,` + + `检查签名 profile 是否含 ACCESS_DDK_HID;若仍被拒则 DriverExtensionAbility 限制为硬性`); + } else { + console.error(`${TAG} 启动自检结论: OH_Hid_Init=${result.initCode} (${result.initError})`); + } + } +} diff --git a/entry/src/main/ets/service/usbdriver/HidDdkReader.ets b/entry/src/main/ets/service/usbdriver/HidDdkReader.ets new file mode 100644 index 00000000..59765733 --- /dev/null +++ b/entry/src/main/ets/service/usbdriver/HidDdkReader.ets @@ -0,0 +1,169 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2024-2025 Moonlight/AlkaidLab + * + * HID DDK Reader - hidraw 输入通道 ArkTS 封装 + * + * 内核中断驱动的 OH_Hid_ReadTimeout 阻塞读,无需 claim USB 接口。 + * 对应 native exports.HidDdk 命名空间(hid_ddk_probe.cpp)。 + */ + +import nativeLib from 'libmoonlight_nativelib.so'; + +const TAG = '[HID-DDK-Reader]'; + +interface HidDdkNativeInterface { + isAvailable(): boolean; + startReader(deviceId: number, readTimeoutMs: number, + onReport: (readerId: number, data: Uint8Array, length: number) => void, + onError: (readerId: number, errorCode: number) => void): number; + stopReader(readerId: number): void; + writeOutput(readerId: number, data: Uint8Array): number; + getReaderStats(readerId: number): HidReaderStats; +} + +interface NativeLibWithHidDdk { + HidDdk?: HidDdkNativeInterface; +} + +export interface HidReaderStats { + iface: number; + descriptorLength: number; + totalReports: number; + totalBytes: number; + lastError: number; + reportsPerSec: number; + running: boolean; +} + +const hidDdkNative = (nativeLib as NativeLibWithHidDdk).HidDdk; + +export class HidDdkReader { + + private readerId: number = -1; + private _running: boolean = false; + // 最近一次 start 失败的 HID_DDK 错误码(0=无错误),供上层日志/回退决策 + private _lastStartError: number = 0; + + static isAvailable(): boolean { + if (!hidDdkNative) { + return false; + } + try { + return hidDdkNative.isAvailable(); + } catch (err) { + console.error(`${TAG} isAvailable 异常:`, err); + return false; + } + } + + get running(): boolean { + return this._running; + } + + get lastStartError(): number { + return this._lastStartError; + } + + /** + * 启动 hidraw 读通道(native 侧同步打开,失败同步返回 false) + * + * @param deviceId DDK 设备 ID(busNum<<32|devAddress,来自 DdkUsbPoller.makeDeviceId) + * @param onReport 每个输入报文回调 + * @param onError 读线程 IO 错误(设备拔出等),通道已终止 + * @param readTimeoutMs 单次读超时(当前 native 固定 100ms,参数预留) + */ + start( + deviceId: number, + onReport: (data: Uint8Array) => void, + onError: (errorCode: number) => void, + readTimeoutMs: number = 100 + ): boolean { + if (!HidDdkReader.isAvailable()) { + console.warn(`${TAG} native 不可用 (系统低于 API 18 或符号缺失)`); + return false; + } + if (this._running) { + console.warn(`${TAG} Reader 已在运行`); + return false; + } + + try { + const readerId = hidDdkNative!.startReader( + deviceId, readTimeoutMs, + (_readerId: number, data: Uint8Array, length: number) => { + if (length > 0) { + onReport(data); + } + }, + (_readerId: number, errorCode: number) => { + this._running = false; + onError(errorCode); + } + ); + + if (readerId < 0) { + // 同步打开失败:readerId 为 -(HID_DDK 错误码) + this._lastStartError = -readerId; + console.error(`${TAG} startReader 失败: code=${-readerId}`); + return false; + } + + this._lastStartError = 0; + this.readerId = readerId; + this._running = true; + console.info(`${TAG} Reader 已启动: readerId=${readerId}`); + return true; + } catch (err) { + console.error(`${TAG} start 异常:`, err); + return false; + } + } + + stop(): void { + if (!this._running && this.readerId < 0) { + return; + } + try { + hidDdkNative!.stopReader(this.readerId); + console.info(`${TAG} Reader 已停止: readerId=${this.readerId}`); + } catch (err) { + console.error(`${TAG} stop 异常:`, err); + } + this._running = false; + this.readerId = -1; + } + + /** + * 发送输出报告(震动/LED),返回写入字节数,负数为错误码 + */ + sendOutput(data: Uint8Array): number { + if (this.readerId < 0 || !hidDdkNative) { + return -1; + } + try { + return hidDdkNative.writeOutput(this.readerId, data); + } catch (err) { + console.error(`${TAG} sendOutput 异常:`, err); + return -1; + } + } + + getStats(): HidReaderStats { + if (this.readerId < 0 || !hidDdkNative) { + return { + iface: 0, descriptorLength: 0, totalReports: 0, totalBytes: 0, + lastError: 0, reportsPerSec: 0, running: false + }; + } + try { + return hidDdkNative.getReaderStats(this.readerId); + } catch (err) { + console.error(`${TAG} getStats 异常:`, err); + return { + iface: 0, descriptorLength: 0, totalReports: 0, totalBytes: 0, + lastError: 0, reportsPerSec: 0, running: false + }; + } + } +} diff --git a/entry/src/main/ets/service/usbdriver/HidReportParserUtil.ets b/entry/src/main/ets/service/usbdriver/HidReportParserUtil.ets new file mode 100644 index 00000000..f194f8b8 --- /dev/null +++ b/entry/src/main/ets/service/usbdriver/HidReportParserUtil.ets @@ -0,0 +1,158 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2024-2025 Moonlight/AlkaidLab + * + * 本程序基于 GPL-3.0 发布,详见 LICENSE + */ + +/** + * 共享 HID 报文解析工具 + * + * 从 NativeHidController 抽取,供多条输入通道复用 + * (usbManager bulkTransfer / HID DDK hidraw 等): + * native Gamepad.parseHidReport 优先,失败时走通用布局 TS 兜底。 + */ + +import { ButtonFlags } from './ControllerConstants'; +import { AXIS_MAX, TRIGGER_MAX } from '../input/GamepadTypes'; +import { NativeHidParserService, NativeButtonFlags } from './NativeHidParserService'; + +const TAG = '[HID-Parser-Util]'; + +/** + * 解析后的控制器状态(Moonlight 归一化格式) + * 轴 -1.0..1.0,扳机 0.0..1.0,buttonFlags 为 Moonlight ButtonFlags + */ +export interface ParsedHidState { + buttonFlags: number; + leftStickX: number; + leftStickY: number; + rightStickX: number; + rightStickY: number; + leftTrigger: number; + rightTrigger: number; +} + +export class HidReportParserUtil { + + private static parser: NativeHidParserService | null = null; + + private static getParser(): NativeHidParserService { + if (!HidReportParserUtil.parser) { + const parser = NativeHidParserService.getInstance(); + parser.init(); + HidReportParserUtil.parser = parser; + } + return HidReportParserUtil.parser; + } + + /** + * 解析原始 HID 报文为 Moonlight 归一化状态 + * + * @param vendorId 设备 VID + * @param productId 设备 PID + * @param data 原始报文字节 + * @param forceProtocolType 0=自动, 1=Xbox, 2=DS4, 3=Switch, 4=Generic, 5=DualSense + * @returns 解析结果;两条路径都失败时返回 null + */ + static parseToState(vendorId: number, productId: number, data: Uint8Array, + forceProtocolType: number = 0): ParsedHidState | null { + const state = HidReportParserUtil.getParser().parseHidReport(vendorId, productId, data, forceProtocolType); + if (state) { + return { + buttonFlags: HidReportParserUtil.convertNativeButtons(state.buttons), + leftStickX: state.leftStickX / AXIS_MAX, + leftStickY: state.leftStickY / AXIS_MAX, + rightStickX: state.rightStickX / AXIS_MAX, + rightStickY: state.rightStickY / AXIS_MAX, + leftTrigger: state.leftTrigger / TRIGGER_MAX, + rightTrigger: state.rightTrigger / TRIGGER_MAX + }; + } + return HidReportParserUtil.parseFallbackLayout(data); + } + + /** + * 通用 HID 手柄布局兜底解析(与原 NativeHidController.fallbackParseHidReport 布局一致) + * + * 布局 (≥9 字节): + * [0] Report ID [1-4] 左/右摇杆 XY (0x80=中心) [5] Triggers/misc + * [6] HAT/D-Pad (0x0F=中心) [7] 功能按钮字节 [8] 面板按钮字节 + */ + static parseFallbackLayout(data: Uint8Array): ParsedHidState | null { + if (data.length < 9) { + console.warn(`${TAG} HID 数据太短: ${data.length}`); + return null; + } + + const clamp = (v: number): number => Math.max(-1.0, Math.min(1.0, v)); + const leftStickX = clamp((data[1] - 128) / 127.0); + const leftStickY = clamp((data[2] - 128) / 127.0); + const rightStickX = clamp((data[3] - 128) / 127.0); + const rightStickY = clamp((data[4] - 128) / 127.0); + + let flags = 0; + const hat = data[6] & 0x0F; + switch (hat) { + case 0: flags |= ButtonFlags.UP_FLAG; break; + case 1: flags |= ButtonFlags.UP_FLAG | ButtonFlags.RIGHT_FLAG; break; + case 2: flags |= ButtonFlags.RIGHT_FLAG; break; + case 3: flags |= ButtonFlags.DOWN_FLAG | ButtonFlags.RIGHT_FLAG; break; + case 4: flags |= ButtonFlags.DOWN_FLAG; break; + case 5: flags |= ButtonFlags.DOWN_FLAG | ButtonFlags.LEFT_FLAG; break; + case 6: flags |= ButtonFlags.LEFT_FLAG; break; + case 7: flags |= ButtonFlags.UP_FLAG | ButtonFlags.LEFT_FLAG; break; + default: break; // 0x0F = center + } + + const btn1 = data[7]; + const btn2 = data[8]; + if (btn1 & 0x01) flags |= ButtonFlags.BACK_FLAG; + if (btn1 & 0x02) flags |= ButtonFlags.PLAY_FLAG; + if (btn1 & 0x04) flags |= ButtonFlags.SPECIAL_BUTTON_FLAG; + if (btn1 & 0x08) flags |= ButtonFlags.LS_CLK_FLAG; + if (btn1 & 0x10) flags |= ButtonFlags.RS_CLK_FLAG; + if (btn1 & 0x20) flags |= ButtonFlags.LB_FLAG; + if (btn1 & 0x40) flags |= ButtonFlags.RB_FLAG; + if (btn2 & 0x01) flags |= ButtonFlags.A_FLAG; + if (btn2 & 0x02) flags |= ButtonFlags.B_FLAG; + if (btn2 & 0x04) flags |= ButtonFlags.X_FLAG; + if (btn2 & 0x08) flags |= ButtonFlags.Y_FLAG; + + return { + buttonFlags: flags, + leftStickX, leftStickY, rightStickX, rightStickY, + leftTrigger: 0, + rightTrigger: 0 + }; + } + + /** + * native 按钮位 → Moonlight ButtonFlags(与原 NativeHidController.convertNativeButtons 一致) + */ + static convertNativeButtons(nativeButtons: number): number { + let flags = 0; + + if (nativeButtons & NativeButtonFlags.UP) flags |= ButtonFlags.UP_FLAG; + if (nativeButtons & NativeButtonFlags.DOWN) flags |= ButtonFlags.DOWN_FLAG; + if (nativeButtons & NativeButtonFlags.LEFT) flags |= ButtonFlags.LEFT_FLAG; + if (nativeButtons & NativeButtonFlags.RIGHT) flags |= ButtonFlags.RIGHT_FLAG; + + if (nativeButtons & NativeButtonFlags.START) flags |= ButtonFlags.PLAY_FLAG; + if (nativeButtons & NativeButtonFlags.BACK) flags |= ButtonFlags.BACK_FLAG; + if (nativeButtons & NativeButtonFlags.LS_CLK) flags |= ButtonFlags.LS_CLK_FLAG; + if (nativeButtons & NativeButtonFlags.RS_CLK) flags |= ButtonFlags.RS_CLK_FLAG; + + if (nativeButtons & NativeButtonFlags.LB) flags |= ButtonFlags.LB_FLAG; + if (nativeButtons & NativeButtonFlags.RB) flags |= ButtonFlags.RB_FLAG; + + if (nativeButtons & NativeButtonFlags.A) flags |= ButtonFlags.A_FLAG; + if (nativeButtons & NativeButtonFlags.B) flags |= ButtonFlags.B_FLAG; + if (nativeButtons & NativeButtonFlags.X) flags |= ButtonFlags.X_FLAG; + if (nativeButtons & NativeButtonFlags.Y) flags |= ButtonFlags.Y_FLAG; + + if (nativeButtons & NativeButtonFlags.HOME) flags |= ButtonFlags.SPECIAL_BUTTON_FLAG; + + return flags; + } +} diff --git a/entry/src/main/ets/service/usbdriver/NativeHidController.ets b/entry/src/main/ets/service/usbdriver/NativeHidController.ets index 369f65fd..959ccd56 100644 --- a/entry/src/main/ets/service/usbdriver/NativeHidController.ets +++ b/entry/src/main/ets/service/usbdriver/NativeHidController.ets @@ -19,8 +19,8 @@ import { usbManager } from '@kit.BasicServicesKit'; import { AbstractController } from './AbstractController'; import { UsbDriverListener } from './UsbDriverListener'; import { ButtonFlags, UsbClass, ControllerType, ControllerCapabilities } from './ControllerConstants'; -import { AXIS_MAX, TRIGGER_MAX } from '../input/GamepadTypes'; -import { NativeHidParserService, NativeGamepadState, NativeGamepadType, NativeButtonFlags } from './NativeHidParserService'; +import { NativeHidParserService, NativeGamepadType } from './NativeHidParserService'; +import { HidReportParserUtil } from './HidReportParserUtil'; import { createUsbError, UsbError } from './UsbErrorCodes'; const TAG = '[USB-NativeHID]'; @@ -478,11 +478,10 @@ export class NativeHidController extends AbstractController { } /** - * 使用原生层处理 HID 报告 + * 使用原生层处理 HID 报告(解析逻辑在 HidReportParserUtil,与 HID DDK 通道共享) */ private processInputReport(data: Uint8Array): void { - // 调用原生解析器,传入强制协议类型 - const state = this.nativeParser.parseHidReport( + const state = HidReportParserUtil.parseToState( this.vendorId, this.productId, data, @@ -490,175 +489,20 @@ export class NativeHidController extends AbstractController { ); if (!state) { - // 原生解析失败,使用 TypeScript 后备解析器 - const hexStr = Array.from(data.slice(0, Math.min(data.length, 16))) - .map(b => b.toString(16).padStart(2, '0')).join(' '); - this.fallbackParseHidReport(data, hexStr); return; } - // 转换原生按钮标志到 Moonlight 格式 - this.buttonFlags = this.convertNativeButtons(state.buttons); + this.buttonFlags = state.buttonFlags; + this.leftStickX = state.leftStickX; + this.leftStickY = state.leftStickY; + this.rightStickX = state.rightStickX; + this.rightStickY = state.rightStickY; + this.leftTrigger = state.leftTrigger; + this.rightTrigger = state.rightTrigger; - // 转换摇杆值(原生层返回 -32768 到 32767,需要转换为 -1.0 到 1.0) - this.leftStickX = state.leftStickX / AXIS_MAX; - this.leftStickY = state.leftStickY / AXIS_MAX; - this.rightStickX = state.rightStickX / AXIS_MAX; - this.rightStickY = state.rightStickY / AXIS_MAX; - - // 转换扣机值(原生层返回 0-255,需要转换为 0.0 到 1.0) - this.leftTrigger = state.leftTrigger / TRIGGER_MAX; - this.rightTrigger = state.rightTrigger / TRIGGER_MAX; - - // 报告输入 this.reportInput(); } - /** - * TypeScript 后备 HID 解析器 - * 用于原生解析器不可用时的通用 HID 手柄解析 - * - * 通用手柄格式 (10字节): - * [0] = Report ID (通常是 0x01) - * [1] = Left Stick X (0x00-0xFF, 0x80 = center) - * [2] = Left Stick Y (0x00-0xFF, 0x80 = center) - * [3] = Right Stick X (0x00-0xFF, 0x80 = center) - * [4] = Right Stick Y (0x00-0xFF, 0x80 = center) - * [5] = Triggers/misc (varies) - * [6] = HAT/D-Pad (0x0F = center, 0-7 = directions) - * [7] = Buttons byte 1 (function buttons: Start, Select, LS, RS, etc.) - * [8] = Buttons byte 2 (face buttons: A, B, X, Y, LB, RB) - * [9] = Reserved - */ - private fallbackParseHidReport(data: Uint8Array, hexStr: string): void { - if (data.length < 9) { - console.warn(`${TAG} HID 数据太短: ${data.length}`); - return; - } - - // 解析摇杆 (字节 1-4, 0x00-0xFF, 0x80=中心) - // 转换为 -1.0 到 1.0 - this.leftStickX = (data[1] - 128) / 127.0; - this.leftStickY = (data[2] - 128) / 127.0; - this.rightStickX = (data[3] - 128) / 127.0; - this.rightStickY = (data[4] - 128) / 127.0; - - // 限制范围 - this.leftStickX = Math.max(-1.0, Math.min(1.0, this.leftStickX)); - this.leftStickY = Math.max(-1.0, Math.min(1.0, this.leftStickY)); - this.rightStickX = Math.max(-1.0, Math.min(1.0, this.rightStickX)); - this.rightStickY = Math.max(-1.0, Math.min(1.0, this.rightStickY)); - - // 解析 HAT/D-Pad (字节 6) - // HAT 值: 0=Up, 1=UpRight, 2=Right, 3=DownRight, 4=Down, 5=DownLeft, 6=Left, 7=UpLeft, 0x0F=Center - let flags = 0; - const hat = data[6] & 0x0F; - switch (hat) { - case 0: // Up - flags |= ButtonFlags.UP_FLAG; - break; - case 1: // Up-Right - flags |= ButtonFlags.UP_FLAG | ButtonFlags.RIGHT_FLAG; - break; - case 2: // Right - flags |= ButtonFlags.RIGHT_FLAG; - break; - case 3: // Down-Right - flags |= ButtonFlags.DOWN_FLAG | ButtonFlags.RIGHT_FLAG; - break; - case 4: // Down - flags |= ButtonFlags.DOWN_FLAG; - break; - case 5: // Down-Left - flags |= ButtonFlags.DOWN_FLAG | ButtonFlags.LEFT_FLAG; - break; - case 6: // Left - flags |= ButtonFlags.LEFT_FLAG; - break; - case 7: // Up-Left - flags |= ButtonFlags.UP_FLAG | ButtonFlags.LEFT_FLAG; - break; - default: // 0x0F = center, no direction - break; - } - - // 解析按钮字节 - const btn1 = data[7]; // 功能按钮 - const btn2 = data[8]; // 面板按钮 - - // VID 0x413D 手柄的按钮映射 (通用 HID 手柄格式): - // 字节 7 (功能按钮): - // bit 0 = Select/Back - // bit 1 = Start - // bit 2 = Home/Guide - // bit 3 = Left Stick Click - // bit 4 = Right Stick Click - // bit 5 = LB - // bit 6 = RB - // bit 7 = LT (digital) - // - // 字节 8 (面板按钮): - // bit 0 = A - // bit 1 = B - // bit 2 = X - // bit 3 = Y - - // 功能按钮 (字节 7) - if (btn1 & 0x01) flags |= ButtonFlags.BACK_FLAG; // Select/Back - if (btn1 & 0x02) flags |= ButtonFlags.PLAY_FLAG; // Start - if (btn1 & 0x04) flags |= ButtonFlags.SPECIAL_BUTTON_FLAG; // Home/Guide - if (btn1 & 0x08) flags |= ButtonFlags.LS_CLK_FLAG; // Left Stick Click - if (btn1 & 0x10) flags |= ButtonFlags.RS_CLK_FLAG; // Right Stick Click - if (btn1 & 0x20) flags |= ButtonFlags.LB_FLAG; // LB - if (btn1 & 0x40) flags |= ButtonFlags.RB_FLAG; // RB - - // 面板按钮 (字节 8) - if (btn2 & 0x01) flags |= ButtonFlags.A_FLAG; // A - if (btn2 & 0x02) flags |= ButtonFlags.B_FLAG; // B - if (btn2 & 0x04) flags |= ButtonFlags.X_FLAG; // X - if (btn2 & 0x08) flags |= ButtonFlags.Y_FLAG; // Y - - // 更新按钮标志 - this.buttonFlags = flags; - - // 报告输入 - this.reportInput(); - } - - /** - * 转换原生按钮标志到 Moonlight 格式 - */ - private convertNativeButtons(nativeButtons: number): number { - let flags = 0; - - // 方向键 - if (nativeButtons & NativeButtonFlags.UP) flags |= ButtonFlags.UP_FLAG; - if (nativeButtons & NativeButtonFlags.DOWN) flags |= ButtonFlags.DOWN_FLAG; - if (nativeButtons & NativeButtonFlags.LEFT) flags |= ButtonFlags.LEFT_FLAG; - if (nativeButtons & NativeButtonFlags.RIGHT) flags |= ButtonFlags.RIGHT_FLAG; - - // 功能键 - if (nativeButtons & NativeButtonFlags.START) flags |= ButtonFlags.PLAY_FLAG; - if (nativeButtons & NativeButtonFlags.BACK) flags |= ButtonFlags.BACK_FLAG; - if (nativeButtons & NativeButtonFlags.LS_CLK) flags |= ButtonFlags.LS_CLK_FLAG; - if (nativeButtons & NativeButtonFlags.RS_CLK) flags |= ButtonFlags.RS_CLK_FLAG; - - // 肩键 - if (nativeButtons & NativeButtonFlags.LB) flags |= ButtonFlags.LB_FLAG; - if (nativeButtons & NativeButtonFlags.RB) flags |= ButtonFlags.RB_FLAG; - - // 面板按钮 - if (nativeButtons & NativeButtonFlags.A) flags |= ButtonFlags.A_FLAG; - if (nativeButtons & NativeButtonFlags.B) flags |= ButtonFlags.B_FLAG; - if (nativeButtons & NativeButtonFlags.X) flags |= ButtonFlags.X_FLAG; - if (nativeButtons & NativeButtonFlags.Y) flags |= ButtonFlags.Y_FLAG; - - // 特殊按钮 - if (nativeButtons & NativeButtonFlags.HOME) flags |= ButtonFlags.SPECIAL_BUTTON_FLAG; - - return flags; - } - rumble(lowFreqMotor: number, highFreqMotor: number): void { if (this.outputEndpoint === 0) { return; diff --git a/entry/src/main/ets/service/usbdriver/UsbDriverService.ets b/entry/src/main/ets/service/usbdriver/UsbDriverService.ets index d3403ddc..111525f0 100644 --- a/entry/src/main/ets/service/usbdriver/UsbDriverService.ets +++ b/entry/src/main/ets/service/usbdriver/UsbDriverService.ets @@ -26,6 +26,8 @@ import { Dualshock4Controller } from './Dualshock4Controller'; import { DualSenseController } from './DualSenseController'; import { SwitchProController } from './SwitchProController'; import { NativeHidController } from './NativeHidController'; +import { HidDdkController } from './HidDdkController'; +import { HidDdkReader } from './HidDdkReader'; import { SettingsService } from '../../service/SettingsService'; import nativeLib from 'libmoonlight_nativelib.so'; @@ -97,6 +99,8 @@ export class UsbDriverService implements UsbDriverListener { private pendingResetWait: Promise | null = null; // Device keys awaiting kernel HID rebind after a controller release. private pendingResetDeviceKeys: Set = new Set(); + // HID DDK 通道接管的设备:从未 detach 内核 HID 驱动,停止时不得进入重绑队列 + private hidDdkDeviceKeys: Set = new Set(); private constructor() { console.info(`${TAG} 创建 USB 驱动服务实例`); @@ -143,7 +147,7 @@ export class UsbDriverService implements UsbDriverListener { */ setControllerProtocolType(controllerId: number, protocolType: number): boolean { const controller = this.controllers.find(c => c.getControllerId() === controllerId); - if (controller && controller instanceof NativeHidController) { + if (controller && (controller instanceof NativeHidController || controller instanceof HidDdkController)) { controller.setForceProtocolType(protocolType); return true; } @@ -447,8 +451,12 @@ export class UsbDriverService implements UsbDriverListener { // 取消订阅 USB 事件 this.unsubscribeUsbEvents(); - // 保存已处理设备的键值,用于后续内核驱动重绑定 + // 保存已处理设备的键值,用于后续内核驱动重绑定。 + // HID DDK 通道设备从未独占接口,排除出重绑队列。 const releasedDeviceKeys = new Set(this.processedDevices); + this.hidDdkDeviceKeys.forEach((deviceKey: string): void => { + releasedDeviceKeys.delete(deviceKey); + }); releasedDeviceKeys.forEach((deviceKey: string): void => { this.pendingResetDeviceKeys.add(deviceKey); }); @@ -743,7 +751,31 @@ export class UsbDriverService implements UsbDriverListener { if (!this.isLifecycleActive(requestGeneration)) { return; } - + + // HID DDK 通道(实验):不 open pipe、不 claim,直接走内核 hidraw;失败回退下方旧链路。 + // 仅在强制 USB 驱动接管模式下启用——混合模式下 GCK 负责输入,USB 通道输入会被忽略, + // 且避免两条输入路径同时处理同一手柄。 + if (inputSettings.hidDdkInputChannel && inputSettings.forceUsbDriverOnly && + HidDdkReader.isAvailable()) { + const hidController = new HidDdkController(device, this.nextDeviceId++, this); + hidController.setDeviceLocation(device.busNum, device.devAddress, device.serial || '', device.name || ''); + + this.processedDevices.add(deviceKey); + this.controllers.push(hidController); + + if (hidController.start()) { + this.pendingResetDeviceKeys.delete(deviceKey); + this.hidDdkDeviceKeys.add(deviceKey); + console.info(`${TAG} HID DDK 通道接管设备: ${device.name}, Key=${deviceKey}`); + return; + } + + // 同步启动失败(权限/deviceId/打开失败),清理后回退旧链路 + this.controllers.pop(); + this.processedDevices.delete(deviceKey); + console.warn(`${TAG} HID DDK 通道启动失败,回退 USB 驱动链路: ${device.name}`); + } + // 打开设备 let pipe: usbManager.USBDevicePipe; try { diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index 97aa84ba..9e39d171 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -118,6 +118,14 @@ "when": "inuse" } }, + { + "name": "ohos.permission.ACCESS_DDK_HID", + "reason": "$string:permission_hid_ddk_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, { "name": "ohos.permission.READ_PASTEBOARD", "reason": "$string:permission_read_pasteboard_reason", diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index fcfea8d1..b2eea21e 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -124,6 +124,10 @@ "name": "permission_usb_ddk_reason", "value": "用于通过原生接口直接访问USB手柄设备,实现高速轮询" }, + { + "name": "permission_hid_ddk_reason", + "value": "用于通过HID协议读取手柄原始报文与报告描述符,提升手柄兼容性" + }, { "name": "permission_read_pasteboard_reason", "value": "用于读取本地剪贴板并与 Sunshine 主机同步" diff --git a/entry/src/main/resources/rawfile/CHANGELOG.md b/entry/src/main/resources/rawfile/CHANGELOG.md index 2b53933d..6981857b 100644 --- a/entry/src/main/resources/rawfile/CHANGELOG.md +++ b/entry/src/main/resources/rawfile/CHANGELOG.md @@ -50,6 +50,27 @@ - 🔧 clear stale targets on PTS reanchor --> +## [未发布] +HID DDK 输入通道(实验性) + +### 新增 +- 实验性 HID DDK 输入通道:通过内核 HID 接口(hidraw)直读 USB 手柄原始报文,免 USB 接口声明与内核驱动重绑定,读取为内核中断驱动;设置路径:输入 → USB 驱动 → HID DDK 输入通道(实验),需配合"强制 USB 驱动接管输入"使用,不可用或启动失败时自动回退原有通道。 +- 手柄测试页(USB 驱动模式)显示每台手柄的传输通道标识与 HID DDK 通道实时状态(接口/描述符/报文率/最近错误)。 + +## [1.0.0.808] - 2026-08-23 +网络连接诊断、HDR 排障与振动回退优化 + +### 新增 +- 增强 HDR 诊断与亮度问题排查信息。 + +### 优化 +- 统一主机连接策略,提升多地址及本地、远程网络切换时的连接稳定性(#118)。 +- 精简设置页与串流菜单中的重复选项,统一陀螺仪辅助等功能入口(#120)。 + +### 修复 +- 区分主机可达状态与已认证连接,避免错误连接路径影响主机发现、配对和串流(#116)。 +- 修复 USB 手柄振动输出不可用时,机身与手柄振动自动回退异常的问题(#119)。 + ## [1.0.0.807] - 2026-08-18 HDR 亮度上报优化 diff --git a/hvigorw b/hvigorw index 5670889a..7be122b0 100644 --- a/hvigorw +++ b/hvigorw @@ -15,6 +15,15 @@ export NODE_OPTIONS="${NODE_OPTIONS:---max_old_space_size=4096}" # Run hvigor with all passed arguments cd "$PROJECT_DIR" +# 0. Windows + DevEco Studio: delegate to its bundled hvigor wrapper so the +# IDE's hvigor/plugin pair stays one consistent instance. Paths only exist +# on a DevEco install, so CI/Linux falls through untouched. +DEVECO_NODE="/c/Program Files/Huawei/DevEco Studio/tools/node/node.exe" +DEVECO_HVIGORW="/c/Program Files/Huawei/DevEco Studio/tools/hvigor/bin/hvigorw.js" +if [ -f "$DEVECO_NODE" ] && [ -f "$DEVECO_HVIGORW" ]; then + exec "$DEVECO_NODE" "$DEVECO_HVIGORW" "$@" +fi + # 1. Check if hvigor is in PATH (CI environment with global install) if command -v hvigor &> /dev/null; then exec hvigor "$@" diff --git a/hvigorw.js b/hvigorw.js index 2e5a6346..36d6fe62 100644 --- a/hvigorw.js +++ b/hvigorw.js @@ -21,6 +21,23 @@ const args = process.argv.slice(2); * Find and execute hvigor */ function findAndRunHvigor() { + // 0. Windows + DevEco Studio: delegate to its bundled hvigor wrapper. + // Runs in a separate process so the IDE's hvigor/plugin pair stays a + // single consistent instance (project-local copies would split it). + if (process.platform === 'win32') { + const devecoHvigorw = 'C:\\Program Files\\Huawei\\DevEco Studio\\tools\\hvigor\\bin\\hvigorw.js'; + const devecoNode = 'C:\\Program Files\\Huawei\\DevEco Studio\\tools\\node\\node.exe'; + if (fs.existsSync(devecoHvigorw) && fs.existsSync(devecoNode)) { + const child = spawn(devecoNode, [devecoHvigorw, ...args], { stdio: 'inherit', windowsHide: true }); + child.on('exit', (code) => process.exit(code ?? 1)); + child.on('error', (error) => { + console.error(`Error: Failed to start DevEco hvigor: ${error.message}`); + process.exit(1); + }); + return; + } + } + // 1. Check hvigor/node_modules const hvigorNodeModules = path.join(PROJECT_DIR, 'hvigor', 'node_modules', '@ohos', 'hvigor', 'bin', 'hvigor.js'); if (fs.existsSync(hvigorNodeModules)) { diff --git a/nativelib/src/main/cpp/CMakeLists.txt b/nativelib/src/main/cpp/CMakeLists.txt index 93426984..6adcbfb7 100644 --- a/nativelib/src/main/cpp/CMakeLists.txt +++ b/nativelib/src/main/cpp/CMakeLists.txt @@ -98,6 +98,7 @@ set(SOURCE_FILES mouse_interceptor.cpp usb_helper.cpp usb_ddk_poller.cpp + hid_ddk_probe.cpp native_render.cpp presentation_scheduler.cpp two_step_presentation_scheduler.cpp diff --git a/nativelib/src/main/cpp/hid_ddk_probe.cpp b/nativelib/src/main/cpp/hid_ddk_probe.cpp new file mode 100644 index 00000000..b22d230b --- /dev/null +++ b/nativelib/src/main/cpp/hid_ddk_probe.cpp @@ -0,0 +1,1062 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2024-2025 Moonlight/AlkaidLab + * + * HID DDK Probe - 见 hid_ddk_probe.h + * + * 不直接链接 libhid.z.so(避免对 API 18 SDK 的硬依赖), + * 通过 dlopen + dlsym 调用,类型定义与官方 保持一致。 + */ + +#include "hid_ddk_probe.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_TAG "HID-DDK-Probe" + +// ============================================================ +// HID DDK 类型与常量 (官方 hid_ddk_types.h 布局, ABI 自 18 起稳定) +// ============================================================ + +struct Hid_DeviceHandle; // 不透明句柄 + +typedef struct Hid_RawDevInfo { + uint32_t busType; + uint16_t vendor; + uint16_t product; +} Hid_RawDevInfo; + +#define HID_DDK_SUCCESS 0 +#define HID_DDK_NO_PERM 201 +#define HID_DDK_INVALID_PARAMETER 401 +#define HID_DDK_FAILURE 27300001 +#define HID_DDK_NULL_PTR 27300002 +#define HID_DDK_INVALID_OPERATION 27300003 +#define HID_DDK_TIMEOUT 27300004 +#define HID_DDK_INIT_ERROR 27300005 +#define HID_DDK_SERVICE_ERROR 27300006 +#define HID_DDK_MEMORY_ERROR 27300007 +#define HID_DDK_IO_ERROR 27300008 +#define HID_DDK_DEVICE_NOT_FOUND 27300009 + +#define PROBE_DESC_MAX 1024 +#define PROBE_SAMPLE_COUNT 3 +#define PROBE_SAMPLE_MAX 64 +#define PROBE_IFACE_MAX 4 + +typedef int32_t (*Fn_OH_Hid_Init)(void); +typedef int32_t (*Fn_OH_Hid_Release)(void); +typedef int32_t (*Fn_OH_Hid_Open)(uint64_t deviceId, uint8_t interfaceIndex, Hid_DeviceHandle **dev); +typedef int32_t (*Fn_OH_Hid_Close)(Hid_DeviceHandle **dev); +typedef int32_t (*Fn_OH_Hid_GetRawInfo)(Hid_DeviceHandle *dev, Hid_RawDevInfo *rawDevInfo); +typedef int32_t (*Fn_OH_Hid_GetRawName)(Hid_DeviceHandle *dev, char *data, uint32_t bufSize); +typedef int32_t (*Fn_OH_Hid_GetReportDescriptor)(Hid_DeviceHandle *dev, uint8_t *buf, uint32_t bufSize, uint32_t *bytesRead); +typedef int32_t (*Fn_OH_Hid_ReadTimeout)(Hid_DeviceHandle *dev, uint8_t *data, uint32_t bufSize, int timeout, uint32_t *bytesRead); +typedef int32_t (*Fn_OH_Hid_Write)(Hid_DeviceHandle *dev, uint8_t *data, uint32_t length, uint32_t *bytesWritten); + +static void *g_hidLib = nullptr; +static bool g_hidLoaded = false; +static Fn_OH_Hid_Init fn_HidInit = nullptr; +static Fn_OH_Hid_Release fn_HidRelease = nullptr; +static Fn_OH_Hid_Open fn_HidOpen = nullptr; +static Fn_OH_Hid_Close fn_HidClose = nullptr; +static Fn_OH_Hid_GetRawInfo fn_HidGetRawInfo = nullptr; +static Fn_OH_Hid_GetRawName fn_HidGetRawName = nullptr; +static Fn_OH_Hid_GetReportDescriptor fn_HidGetDesc = nullptr; +static Fn_OH_Hid_ReadTimeout fn_HidReadTimeout = nullptr; +static Fn_OH_Hid_Write fn_HidWrite = nullptr; + +static const char *hidErrStr(int32_t code) { + switch (code) { + case HID_DDK_SUCCESS: return "SUCCESS"; + case HID_DDK_NO_PERM: return "NO_PERM"; + case HID_DDK_INVALID_PARAMETER: return "INVALID_PARAMETER"; + case HID_DDK_FAILURE: return "FAILURE"; + case HID_DDK_NULL_PTR: return "NULL_PTR"; + case HID_DDK_INVALID_OPERATION: return "INVALID_OPERATION"; + case HID_DDK_TIMEOUT: return "TIMEOUT"; + case HID_DDK_INIT_ERROR: return "INIT_ERROR"; + case HID_DDK_SERVICE_ERROR: return "SERVICE_ERROR"; + case HID_DDK_MEMORY_ERROR: return "MEMORY_ERROR"; + case HID_DDK_IO_ERROR: return "IO_ERROR"; + case HID_DDK_DEVICE_NOT_FOUND: return "DEVICE_NOT_FOUND"; + default: return "UNKNOWN"; + } +} + +static bool loadHidLibrary() { + if (g_hidLoaded) return true; + + g_hidLib = dlopen("libhid.z.so", RTLD_LAZY); + if (!g_hidLib) { + OH_LOG_ERROR(LOG_APP, "[%{public}s] dlopen libhid.z.so 失败: %{public}s", LOG_TAG, dlerror()); + return false; + } + + fn_HidInit = (Fn_OH_Hid_Init)dlsym(g_hidLib, "OH_Hid_Init"); + fn_HidRelease = (Fn_OH_Hid_Release)dlsym(g_hidLib, "OH_Hid_Release"); + fn_HidOpen = (Fn_OH_Hid_Open)dlsym(g_hidLib, "OH_Hid_Open"); + fn_HidClose = (Fn_OH_Hid_Close)dlsym(g_hidLib, "OH_Hid_Close"); + fn_HidGetRawInfo = (Fn_OH_Hid_GetRawInfo)dlsym(g_hidLib, "OH_Hid_GetRawInfo"); + fn_HidGetRawName = (Fn_OH_Hid_GetRawName)dlsym(g_hidLib, "OH_Hid_GetRawName"); + fn_HidGetDesc = (Fn_OH_Hid_GetReportDescriptor)dlsym(g_hidLib, "OH_Hid_GetReportDescriptor"); + fn_HidReadTimeout = (Fn_OH_Hid_ReadTimeout)dlsym(g_hidLib, "OH_Hid_ReadTimeout"); + fn_HidWrite = (Fn_OH_Hid_Write)dlsym(g_hidLib, "OH_Hid_Write"); + + if (!fn_HidInit || !fn_HidOpen || !fn_HidClose || !fn_HidReadTimeout) { + OH_LOG_ERROR(LOG_APP, "[%{public}s] 核心 OH_Hid_* 符号缺失 (系统低于 API 18?): " + "Init=%{public}s Open=%{public}s Close=%{public}s Read=%{public}s", + LOG_TAG, + fn_HidInit ? "OK" : "MISS", fn_HidOpen ? "OK" : "MISS", + fn_HidClose ? "OK" : "MISS", fn_HidReadTimeout ? "OK" : "MISS"); + return false; + } + + g_hidLoaded = true; + OH_LOG_INFO(LOG_APP, "[%{public}s] libhid.z.so 加载成功 (RawInfo=%{public}s RawName=%{public}s GetDesc=%{public}s Write=%{public}s)", + LOG_TAG, fn_HidGetRawInfo ? "OK" : "MISS", + fn_HidGetRawName ? "OK" : "MISS", fn_HidGetDesc ? "OK" : "MISS", + fn_HidWrite ? "OK" : "MISS"); + return true; +} + +// ============================================================ +// OH_Hid_Init/Release 引用计数(probe 与常驻 reader 共享, +// 避免探测结束时 Release 拆掉 reader 正在使用的 DDK 连接) +// ============================================================ + +static pthread_mutex_t g_hidInitRefMutex = PTHREAD_MUTEX_INITIALIZER; +static int g_hidInitRefCount = 0; + +static bool hidInitRef() { + pthread_mutex_lock(&g_hidInitRefMutex); + if (g_hidInitRefCount == 0) { + int32_t code = fn_HidInit(); + if (code != HID_DDK_SUCCESS) { + pthread_mutex_unlock(&g_hidInitRefMutex); + OH_LOG_ERROR(LOG_APP, "[%{public}s] OH_Hid_Init 失败: %{public}d (%{public}s)", + LOG_TAG, code, hidErrStr(code)); + return false; + } + OH_LOG_INFO(LOG_APP, "[%{public}s] OH_Hid_Init 成功 (引用计数 0→1)", LOG_TAG); + } + g_hidInitRefCount++; + pthread_mutex_unlock(&g_hidInitRefMutex); + return true; +} + +static void hidReleaseRef() { + pthread_mutex_lock(&g_hidInitRefMutex); + if (g_hidInitRefCount > 0) { + g_hidInitRefCount--; + if (g_hidInitRefCount == 0) { + int32_t code = fn_HidRelease(); + OH_LOG_INFO(LOG_APP, "[%{public}s] OH_Hid_Release (引用计数→0): %{public}d", LOG_TAG, code); + } + } + pthread_mutex_unlock(&g_hidInitRefMutex); +} + +// ============================================================ +// 探测结果 (线程间传递, JS 线程构建返回对象后释放) +// ============================================================ + +struct HidProbeResult { + bool available; // 库与符号可用 + bool opened; // 至少一个 interface 打开成功 + int32_t initCode; + int32_t openCode; // 最后一次 Open 的返回码 + int32_t descCode; // GetReportDescriptor 返回码 + uint8_t openIface; + uint16_t vid; + uint16_t pid; + uint32_t busType; + char name[128]; + uint32_t descLen; + uint8_t desc[PROBE_DESC_MAX]; + uint64_t reportCount; + double reportsPerSec; + uint32_t sampleCount; + uint8_t samples[PROBE_SAMPLE_COUNT][PROBE_SAMPLE_MAX]; + uint32_t sampleLens[PROBE_SAMPLE_COUNT]; +}; + +static void probeResultToJs(napi_env env, napi_value result, const HidProbeResult *r) { + auto setBool = [&](const char *name, bool val) { + napi_value v; napi_get_boolean(env, val, &v); + napi_set_named_property(env, result, name, v); + }; + auto setInt = [&](const char *name, int64_t val) { + napi_value v; napi_create_int64(env, val, &v); + napi_set_named_property(env, result, name, v); + }; + auto setDouble = [&](const char *name, double val) { + napi_value v; napi_create_double(env, val, &v); + napi_set_named_property(env, result, name, v); + }; + auto setStr = [&](const char *name, const char *val) { + napi_value v; napi_create_string_utf8(env, val, NAPI_AUTO_LENGTH, &v); + napi_set_named_property(env, result, name, v); + }; + + setBool("available", r->available); + setBool("opened", r->opened); + setInt("initCode", r->initCode); + setInt("openCode", r->openCode); + setInt("descCode", r->descCode); + setStr("initError", r->initCode == HID_DDK_SUCCESS ? "" : hidErrStr(r->initCode)); + setStr("openError", r->openCode == HID_DDK_SUCCESS ? "" : hidErrStr(r->openCode)); + + if (r->opened) { + setInt("interfaceIndex", r->openIface); + setInt("vid", r->vid); + setInt("pid", r->pid); + setInt("busType", r->busType); + setStr("name", r->name); + setInt("descriptorLength", r->descLen); + setInt("reportCount", (int64_t)r->reportCount); + setDouble("reportsPerSec", r->reportsPerSec); + + // 描述符 Uint8Array + if (r->descLen > 0) { + void *bufData = nullptr; + napi_value arrayBuffer; + if (napi_create_arraybuffer(env, r->descLen, &bufData, &arrayBuffer) == napi_ok && bufData) { + memcpy(bufData, r->desc, r->descLen); + napi_value desc; + if (napi_create_typedarray(env, napi_uint8_array, r->descLen, arrayBuffer, 0, &desc) == napi_ok) { + napi_set_named_property(env, result, "descriptor", desc); + } + } + } + + // 样例报文 + napi_value samples; + napi_create_array(env, &samples); + for (uint32_t i = 0; i < r->sampleCount && i < PROBE_SAMPLE_COUNT; i++) { + void *bufData = nullptr; + napi_value arrayBuffer; + if (napi_create_arraybuffer(env, r->sampleLens[i], &bufData, &arrayBuffer) == napi_ok && bufData) { + memcpy(bufData, r->samples[i], r->sampleLens[i]); + napi_value sample; + if (napi_create_typedarray(env, napi_uint8_array, r->sampleLens[i], arrayBuffer, 0, &sample) == napi_ok) { + napi_set_element(env, samples, i, sample); + } + } + } + napi_set_named_property(env, result, "sampleReports", samples); + } +} + +static void probeResultOnJs(napi_env env, napi_value js_callback, void *context, void *rawData) { + HidProbeResult *r = (HidProbeResult *)rawData; + if (!env || !js_callback || !r) { + free(r); + return; + } + napi_value result; + napi_create_object(env, &result); + probeResultToJs(env, result, r); + napi_value undefined; + napi_get_undefined(env, &undefined); + napi_call_function(env, undefined, js_callback, 1, &result, nullptr); + free(r); +} + +// ============================================================ +// 探测线程 +// ============================================================ + +struct ProbeThreadArgs { + uint64_t deviceId; + uint32_t readMs; + napi_threadsafe_function tsfn; +}; + +static void *probeThread(void *arg) { + ProbeThreadArgs *args = (ProbeThreadArgs *)arg; + uint64_t deviceId = args->deviceId; + uint32_t readMs = args->readMs; + napi_threadsafe_function tsfn = args->tsfn; + free(args); + + HidProbeResult *r = (HidProbeResult *)calloc(1, sizeof(HidProbeResult)); + r->available = true; + + OH_LOG_INFO(LOG_APP, "[%{public}s] 开始探测: deviceId=%{public}llu readMs=%{public}u", + LOG_TAG, (unsigned long long)deviceId, readMs); + + // Step 1: Init — 权限与服务的第一个信号点(与常驻 reader 共享引用计数) + if (!hidInitRef()) { + r->initCode = HID_DDK_INIT_ERROR; + napi_call_threadsafe_function(tsfn, r, napi_tsfn_nonblocking); + napi_release_threadsafe_function(tsfn, napi_tsfn_release); + return nullptr; + } + r->initCode = HID_DDK_SUCCESS; + OH_LOG_INFO(LOG_APP, "[%{public}s] OH_Hid_Init: %{public}d (%{public}s)", + LOG_TAG, r->initCode, hidErrStr(r->initCode)); + + // Step 2: 依次尝试 interface 0..4 打开 + Hid_DeviceHandle *dev = nullptr; + for (uint8_t iface = 0; iface <= PROBE_IFACE_MAX; iface++) { + int32_t code = fn_HidOpen(deviceId, iface, &dev); + OH_LOG_INFO(LOG_APP, "[%{public}s] OH_Hid_Open(iface=%{public}u): %{public}d (%{public}s)", + LOG_TAG, iface, code, hidErrStr(code)); + if (code == HID_DDK_SUCCESS && dev) { + r->opened = true; + r->openCode = HID_DDK_SUCCESS; + r->openIface = iface; + break; + } + r->openCode = code; + dev = nullptr; + if (code != HID_DDK_DEVICE_NOT_FOUND && code != HID_DDK_INVALID_PARAMETER) { + // 权限/服务级错误,换接口也不会成功 + break; + } + } + + if (r->opened && dev) { + // Step 3: 设备信息 + if (fn_HidGetRawInfo) { + Hid_RawDevInfo info; + memset(&info, 0, sizeof(info)); + int32_t code = fn_HidGetRawInfo(dev, &info); + if (code == HID_DDK_SUCCESS) { + r->busType = info.busType; + r->vid = info.vendor; + r->pid = info.product; + OH_LOG_INFO(LOG_APP, "[%{public}s] RawInfo: bus=%{public}u VID=0x%{public}x PID=0x%{public}x", + LOG_TAG, info.busType, info.vendor, info.product); + } else { + OH_LOG_WARN(LOG_APP, "[%{public}s] GetRawInfo: %{public}d (%{public}s)", + LOG_TAG, code, hidErrStr(code)); + } + } + if (fn_HidGetRawName) { + char name[128] = {0}; + int32_t code = fn_HidGetRawName(dev, name, sizeof(name) - 1); + if (code == HID_DDK_SUCCESS) { + strncpy(r->name, name, sizeof(r->name) - 1); + OH_LOG_INFO(LOG_APP, "[%{public}s] RawName: %{public}s", LOG_TAG, name); + } else { + OH_LOG_WARN(LOG_APP, "[%{public}s] GetRawName: %{public}d (%{public}s)", + LOG_TAG, code, hidErrStr(code)); + } + } + + // Step 4: 报告描述符 + if (fn_HidGetDesc) { + uint32_t descRead = 0; + int32_t code = fn_HidGetDesc(dev, r->desc, PROBE_DESC_MAX, &descRead); + r->descCode = code; + r->descLen = descRead; + if (code == HID_DDK_SUCCESS && descRead > 0) { + OH_LOG_INFO(LOG_APP, "[%{public}s] ReportDescriptor: %{public}u 字节", LOG_TAG, descRead); + // 打印前 96 字节 hex + char hex[97 * 3] = {0}; + uint32_t dumpLen = descRead < 96 ? descRead : 96; + for (uint32_t i = 0; i < dumpLen; i++) { + snprintf(hex + i * 3, 4, "%02x ", r->desc[i]); + } + OH_LOG_INFO(LOG_APP, "[%{public}s] 描述符前%{public}u字节: %{public}s", LOG_TAG, dumpLen, hex); + } else { + OH_LOG_WARN(LOG_APP, "[%{public}s] GetReportDescriptor: %{public}d (%{public}s)", + LOG_TAG, code, hidErrStr(code)); + } + } + + // Step 5: 限时读输入报文 + uint64_t startMs = 0, nowMs = 0; + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + startMs = ts.tv_sec * 1000ULL + ts.tv_nsec / 1000000ULL; + + uint8_t buf[PROBE_SAMPLE_MAX]; + while (true) { + uint32_t bytesRead = 0; + int32_t code = fn_HidReadTimeout(dev, buf, sizeof(buf), 300, &bytesRead); + if (code == HID_DDK_SUCCESS && bytesRead > 0) { + r->reportCount++; + if (r->sampleCount < PROBE_SAMPLE_COUNT) { + uint32_t len = bytesRead > PROBE_SAMPLE_MAX ? PROBE_SAMPLE_MAX : bytesRead; + memcpy(r->samples[r->sampleCount], buf, len); + r->sampleLens[r->sampleCount] = len; + char hex[PROBE_SAMPLE_MAX * 3 + 1] = {0}; + for (uint32_t i = 0; i < len; i++) { + snprintf(hex + i * 3, 4, "%02x ", buf[i]); + } + OH_LOG_INFO(LOG_APP, "[%{public}s] 报文样例#%{public}u (%{public}u字节): %{public}s", + LOG_TAG, r->sampleCount + 1, len, hex); + r->sampleCount++; + } + } else if (code != HID_DDK_TIMEOUT) { + OH_LOG_WARN(LOG_APP, "[%{public}s] ReadTimeout: %{public}d (%{public}s)", + LOG_TAG, code, hidErrStr(code)); + if (code == HID_DDK_NO_PERM || code == HID_DDK_INIT_ERROR || code == HID_DDK_SERVICE_ERROR) { + break; + } + } + + clock_gettime(CLOCK_MONOTONIC, &ts); + nowMs = ts.tv_sec * 1000ULL + ts.tv_nsec / 1000000ULL; + if (nowMs - startMs >= readMs) break; + } + + if (r->reportCount > 0 && nowMs > startMs) { + r->reportsPerSec = (double)r->reportCount * 1000.0 / (double)(nowMs - startMs); + } + OH_LOG_INFO(LOG_APP, "[%{public}s] 读取完成: %{public}llu 报文 / %{public}u ms ≈ %{public}.1f Hz", + LOG_TAG, (unsigned long long)r->reportCount, readMs, r->reportsPerSec); + + // Step 6: 关闭 + int32_t closeCode = fn_HidClose(&dev); + OH_LOG_INFO(LOG_APP, "[%{public}s] OH_Hid_Close: %{public}d (%{public}s)", + LOG_TAG, closeCode, hidErrStr(closeCode)); + } + + // 释放本探测持有的引用(引用计数归零时才真正 Release, + // 避免拆掉并发运行的常驻 reader 正在使用的 DDK 连接) + hidReleaseRef(); + OH_LOG_INFO(LOG_APP, "[%{public}s] 探测 Release 完成", LOG_TAG); + + OH_LOG_INFO(LOG_APP, "[%{public}s] 探测结束: opened=%{public}d iface=%{public}u desc=%{public}uB reports=%{public}llu", + LOG_TAG, (int)r->opened, r->openIface, r->descLen, (unsigned long long)r->reportCount); + + napi_call_threadsafe_function(tsfn, r, napi_tsfn_nonblocking); + napi_release_threadsafe_function(tsfn, napi_tsfn_release); + return nullptr; +} + +// ============================================================ +// NAPI +// ============================================================ + +static napi_value HidProbe_IsAvailable(napi_env env, napi_callback_info info) { + (void)info; + bool ok = loadHidLibrary(); + napi_value result; + napi_get_boolean(env, ok, &result); + return result; +} + +static napi_value HidProbe_Probe(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + if (argc < 3) { + napi_value undefined; + napi_get_undefined(env, &undefined); + return undefined; + } + + int64_t deviceId64 = 0; + int32_t readMs = 2000; + napi_get_value_int64(env, args[0], &deviceId64); + napi_get_value_int32(env, args[1], &readMs); + if (readMs <= 0 || readMs > 10000) readMs = 2000; + + napi_value resName; + napi_create_string_utf8(env, "HidDdkProbeResult", NAPI_AUTO_LENGTH, &resName); + napi_threadsafe_function tsfn; + napi_status status = napi_create_threadsafe_function( + env, args[2], nullptr, resName, 2, 1, nullptr, nullptr, nullptr, + probeResultOnJs, &tsfn + ); + if (status != napi_ok) { + napi_value undefined; + napi_get_undefined(env, &undefined); + return undefined; + } + + if (!loadHidLibrary()) { + // 库不可用 → 同样走回调,保持单一结果通道 + HidProbeResult *r = (HidProbeResult *)calloc(1, sizeof(HidProbeResult)); + r->available = false; + r->initCode = HID_DDK_INIT_ERROR; + napi_call_threadsafe_function(tsfn, r, napi_tsfn_nonblocking); + napi_release_threadsafe_function(tsfn, napi_tsfn_release); + napi_value undefined; + napi_get_undefined(env, &undefined); + return undefined; + } + + ProbeThreadArgs *targs = (ProbeThreadArgs *)malloc(sizeof(ProbeThreadArgs)); + targs->deviceId = (uint64_t)deviceId64; + targs->readMs = (uint32_t)readMs; + targs->tsfn = tsfn; + + pthread_t thread; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + if (pthread_create(&thread, &attr, probeThread, targs) != 0) { + free(targs); + HidProbeResult *r = (HidProbeResult *)calloc(1, sizeof(HidProbeResult)); + r->available = true; + r->initCode = HID_DDK_FAILURE; + napi_call_threadsafe_function(tsfn, r, napi_tsfn_nonblocking); + napi_release_threadsafe_function(tsfn, napi_tsfn_release); + } + pthread_attr_destroy(&attr); + + napi_value undefined; + napi_get_undefined(env, &undefined); + return undefined; +} + +void HidDdkProbe_Init(napi_env env, napi_value exports) { + napi_value obj; + napi_create_object(env, &obj); + + napi_property_descriptor methods[] = { + { "isAvailable", nullptr, HidProbe_IsAvailable, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "probe", nullptr, HidProbe_Probe, nullptr, nullptr, nullptr, napi_default, nullptr }, + }; + + napi_define_properties(env, obj, sizeof(methods) / sizeof(methods[0]), methods); + napi_set_named_property(env, exports, "HidDdkProbe", obj); + + OH_LOG_INFO(LOG_APP, "[%{public}s] HID DDK Probe NAPI 已注册", LOG_TAG); +} + +// ============================================================ +// 常驻 Reader - HID DDK 输入通道(供 HidDdkController 使用) +// +// 与 UsbDdkPoller 的区别:走内核 hidraw 通道(OH_Hid_*), +// 无需 claim USB 接口、无需内核驱动重绑定,读为阻塞事件驱动。 +// ============================================================ + +#define HID_READER_MAX 4 +#define HID_READER_REPORT_MAX 256 + +struct HidReaderContext { + std::atomic running; + pthread_t thread; + bool threadCreated; + pthread_mutex_t handleMutex; + Hid_DeviceHandle *handle; + uint64_t deviceId; + uint8_t iface; + uint32_t descLen; + int32_t lastError; + + // 统计(1 秒滚动窗口) + uint64_t totalReports; + uint64_t totalBytes; + uint64_t windowReports; + uint64_t windowStartMs; + double reportsPerSec; + + // 入队去重 + 限速(与 UsbDdkPoller 同策略:内容未变不入队, + // 回调最小间隔 2ms,防止 JS 线程繁忙时无界 tsfn 队列增长) + uint8_t lastInputData[HID_READER_REPORT_MAX]; + uint32_t lastInputLen; + bool lastInputValid; + uint64_t lastCallbackTimeMs; + + napi_threadsafe_function reportTsfn; + napi_threadsafe_function errorTsfn; +}; + +static HidReaderContext g_hidReaders[HID_READER_MAX]; +static pthread_mutex_t g_hidReaderMutex = PTHREAD_MUTEX_INITIALIZER; +static bool g_hidReaderPoolInited = false; + +static void initReaderPool() { + if (g_hidReaderPoolInited) return; + for (int i = 0; i < HID_READER_MAX; i++) { + g_hidReaders[i].running.store(false); + g_hidReaders[i].threadCreated = false; + g_hidReaders[i].handle = nullptr; + g_hidReaders[i].reportTsfn = nullptr; + g_hidReaders[i].errorTsfn = nullptr; + pthread_mutex_init(&g_hidReaders[i].handleMutex, nullptr); + } + g_hidReaderPoolInited = true; +} + +static int allocateReader() { + for (int i = 0; i < HID_READER_MAX; i++) { + if (!g_hidReaders[i].running.load() && !g_hidReaders[i].threadCreated) { + return i; + } + } + return -1; +} + +static uint64_t nowMs() { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000ULL + ts.tv_nsec / 1000000ULL; +} + +// ---- tsfn 回调数据 ---- + +struct HidReportEvent { + int32_t readerId; + uint8_t *data; + uint32_t len; +}; + +struct HidReaderEvent { + int32_t readerId; + int32_t code; +}; + +static void readerReportOnJs(napi_env env, napi_value js_callback, void *context, void *rawData) { + HidReportEvent *ev = (HidReportEvent *)rawData; + if (!env || !js_callback || !ev) { + if (ev) { free(ev->data); free(ev); } + return; + } + void *bufData = nullptr; + napi_value arrayBuffer; + if (napi_create_arraybuffer(env, ev->len, &bufData, &arrayBuffer) == napi_ok && bufData) { + memcpy(bufData, ev->data, ev->len); + napi_value data; + if (napi_create_typedarray(env, napi_uint8_array, ev->len, arrayBuffer, 0, &data) == napi_ok) { + napi_value readerIdVal, lenVal, undefined; + napi_create_int32(env, ev->readerId, &readerIdVal); + napi_create_int32(env, (int32_t)ev->len, &lenVal); + napi_get_undefined(env, &undefined); + napi_value argv[3] = { readerIdVal, data, lenVal }; + napi_call_function(env, undefined, js_callback, 3, argv, nullptr); + } + } + free(ev->data); + free(ev); +} + +static void readerEventOnJs(napi_env env, napi_value js_callback, void *context, void *rawData) { + HidReaderEvent *ev = (HidReaderEvent *)rawData; + if (!env || !js_callback || !ev) { + if (ev) free(ev); + return; + } + napi_value readerIdVal, codeVal, undefined; + napi_create_int32(env, ev->readerId, &readerIdVal); + napi_create_int32(env, ev->code, &codeVal); + napi_get_undefined(env, &undefined); + napi_value argv[2] = { readerIdVal, codeVal }; + napi_call_function(env, undefined, js_callback, 2, argv, nullptr); + free(ev); +} + +static void sendReaderEvent(napi_threadsafe_function tsfn, int32_t readerId, int32_t code) { + if (!tsfn) return; + HidReaderEvent *ev = (HidReaderEvent *)malloc(sizeof(HidReaderEvent)); + if (!ev) return; + ev->readerId = readerId; + ev->code = code; + if (napi_call_threadsafe_function(tsfn, ev, napi_tsfn_nonblocking) != napi_ok) { + free(ev); + } +} + +// ---- 读线程(Open 已在 startReader 调用线程同步完成)---- + +static void *hidReaderThread(void *arg) { + int readerId = (int)(intptr_t)arg; + HidReaderContext *ctx = &g_hidReaders[readerId]; + + OH_LOG_INFO(LOG_APP, "[%{public}s] Reader#%{public}d 线程启动: deviceId=%{public}llu iface=%{public}u", + LOG_TAG, readerId, (unsigned long long)ctx->deviceId, ctx->iface); + + ctx->windowStartMs = nowMs(); + ctx->windowReports = 0; + + while (ctx->running.load()) { + uint8_t buf[HID_READER_REPORT_MAX]; + uint32_t bytesRead = 0; + int32_t code = fn_HidReadTimeout(ctx->handle, buf, sizeof(buf), 100, &bytesRead); + if (!ctx->running.load()) break; + + if (code == HID_DDK_SUCCESS && bytesRead > 0) { + ctx->totalReports++; + ctx->totalBytes += bytesRead; + ctx->windowReports++; + + uint64_t now = nowMs(); + if (now - ctx->windowStartMs >= 1000) { + ctx->reportsPerSec = (double)ctx->windowReports * 1000.0 / (double)(now - ctx->windowStartMs); + ctx->windowReports = 0; + ctx->windowStartMs = now; + } + + // 去重:与上一帧完全相同则不入队(状态未变化) + if (ctx->lastInputValid && bytesRead == ctx->lastInputLen && + memcmp(buf, ctx->lastInputData, bytesRead) == 0) { + continue; + } + // 限速:回调最小间隔 2ms(500Hz 上限),不更新去重缓存, + // 累积变化在下个间隔窗口发出 + if (ctx->lastCallbackTimeMs > 0 && now - ctx->lastCallbackTimeMs < 2) { + continue; + } + ctx->lastCallbackTimeMs = now; + + if (bytesRead <= sizeof(ctx->lastInputData)) { + memcpy(ctx->lastInputData, buf, bytesRead); + ctx->lastInputLen = bytesRead; + ctx->lastInputValid = true; + } + + HidReportEvent *ev = (HidReportEvent *)malloc(sizeof(HidReportEvent)); + if (ev) { + ev->readerId = readerId; + ev->len = bytesRead; + ev->data = (uint8_t *)malloc(bytesRead); + if (ev->data) { + memcpy(ev->data, buf, bytesRead); + if (napi_call_threadsafe_function(ctx->reportTsfn, ev, napi_tsfn_nonblocking) != napi_ok) { + free(ev->data); + free(ev); + } + } else { + free(ev); + } + } + } else if (code == HID_DDK_TIMEOUT) { + continue; + } else { + // IO 错误(设备拔出等)→ 上报并退出 + OH_LOG_ERROR(LOG_APP, "[%{public}s] Reader#%{public}d 读失败: %{public}d (%{public}s)", + LOG_TAG, readerId, code, hidErrStr(code)); + ctx->lastError = code; + sendReaderEvent(ctx->errorTsfn, readerId, code); + break; + } + } + + pthread_mutex_lock(&ctx->handleMutex); + if (ctx->handle) { + int32_t closeCode = fn_HidClose(&ctx->handle); + ctx->handle = nullptr; + OH_LOG_INFO(LOG_APP, "[%{public}s] Reader#%{public}d 关闭: %{public}d, 共 %{public}llu 报文", + LOG_TAG, readerId, closeCode, (unsigned long long)ctx->totalReports); + } + pthread_mutex_unlock(&ctx->handleMutex); + + hidReleaseRef(); + ctx->running.store(false); + OH_LOG_INFO(LOG_APP, "[%{public}s] Reader#%{public}d 线程退出", LOG_TAG, readerId); + return nullptr; +} + +// ---- NAPI ---- + +static napi_value HidReader_IsAvailable(napi_env env, napi_callback_info info) { + (void)info; + bool ok = loadHidLibrary() && fn_HidWrite != nullptr; + napi_value result; + napi_get_boolean(env, ok, &result); + return result; +} + +// 返回 readerId(≥0);失败返回 -(HID_DDK 错误码),调用方可同步回退 +// args: deviceId, readTimeoutMs(预留,当前固定 100ms), onReport(readerId, data, length), onError(readerId, code) +static napi_value HidReader_StartReader(napi_env env, napi_callback_info info) { + size_t argc = 4; + napi_value args[4]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (argc < 4) { + napi_value r; + napi_create_int32(env, -HID_DDK_INVALID_PARAMETER, &r); + return r; + } + + int64_t deviceId64 = 0; + napi_get_value_int64(env, args[0], &deviceId64); + + initReaderPool(); + if (!loadHidLibrary()) { + napi_value r; + napi_create_int32(env, -HID_DDK_INIT_ERROR, &r); + return r; + } + + // Init(同步,权限/服务检查在此刻出结果) + if (!hidInitRef()) { + napi_value r; + napi_create_int32(env, -HID_DDK_INIT_ERROR, &r); + return r; + } + + pthread_mutex_lock(&g_hidReaderMutex); + int readerId = allocateReader(); + if (readerId < 0) { + pthread_mutex_unlock(&g_hidReaderMutex); + hidReleaseRef(); + napi_value r; + napi_create_int32(env, -HID_DDK_FAILURE, &r); + return r; + } + HidReaderContext *ctx = &g_hidReaders[readerId]; + ctx->running.store(false); + ctx->handle = nullptr; + ctx->deviceId = (uint64_t)deviceId64; + ctx->iface = 0; + ctx->descLen = 0; + ctx->lastError = 0; + ctx->totalReports = 0; + ctx->totalBytes = 0; + ctx->windowReports = 0; + ctx->windowStartMs = 0; + ctx->reportsPerSec = 0; + ctx->lastInputLen = 0; + ctx->lastInputValid = false; + ctx->lastCallbackTimeMs = 0; + + // iface 扫描(同步):首个 Open 成功且描述符非空者 + bool opened = false; + int32_t failCode = HID_DDK_DEVICE_NOT_FOUND; + for (uint8_t iface = 0; iface <= PROBE_IFACE_MAX; iface++) { + Hid_DeviceHandle *dev = nullptr; + int32_t code = fn_HidOpen(ctx->deviceId, iface, &dev); + if (code == HID_DDK_SUCCESS && dev) { + uint8_t descBuf[128]; + uint32_t descRead = 0; + bool descOk = !fn_HidGetDesc; // 无 GetDesc 符号时退化为仅要求 Open 成功 + if (fn_HidGetDesc) { + int32_t dcode = fn_HidGetDesc(dev, descBuf, sizeof(descBuf), &descRead); + descOk = (dcode == HID_DDK_SUCCESS && descRead > 0); + } + if (descOk) { + ctx->handle = dev; + ctx->iface = iface; + ctx->descLen = descRead; + opened = true; + OH_LOG_INFO(LOG_APP, "[%{public}s] Reader#%{public}d 打开成功: iface=%{public}u desc=%{public}uB", + LOG_TAG, readerId, iface, descRead); + break; + } + OH_LOG_WARN(LOG_APP, "[%{public}s] Reader#%{public}d iface=%{public}u 打开但无描述符,换下一个", + LOG_TAG, readerId, iface); + fn_HidClose(&dev); + continue; + } + if (code == HID_DDK_NO_PERM || code == HID_DDK_INIT_ERROR || code == HID_DDK_SERVICE_ERROR) { + OH_LOG_ERROR(LOG_APP, "[%{public}s] Reader#%{public}d Open 失败: %{public}d (%{public}s)", + LOG_TAG, readerId, code, hidErrStr(code)); + failCode = code; + break; + } + // DEVICE_NOT_FOUND → 下一个 iface + failCode = code; + } + + if (!opened) { + ctx->lastError = failCode; + pthread_mutex_unlock(&g_hidReaderMutex); + hidReleaseRef(); + napi_value r; + napi_create_int32(env, -failCode, &r); + return r; + } + + napi_value name1, name2; + napi_create_string_utf8(env, "HidDdkReport", NAPI_AUTO_LENGTH, &name1); + napi_create_string_utf8(env, "HidDdkError", NAPI_AUTO_LENGTH, &name2); + + bool ok = napi_create_threadsafe_function(env, args[2], nullptr, name1, 0, 1, + nullptr, nullptr, nullptr, readerReportOnJs, &ctx->reportTsfn) == napi_ok + && napi_create_threadsafe_function(env, args[3], nullptr, name2, 8, 1, + nullptr, nullptr, nullptr, readerEventOnJs, &ctx->errorTsfn) == napi_ok; + + if (!ok) { + if (ctx->handle) { fn_HidClose(&ctx->handle); ctx->handle = nullptr; } + if (ctx->reportTsfn) napi_release_threadsafe_function(ctx->reportTsfn, napi_tsfn_abort); + if (ctx->errorTsfn) napi_release_threadsafe_function(ctx->errorTsfn, napi_tsfn_abort); + ctx->reportTsfn = nullptr; + ctx->errorTsfn = nullptr; + pthread_mutex_unlock(&g_hidReaderMutex); + hidReleaseRef(); + OH_LOG_ERROR(LOG_APP, "[%{public}s] Reader#%{public}d 创建 tsfn 失败", LOG_TAG, readerId); + napi_value r; + napi_create_int32(env, -HID_DDK_FAILURE, &r); + return r; + } + + ctx->running.store(true); + int pret = pthread_create(&ctx->thread, nullptr, hidReaderThread, (void *)(intptr_t)readerId); + if (pret != 0) { + ctx->running.store(false); + if (ctx->handle) { fn_HidClose(&ctx->handle); ctx->handle = nullptr; } + napi_release_threadsafe_function(ctx->reportTsfn, napi_tsfn_abort); + napi_release_threadsafe_function(ctx->errorTsfn, napi_tsfn_abort); + ctx->reportTsfn = nullptr; + ctx->errorTsfn = nullptr; + pthread_mutex_unlock(&g_hidReaderMutex); + hidReleaseRef(); + OH_LOG_ERROR(LOG_APP, "[%{public}s] Reader pthread_create 失败: %{public}d", LOG_TAG, pret); + napi_value r; + napi_create_int32(env, -HID_DDK_FAILURE, &r); + return r; + } + + ctx->threadCreated = true; + pthread_mutex_unlock(&g_hidReaderMutex); + + napi_value r; + napi_create_int32(env, readerId, &r); + return r; +} + +static napi_value HidReader_StopReader(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + int32_t readerId = -1; + if (argc >= 1) napi_get_value_int32(env, args[0], &readerId); + if (readerId < 0 || readerId >= HID_READER_MAX) { + napi_value r; + napi_create_int32(env, -1, &r); + return r; + } + + HidReaderContext *ctx = &g_hidReaders[readerId]; + pthread_mutex_lock(&g_hidReaderMutex); + if (!ctx->threadCreated) { + pthread_mutex_unlock(&g_hidReaderMutex); + napi_value r; + napi_create_int32(env, -1, &r); + return r; + } + ctx->running.store(false); + pthread_mutex_unlock(&g_hidReaderMutex); + + pthread_join(ctx->thread, nullptr); + + pthread_mutex_lock(&g_hidReaderMutex); + if (ctx->reportTsfn) { napi_release_threadsafe_function(ctx->reportTsfn, napi_tsfn_release); ctx->reportTsfn = nullptr; } + if (ctx->errorTsfn) { napi_release_threadsafe_function(ctx->errorTsfn, napi_tsfn_release); ctx->errorTsfn = nullptr; } + ctx->threadCreated = false; + pthread_mutex_unlock(&g_hidReaderMutex); + + OH_LOG_INFO(LOG_APP, "[%{public}s] Reader#%{public}d 已停止", LOG_TAG, readerId); + napi_value r; + napi_create_int32(env, 0, &r); + return r; +} + +static napi_value HidReader_WriteOutput(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + int32_t readerId = -1; + if (argc >= 2) napi_get_value_int32(env, args[0], &readerId); + if (readerId < 0 || readerId >= HID_READER_MAX) { + napi_value r; + napi_create_int32(env, -1, &r); + return r; + } + + HidReaderContext *ctx = &g_hidReaders[readerId]; + + uint8_t *data = nullptr; + size_t len = 0; + bool isTyped = false; + napi_is_typedarray(env, args[1], &isTyped); + if (isTyped) { + napi_typedarray_type type; + void *buf = nullptr; + napi_get_typedarray_info(env, args[1], &type, &len, &buf, nullptr, nullptr); + data = (uint8_t *)buf; + } else { + void *buf = nullptr; + napi_get_arraybuffer_info(env, args[1], &buf, &len); + data = (uint8_t *)buf; + } + if (!data || len == 0 || len > 64) { + napi_value r; + napi_create_int32(env, -1, &r); + return r; + } + + pthread_mutex_lock(&ctx->handleMutex); + if (!ctx->handle) { + pthread_mutex_unlock(&ctx->handleMutex); + napi_value r; + napi_create_int32(env, -1, &r); + return r; + } + uint32_t written = 0; + int32_t code = fn_HidWrite(ctx->handle, data, (uint32_t)len, &written); + pthread_mutex_unlock(&ctx->handleMutex); + + if (code != HID_DDK_SUCCESS) { + OH_LOG_WARN(LOG_APP, "[%{public}s] Reader#%{public}d WriteOutput: %{public}d (%{public}s)", + LOG_TAG, readerId, code, hidErrStr(code)); + ctx->lastError = code; + napi_value r; + napi_create_int32(env, -code, &r); + return r; + } + napi_value r; + napi_create_int32(env, (int32_t)written, &r); + return r; +} + +static napi_value HidReader_GetReaderStats(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + int32_t readerId = -1; + if (argc >= 1) napi_get_value_int32(env, args[0], &readerId); + + napi_value result; + napi_create_object(env, &result); + if (readerId < 0 || readerId >= HID_READER_MAX) return result; + + HidReaderContext *ctx = &g_hidReaders[readerId]; + auto setInt = [&](const char *name, int64_t val) { + napi_value v; napi_create_int64(env, val, &v); + napi_set_named_property(env, result, name, v); + }; + napi_value rateVal; + napi_create_double(env, ctx->reportsPerSec, &rateVal); + napi_set_named_property(env, result, "reportsPerSec", rateVal); + + setInt("iface", ctx->iface); + setInt("descriptorLength", ctx->descLen); + setInt("totalReports", (int64_t)ctx->totalReports); + setInt("totalBytes", (int64_t)ctx->totalBytes); + setInt("lastError", ctx->lastError); + napi_value runningVal; + napi_get_boolean(env, ctx->running.load(), &runningVal); + napi_set_named_property(env, result, "running", runningVal); + return result; +} + +void HidDdkReader_Init(napi_env env, napi_value exports) { + napi_value obj; + napi_create_object(env, &obj); + + napi_property_descriptor methods[] = { + { "isAvailable", nullptr, HidReader_IsAvailable, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "startReader", nullptr, HidReader_StartReader, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "stopReader", nullptr, HidReader_StopReader, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "writeOutput", nullptr, HidReader_WriteOutput, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "getReaderStats", nullptr, HidReader_GetReaderStats, nullptr, nullptr, nullptr, napi_default, nullptr }, + }; + + napi_define_properties(env, obj, sizeof(methods) / sizeof(methods[0]), methods); + napi_set_named_property(env, exports, "HidDdk", obj); + + OH_LOG_INFO(LOG_APP, "[%{public}s] HID DDK Reader NAPI 已注册", LOG_TAG); +} diff --git a/nativelib/src/main/cpp/hid_ddk_probe.h b/nativelib/src/main/cpp/hid_ddk_probe.h new file mode 100644 index 00000000..4e18588a --- /dev/null +++ b/nativelib/src/main/cpp/hid_ddk_probe.h @@ -0,0 +1,46 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2024-2025 Moonlight/AlkaidLab + * + * HID DDK Probe - 验证 HID DDK (libhid.z.so) 能否在主进程访问 USB 手柄 + * + * 探测链路(全程日志,只读不写,不接管输入): + * 1. dlopen("libhid.z.so") + dlsym 解析 OH_Hid_* host 侧 API (API 18+) + * 2. OH_Hid_Init() → 权限/服务可用性 + * 3. OH_Hid_Open(deviceId, iface 0..4) → 打开 hidraw 通道 + * 4. OH_Hid_GetRawInfo / GetRawName / GetReportDescriptor → 设备与描述符 + * 5. OH_Hid_ReadTimeout() 限时读输入报文 → 频率与样例 + * 6. OH_Hid_Close() → OH_Hid_Release() + */ + +#ifndef HID_DDK_PROBE_H +#define HID_DDK_PROBE_H + +#include + +/** + * 初始化 HID DDK Probe NAPI 模块。 + * + * 注册到 exports.HidDdkProbe 命名空间: + * - isAvailable(): boolean + * - probe(deviceId, readMs, onResult): void — 异步探测,结果经回调返回 + */ +void HidDdkProbe_Init(napi_env env, napi_value exports); + +/** + * 初始化 HID DDK Reader NAPI 模块(常驻输入通道)。 + * + * 注册到 exports.HidDdk 命名空间: + * - isAvailable(): boolean + * - startReader(deviceId, readTimeoutMs, onReport, onError): number — readerId(≥0), + * 同步打开设备,失败返回 -(HID_DDK 错误码) 供调用方回退。 + * 注意:打开(含 iface 0..4 扫描,每接口一次 HID 服务 IPC + 描述符读取) + * 在调用线程同步执行,典型耗时几毫秒,设备响应慢时最坏可达数百毫秒。 + * 选择同步语义是为了让调用方(UsbDriverService)能确定性回退旧通道。 + * - stopReader(readerId): void + * - writeOutput(readerId, data): number — OH_Hid_Write(震动等输出报告) + * - getReaderStats(readerId): object — iface/descLen/reports/reportsPerSec/lastError + */ +void HidDdkReader_Init(napi_env env, napi_value exports); + +#endif // HID_DDK_PROBE_H diff --git a/nativelib/src/main/cpp/napi_init.cpp b/nativelib/src/main/cpp/napi_init.cpp index a61c7953..aebd1157 100644 --- a/nativelib/src/main/cpp/napi_init.cpp +++ b/nativelib/src/main/cpp/napi_init.cpp @@ -26,6 +26,7 @@ #include "mouse_interceptor.h" #include "usb_helper.h" #include "usb_ddk_poller.h" +#include "hid_ddk_probe.h" // SDL3 库尚未移植到 HarmonyOS,暂时禁用 // #include "sdl3/sdl3_gamepad_napi.h" @@ -174,6 +175,12 @@ static napi_value Init(napi_env env, napi_value exports) { // 初始化 USB DDK Poller NAPI (DDK 高速轮询) UsbDdkPoller_Init(env, exports); + + // 初始化 HID DDK Probe NAPI (HID DDK 可用性探测) + HidDdkProbe_Init(env, exports); + + // 初始化 HID DDK Reader NAPI (HID DDK 常驻输入通道) + HidDdkReader_Init(env, exports); // SDL3 库尚未移植到 HarmonyOS,SDL3 NAPI 暂时禁用 // 当前使用内置的 SDL GameControllerDB 映射数据替代