# handwriting-board **Repository Path**: tigerchentiger/handwriting-board ## Basic Information - **Project Name**: handwriting-board - **Description**: 手写板驱动demo. - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 11 - **Created**: 2024-07-22 - **Last Updated**: 2024-07-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # handwriting-board #### 介绍 此项目为XX手写板打样demo,该项目是手写板应用、手写版驱动合一项目,建议手写板应用与手写板驱动分开项目管理更清晰。 手写板应用主要功能: - 设备连接状态显示(查询、绑定设备驱动) - 手写板按键设置 - 手写板区域设置 - 退出(解绑设备驱动) 手写板驱动应用主要功能: - 与设备进行USB通信(获取设备输入) - 与系统进行通信(向系统写入事件) #### 软件架构 ![驱动架构图](./architecture.png) - 扩展外设管理服务:主要职责设备管理,驱动包管理 - 扩展外设应用:主要职责对驱动进行自定义设置,包括按键、区域等 - 外设扩展驱动(应用):主要职责与系统及设备之间通信,将用户操作体现到系统上 #### 关键代码讲解 手写板应用关键代码: 1.在Index.ets中,页面加载时初始化RpcTool ``` aboutToAppear() { RpcTool.getInstance().init(this) RpcTool.getInstance().bindDevice() } ``` 2.在RpcTool.ets中查询设备列表,并根据vorderId,productId匹配设备 引包: ``` import deviceManager from '@ohos.driver.deviceManager'; ``` 查询设备列表: ``` private query() { hilog.info(0, 'testTag ui', 'query enter'); if (this.isBinding || this.bindState) { hilog.info(0, 'testTag ui', 'has bind. device id:' + this.deviceId) return } if (this.deviceId != -1) { hilog.info(0, 'testTag ui', 'has query. device id:' + this.deviceId) return; } this.isQuerying = true try { // 通过ArkTs接口查询USB设备列表 let devices = deviceManager.queryDevices(deviceManager.BusType.USB); for (let item of devices) { let device = item as deviceManager.USBDevice; hilog.info(0, 'testTag ui', 'querydevice id:' + device.deviceId + ',bustype:' + device.busType + ',vid:' + device.vendorId + ',pid:' + device.productId + ', des:' + device.description); // 通过产品ID过滤目标设备 let index = PRODUCT_ID_LIST.indexOf(device.productId) if (index >= 0) { this.deviceId = device.deviceId; this.vendorId = device.vendorId; this.productId = device.productId; this.description = device.description; break; } } } catch (error) { hilog.error(0, 'testTag ui', `Failed to query device. Code is ${error.code}, message is ${error.message}`); } this.message = this.deviceId.toString(); this.isQuerying = false } ``` 3.在RpcTool.ets绑定设备 ``` private async bind() { try { hilog.info(0, 'testTag ui', 'bindDevice id is:' + this.deviceId); // 通过ArkTs接口通过设备Id将设备绑定到设备驱动上,可以获取到设备驱动的远程对象 let data = await deviceManager.bindDevice(this.deviceId, async (error, data) => { hilog.error(0, 'testTag ui', 'Device is disconnected:' + data); this.bindState = false; this.isBinding = false; try { await deviceManager.unbindDevice(data) } catch (e) { hilog.error(0, 'testTag ui', 'unbindDevice failed, failed code=' + e?.code); } this.bindStateChanged() }); hilog.info(0, 'testTag ui', 'bindDevice success:' + data.deviceId); // 获取到设备驱动的远程对象,可以通过远程对象与设备驱动进行通信(配置驱动参数) this.remote = data.remote as rpc.RemoteObject; if (this.remote === null) { hilog.error(0, 'testTag ui', 'create remote fail'); this.bindState = false; this.isBinding = false; return; } this.bindState = true; this.isBinding = false; hilog.info(0, 'testTag ui', 'create remote success'); } catch (error) { hilog.error(0, 'testTag ui', 'bindDevice fail. Code:' + error + ', message:' + error.message); this.bindState = false; this.isBinding = false; } } ``` 4.在RpcTool.ets根据用户选择,与驱动进行IPC通信 ``` setKeyValue(keyIndex: number, keyValue: number) { // 直接调用rpc的接口向服务端发送消息,客户端需自行对入参进行序列化,对返回值进行反序列化,操作繁琐 let option = new rpc.MessageOption(); let dataSend = new rpc.MessageSequence(); let reply = new rpc.MessageSequence(); dataSend.writeInt(keyIndex); dataSend.writeInt(keyValue); hilog.info(0, 'testTag ui', `send key value to remote keyIndex:${keyIndex} keyValue:${keyValue}`); if (this.remote == null) { hilog.error(0, 'testTag ui', `the remote is null`) return; } // 通过驱动的远程对象与设备驱动进行通信,开发者需约定好应用侧与驱动侧的通信规则 this.remote.sendMessageRequest(REQUEST_CODE_KEY_VALUE, dataSend, reply, option).then((ret) => { let msg = reply.readInt(); hilog.info(0, 'testTag ui', `setKeyValue ret:${ret} msg:${msg}`); }).catch((error) => { hilog.info(0, 'testTag ui', 'setKeyValue failed'); }); } ``` 手写板驱动关键代码: 1.继承驱动扩展能力DriverExtensionAbility,Onit初始化USB接口 引包: ``` // 导入扩展驱动外设元能力 import Extension from '@ohos.app.ability.DriverExtensionAbility'; ``` 驱动类: ``` export default class DriverExtAbility extends Extension { // 获取自定义按键的映射关系 getKeyMapInfo() { let filePath = this.context.filesDir; let keyStorage = dataStorage.getStorageSync(filePath + '/keyInfo'); STORAGE_KEY.forEach((value, index) => { defaultIndexMap[index] = keyStorage.getSync(value.key, value.default) }) } // 获取手写板自定义的输入方向 getDirectionInfo() { let filePath = this.context.filesDir; let keyStorage = dataStorage.getStorageSync(filePath + '/keyInfo'); direction = keyStorage.getSync('direction', 0); } // 当有设备插入到系统,扩展外设框架会通过vId与pId匹配到外设驱动包,匹配到后会通过系统元能力会拉起扩展外设驱动元能力,元能力拉起来便会调用onInit onInit(want): void { deviceId = want.parameters["deviceId"]; // 初始化USB接口,并获取描述信息 let ret = testNapi.getProductDesc(want.parameters["deviceId"]); if (ret != undefined) { [globalThis.productDesc, globalThis.productId] = ret; } this.getKeyMapInfo() this.getDirectionInfo() // 获取屏幕分辨率 display.getAllDisplays((err, data:Array) => { hilog.info(0, 'testTag', 'getAllDisplays callback'); if (err.code) { testNapi.setDisplayInfo(2160, 1440); } else { testNapi.setDisplayInfo(data[0].width, data[0].height); } // 传递给native层 testNapi.setButtonMapping(defaultIndexMap); testNapi.setDirection(direction); GetData(); //PowerManagwer(); }); globalThis.context = this.context; } // 外设驱动元能力释放 onRelease() :void { hilog.info(0, 'testTag', 'DriverAbility onRelease'); // 释放底层资源 testNapi.releaseResource(); } // 外设驱动元能力被连接 onConnect(want): rpc.RemoteObject { hilog.info(0, 'testTag', 'DriverAbility onConnect'); globalThis?.connectStatus = true; let robj = new FirstDriverAbilityStub("remote") return robj; } // 外设驱动元能力断开连接 onDisconnect(want): void { hilog.info(0, 'testTag', 'DriverAbility onDisconnect'); globalThis?.connectStatus = false; } onDump(params): Array { hilog.info(0, 'testTag', 'DriverAbility onDump, params:' + JSON.stringify(params)); return ['params']; } }; ``` 与应用远程IPC Stub类: ``` // 驱动侧,与应用侧通信的远程对象 class FirstDriverAbilityStub extends rpc.RemoteObject { constructor(des) { if (typeof des === 'string') { super(des); } else { return null; } } // 与应用侧约定好的通讯规则 onRemoteMessageRequest(code: number, data: MessageSequence, reply: MessageSequence, options: MessageOption ) { hilog.info(0, 'testTag', `testtag driver extension onRemoteMessageRequest called ${code}`); try{ // code等于1,表示设置自定义按键映射关系 if (code == 1) { let keyIndex = data.readInt(); let keyValue = data.readInt(); console.info(`testtag driver extension start setOneButtonMapping key:${keyIndex} val:${keyValue}`); testNapi.setOneButtonMapping(keyIndex, keyValue); console.info(`testtag driver extension setOneButtonMapping called key:${keyIndex} val:${keyValue}`); reply.writeInt(keyIndex); return true; } else if (code == 2) { // code等于2,表示设置自定义方向 let direction = data.readInt(); console.info(`testtag driver start setDirection direction:${direction}`); testNapi.setDirection(direction); console.info(`testtag driver setDirection called`); reply.writeInt(direction); return true; } else if (code == 3) { // code等于2,表示连接状态变更 let type = data.readInt(); type = (globalThis?.connectStatus ? 1 : 0); hilog.info(0, 'testTag', `testtag get connection status called status:${type}`); reply.writeInt(type); return true; } } catch (error) { hilog.info(0, 'testTag', 'onRemoteMessageRequest exception'); } return true; } } ``` 2.USB接口初始化及参数设置 ``` static napi_value GetProductDesc(napi_env env, napi_callback_info info) { ...... // 省略代码 // 获取设备描述符 auto [res, devDesc] = GetDeviceInfo(); if (!res) { napi_get_undefined(env, &result); return result; } // 获取配置描述符,并占用接口 if (!GetPipeInfo()) { napi_get_undefined(env, &result); return result; } // 初始化数据转换参数,vid及pid DataParser::GetInstance().Init(devDesc.idVendor, devDesc.idProduct); // 设置配置描述符 SetFeature(devDesc.idProduct); // 获取产品描述信息 auto productDesc = GetProductStringDescriptor(devDesc.iProduct); if (!productDesc.has_value()) { OH_LOG_ERROR(LOG_APP, "GetProductStringDescriptor failed"); napi_get_undefined(env, &result); return result; } return CreateJsArray(env, productDesc.value(), devDesc.idProduct); } ``` GetPipeInfo:获取配置描述符,并占用接口 ``` static bool GetPipeInfo() { struct UsbDdkConfigDescriptor *config = nullptr; // 获取配置描述符 auto ret = OH_Usb_GetConfigDescriptor(g_devHandle, 1, &config); if (ret != 0) { OH_LOG_ERROR(LOG_APP, "get config desc failed:%{public}d", ret); return false; } // 从配置描述符中找到手写板相关的接口和端点 auto [res, interface, endpoint, maxPktSize] = GetInterfaceAndEndpoint(config); if (!res) { OH_LOG_ERROR(LOG_APP, "GetInterfaceAndEndpoint failed"); return false; } // 释放配置描述符,防止内存泄露 OH_Usb_FreeConfigDescriptor(config); g_dataEp = endpoint; g_maxPktSize = maxPktSize; g_interface = interface; // 占用接口,同时也会卸载内核键盘驱动 ret = OH_Usb_ClaimInterface(g_devHandle, g_interface, &g_interfaceHandle); if (ret != 0) { OH_LOG_ERROR(LOG_APP, "claim interface failed%{public}d", ret); return false; } return true; } ``` SetFeature:向控制节点写入控制信息 ``` // 向设备写入配置信息 static void SetFeature(uint16_t iProduct) { OH_LOG_INFO(LOG_APP, "SetFeature enter\n"); if(iProduct != 0xA150 && iProduct != 0xA153) { OH_LOG_WARN(LOG_APP, "no need set feature\n"); return; } // 设置feature uint32_t len = 100; struct UsbControlRequestSetup strDescSetup; strDescSetup.bmRequestType = 0x21; strDescSetup.bRequest = 0x09; strDescSetup.wValue = 0x03 << 8 | 0x02; // desc Index strDescSetup.wIndex = 0x0; strDescSetup.wLength = 0x02; uint8_t data[128] = {0x02, 0x02}; uint32_t dataLen = 2; int32_t ret = OH_Usb_SendControlWriteRequest(g_interfaceHandle, &strDescSetup, UINT_MAX, data, dataLen); if(ret < 0) { OH_LOG_ERROR(LOG_APP, "OH_Usb_SendControlWriteRequest error ret:%{public}d", ret); } OH_LOG_INFO(LOG_APP, "SetFeature OK!"); } ``` GetProductStringDescriptor:获取产品信息 ``` static std::optional GetProductStringDescriptor(uint16_t iProduct) { uint8_t strDesc[100] = {0}; // 获取产品字符串描述符 uint32_t len = 100; struct UsbControlRequestSetup strDescSetup; strDescSetup.bmRequestType = 0x80; strDescSetup.bRequest = 0x06; strDescSetup.wValue = (0x03 << 8) | (iProduct); // desc Index strDescSetup.wIndex = 0x409; // language Id strDescSetup.wLength = len; auto ret = OH_Usb_SendControlReadRequest(g_interfaceHandle, &strDescSetup, UINT32_MAX, strDesc, &len); if (ret != 0) { OH_LOG_ERROR(LOG_APP, "send ctrl read failed%{public}d", ret); return {}; } // 将unicode形式的描述符转化为ASCII, 便于打印 std::string desc = UnicodeToAsc(strDesc + 2, len - 2); OH_LOG_INFO(LOG_APP, "strDesc %{public}s\n", desc.data()); return desc; } ``` 3.创建异步任务,从设备获取输入信息,getBoardInput ``` static napi_value GetBoardInput(napi_env env, napi_callback_info info) { ...... // 省略代码 // 创建异步任务 napi_status status = napi_create_async_work(env, nullptr, resource, g_getKeyboardExecute, g_getKeyboardComplete, reinterpret_cast(asyncContext), &asyncContext->work); if (status != napi_ok) { OH_LOG_ERROR(LOG_APP, "%{public}s create async work failed", __func__); return result; } napi_queue_async_work(env, asyncContext->work); return result; } ``` g_getKeyboardExecute:执行任务 ``` static auto g_getKeyboardExecute = [](napi_env env, void *data) { AsyncContext *asyncContext = reinterpret_cast(data); uint32_t bufferLen = g_maxPktSize; // 占用接口,同时也会卸载内核键盘驱动 // 创建用于存放数据的缓冲区 int32_t ret = OH_Usb_CreateDeviceMemMap(g_devHandle, bufferLen, &devMmap); if (ret != 0 || devMmap == nullptr) { return; } DataParser &parser = DataParser::GetInstance(); parser.StartWork(); stopIo = false; while (!stopIo) { // 读取手写板数据 ret = GetUsbBoardInput(devMmap); if (ret != 0) { std::this_thread::sleep_for(std::chrono::seconds(1)); continue; } // 处理读取到的数据 parser.ParseData(devMmap->address, devMmap->transferedLength); } if (ret == 0) { asyncContext->status = napi_ok; } else { asyncContext->status = napi_generic_failure; } }; ``` GetUsbBoardInput:通过终端方式读取设备输入信息 ``` static int32_t GetUsbBoardInput(UsbDeviceMemMap *devMmap) { struct UsbRequestPipe pipe; pipe.interfaceHandle = g_interfaceHandle; pipe.endpoint = g_dataEp; pipe.timeout = UINT32_MAX; // 等待5s // 通过USB中断传输方式,读取键值 int32_t ret = OH_Usb_SendPipeRequest(&pipe, devMmap); if (ret != 0) { OH_LOG_ERROR(LOG_APP, "pipe read failed%{public}d", ret); return -1; } return 0; } ``` g_getKeyboardComplete:线程结束,进行释放资源 4.通过DataParser转换数据,向系统写入数据 创建键盘设备: ``` int32_t InjectThread::CreateKeyBoardDevice(std::string deviceName) const { // 设备属性 Hid_Device hidDevice = { .deviceName = deviceName.c_str(), .vendorId = 0x6006, .productId = 0x6008, .version = 1, .bustype = BUS_USB}; // 设备关注事件类型 std::vector eventType = {HID_EV_KEY}; Hid_EventTypeArray eventTypeArray = {.hidEventType = eventType.data(), .length = (uint16_t)eventType.size()}; // 设置设备关注的键值 std::vector keyCode = { HID_KEY_1, HID_KEY_SPACE, HID_KEY_BACKSPACE, HID_KEY_ENTER, HID_KEY_ESC, HID_KEY_SYSRQ, HID_KEY_LEFT_SHIFT, HID_KEY_RIGHT_SHIFT, HID_KEY_VOLUME_DOWN, HID_KEY_VOLUME_UP, HID_KEY_0, HID_KEY_2, HID_KEY_3, HID_KEY_4, HID_KEY_5, HID_KEY_6, HID_KEY_7, HID_KEY_8, HID_KEY_9, HID_KEY_A, HID_KEY_B, HID_KEY_C, HID_KEY_D, HID_KEY_E, HID_KEY_F, HID_KEY_G, HID_KEY_H, HID_KEY_I, HID_KEY_J, HID_KEY_K, HID_KEY_L, HID_KEY_M, HID_KEY_N, HID_KEY_O, HID_KEY_P, HID_KEY_Q, HID_KEY_R, HID_KEY_S, HID_KEY_T, HID_KEY_U, HID_KEY_V, HID_KEY_W, HID_KEY_X, HID_KEY_Y, HID_KEY_Z, HID_KEY_DELETE}; Hid_KeyCodeArray keyCodeArray = {.hidKeyCode = keyCode.data(), .length = (uint16_t)keyCode.size()}; // 设备关注的事件 Hid_EventProperties hidEventProp = {.hidEventTypes = eventTypeArray, .hidKeys = keyCodeArray}; // 创建以上属性的设备 return CreateDevice(&hidDevice, &hidEventProp); } ``` 创建触摸设备: ``` int32_t InjectThread::CreateTouchPadDevice(std::string deviceName) const { std::vector deviceProp = {HID_PROP_DIRECT}; Hid_Device hidDevice = { .deviceName = deviceName.c_str(), .vendorId = 0x6006, .productId = 0x6006, .version = 1, .bustype = BUS_USB, .properties = deviceProp.data(), .propLength = (uint16_t)deviceProp.size() }; // 设备关注事件类型 std::vector eventType = {HID_EV_ABS, HID_EV_KEY, HID_EV_SYN, HID_EV_MSC}; Hid_EventTypeArray eventTypeArray = {.hidEventType = eventType.data(), .length = (uint16_t)eventType.size()}; // 设备关注按键键值范围 std::vector keyCode = {HID_BTN_TOOL_PEN, HID_BTN_TOOL_RUBBER, HID_BTN_TOUCH, HID_BTN_STYLUS, HID_BTN_RIGHT}; Hid_KeyCodeArray keyCodeArray = {.hidKeyCode = keyCode.data(), .length = (uint16_t)keyCode.size()}; // 设备关注特殊事件扫描 std::vector mscEvent = {HID_MSC_SCAN}; Hid_MscEventArray mscEventArray = {.hidMscEvent = mscEvent.data(), .length = (uint16_t)mscEvent.size()}; // 设备关注绝对事件坐标值及压力值 std::vector absAxes = {HID_ABS_X, HID_ABS_Y, HID_ABS_PRESSURE}; Hid_AbsAxesArray absAxesArray = {.hidAbsAxes = absAxes.data(), .length = (uint16_t)absAxes.size()}; Hid_EventProperties hidEventProp = { .hidEventTypes = eventTypeArray, .hidKeys = keyCodeArray, .hidAbs = absAxesArray, .hidMiscellaneous=mscEventArray }; int32_t maxX, maxY, maxPressure; DataParser::GetInstance().GetAbsMax(maxX, maxY, maxPressure); OH_LOG_INFO(LOG_APP, "OH_Usb_CreateDevice maxX:%{public}d, maxY:%{public}d, maxPressure:%{public}d", maxX, maxY, maxPressure); hidEventProp.hidAbsMin[HID_ABS_X] = 0; hidEventProp.hidAbsMin[HID_ABS_Y] = 0; hidEventProp.hidAbsMin[HID_ABS_PRESSURE] = 0; hidEventProp.hidAbsMax[HID_ABS_X] = maxX; hidEventProp.hidAbsMax[HID_ABS_Y] = maxY; hidEventProp.hidAbsMax[HID_ABS_PRESSURE] = maxPressure; // 创建以上属性的设备 return CreateDevice(&hidDevice, &hidEventProp); } ```