1# uart手势模块的检测app 2## 作品介绍 3通过NAPI框架实现,检测手势后调用js端的回调函数 4## 开发环境 51. 搭载OpenHarmony-3.2-Beat5版本的Unionpi Tiger开发板 62. DevEco Studio 3.1.0.0 73. uart手势识别带触摸传感器 8## uart简介 9UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是一种双向、串行、异步的通信总线,仅用一根数据接收线和一根数据发送线就能实现全双工通信。典型的串口通信使用3根线完成,分别是:发送线(TX)、接收线(RX)和地线(GND),通信时必须将双方的TX和RX交叉连接并且GND相连才可正常通信 10## 手势识别带触摸传感器简介 11商品链接:[**Gravity: 手势识别带触摸传感器**](https://www.dfrobot.com.cn/goods-1994.html) 12 13 14这款是一款集成了手势识别功能和触摸识别功能的传感器模块,它的检测距离为30cm,距离0-30cm可调。其中,能够识别向右、向左、向后、向前、下压、上拉及上拉下压后手松开共7种手势,以及5路的触摸信号,并具有自动睡眠和唤醒的功能。模块自带手势识别算法,输出数据简洁可靠,可通过串口直接与arduino及树莓派等控制器或上位机通讯。用于智能灯、人机交互、智能小车、趣味游戏等多功能远距离手势控制端。 15 16 17### 接线方式 18 **3.3V —— 正极** 19 20 **RX —— TX** 21 22 **TX —— RX** 23 24 **GND —— 负极** 25 26### 通信参数及方式 27#### 通信参数 28串口波特率 9600; 298 个数据位; 30无校验位; 311 个停止位; 32#### 通信方式 33格式: 340xAA,数据码,检验码,0x55; 35帧头:0xAA; 36帧尾:0x55; 37 38数据码: 39右:0x01; 40左:0x02; 41后:0x03; 42前:0x04; 43下压:0x05; 44上拉:0x06; 45上拉下压后手松开:0x07; 46触摸 1:0x21; 47触摸 2:0x22; 48触摸 3:0x23; 49触摸 4:0x24; 50触摸 5:0x25; 51 52校验码: 53校验码为数据码的反码,比如做右动作时,数据码为 0x01,校验码为 0xFE。 54## uart实现 55由于hdf框架的uart驱动无法使用(前面初始化部分返回都为success,read却一直返回-17,也没有读到数据),经过修改适配层源码后解决了这一问题,通过对hdf uart部分源码的研究,发现适配层没有编写`int32_t (*SetTransMode)(struct UartHost *host, enum UartTransMode mode)`的相关代码,只是写了个空实现,这导致hdf uart部分不能实现阻塞模式的读取,因此先实现这一部分,代码如下: 56 57主要思路是根据参数修改file mode的O_NDELAY,然后设置阻塞模式需要的两个参数VMIN和VTIME分别为1和0(读到一个字节就结束阻塞,不设置超时时间) 58```C++ 59static int32_t UartAdapterSetTransMode(struct UartHost *host, enum UartTransMode mode) 60{ 61 struct termios termios; 62 struct file *fp = NULL; 63 if (host == NULL) { 64 return HDF_ERR_INVALID_OBJECT; 65 } 66 fp = (struct file *)host->priv; 67 if (UartAdapterIoctlInner(fp, TCGETS, (unsigned long)&termios) < 0) { 68 HDF_LOGE("tcgets fail"); 69 return HDF_FAILURE; 70 } 71 if(mode==UART_MODE_RD_BLOCK){ 72 fp->f_flags&=~O_NDELAY; 73 termios.c_cc[VMIN] = 1; 74 termios.c_cc[VTIME] = 0; 75 }else if(mode==UART_MODE_RD_NONBLOCK){ 76 fp->f_flags|=O_NDELAY; 77 termios.c_cc[VMIN] = 0; 78 termios.c_cc[VTIME] = 0; 79 }else{ 80 return HDF_ERR_INVALID_PARAM; 81 } 82 if (UartAdapterIoctlInner(fp, TCSETS, (unsigned long)&termios) < 0) { 83 HDF_LOGE("tcsets fail"); 84 return HDF_FAILURE; 85 } 86 return HDF_SUCCESS; 87} 88``` 89然后使用UartOpen()打开串口,设置好参数和阻塞模式后就可以调用UartRead()成功读到数据了 90## NAPI层 91### 实现原理 92js层可设置相应的回调函数(触摸模块的事件处理函数),为了监控触摸事件,在注册napi函数之后,开启一个新的线程。该线程会一直读取串口数据,当串口有数据到来时,如果js层之前已经设置过回调函数,则调用该回调函数。但由于只能由主线程才能调用`napi_call_funtion`(如果跨线程调用会crash),需要借助napi框架`threadsafe_funtion`的相关函数,设置回调时,首先调用`napi_create_threadsafe_function`将js传来的回调函数转换为一个`napi_threadsafe_function`,然后串口读取线程调用`napi_call_threadsafe_function`来间接调用js层的回调函数 93### 实现代码 94#### 设置js回调函数 95```C++ 96static napi_value onTouch(napi_env env, napi_callback_info cb) 97{ 98 napi_value ret; 99 napi_get_undefined(env, &ret); 100 // 获取js传来的参数 101 size_t argc = 1; 102 napi_value argv; 103 napi_get_cb_info(env, cb, &argc, &argv, nullptr, nullptr); 104 // 校验参数数量 105 NAPI_ASSERT(env, argc == 1, "requires 1 parameter"); 106 // 校验参数类型 107 napi_valuetype type; 108 napi_typeof(env, argv, &type); 109 NAPI_ASSERT(env, type == napi_function, "frist parameter type requires callback"); 110 // 创建传来的回调函数的引用 111 napi_ref funRef; 112 napi_create_reference(env, argv, 1, &funRef); 113 // 创建资源和资源名 114 napi_value resource, resourceName; 115 napi_get_null(env, &resource); 116 napi_create_string_latin1(env, "touchCallback", NAPI_AUTO_LENGTH, &resourceName); 117 callbackMutex.lock(); 118 if (touchCallback != nullptr) { 119 // 如果先前设置过回调函数,释放先前回调函数的相关资源 120 napi_ref funRef; 121 napi_get_threadsafe_function_context(touchCallback, (void**)&funRef); 122 napi_delete_reference(env, funRef); 123 napi_release_threadsafe_function(touchCallback, napi_tsfn_release); 124 } 125 // 创建threadsafe_function 126 napi_create_threadsafe_function(env, argv, resource, resourceName, 0, 1, 127 nullptr, nullptr, funRef, callfunc, &touchCallback); 128 callbackMutex.unlock(); 129 return ret; 130} 131``` 132#### threadsafe_function的回调函数 133napi_call_threadsafe_function实际上并不会直接调用js的回调函数,而是通知主线程,让主线程调用创建threadsafe_function时设置的回调函数,这个回调函数负责将c/c++的参数转换为js的参数,然后才会调用真正的js回调函数(这些都只能在主线程做),代码如下: 134```C++ 135static void callfunc(napi_env env, napi_value js_callback, void* context, void* data) 136{ 137 // 将*data转换为对应的类型 138 uint8_t* codePtr = reinterpret_cast<uint8_t*>(data); 139 // 从content中取出指向js回调函数的引用 140 napi_ref funRef = reinterpret_cast<napi_ref>(context); 141 napi_value code, result, global, fun; 142 napi_get_global(env, &global); 143 // 将事件码转换为js变量 144 napi_create_uint32(env, *codePtr, &code); 145 // 取出引用指向的js回调函数 146 napi_get_reference_value(env, funRef, &fun); 147 delete codePtr; 148 // 调用js回调函数 149 napi_call_function(env, global, fun, 1, &code, &result); 150} 151``` 152#### 串口读取线程调用threadsafe_function函数 153该线程会一直读取串口的数据,并调用`napi_call_threadsafe_function`函数,代码如下: 154```C++ 155static void readTouchLoop() 156{ 157 while (true) { 158 int32_t res = uart1->readTouch(); 159 if (res != -1) { 160 uint8_t* codePtr = new uint8_t; 161 *codePtr = static_cast<uint8_t>(res); 162 callbackMutex.lock(); 163 if (touchCallback != nullptr) { 164 napi_call_threadsafe_function(touchCallback, codePtr, napi_tsfn_nonblocking); 165 } 166 callbackMutex.unlock(); 167 } 168 } 169} 170``` 171#### 设置触摸事件枚举 172为了方便app层知晓事件码对应了什么事件,napi层需要设置触摸事件的枚举。具体方法是创建EventCode对象,然后将各个触摸事件的枚举值设置为对象的静态字段,然后添加进导出对象中。 173```C++ 174static napi_value defineEventCode(napi_env env) 175{ 176 using namespace Uart; 177 napi_value eventCode, right, left, back, forward, pullUp, pullDown, pullMove, 178 touch1, touch2, touch3, touch4, touch5; 179 // 创建EventCode对象 180 napi_create_object(env, &eventCode); 181 // 用c++的常量创建对应的js变量 182 napi_create_uint32(env, static_cast<uint32_t>(eventCode::right), &right); 183 napi_create_uint32(env, static_cast<uint32_t>(eventCode::left), &left); 184 napi_create_uint32(env, static_cast<uint32_t>(eventCode::back), &back); 185 napi_create_uint32(env, static_cast<uint32_t>(eventCode::forward), &forward); 186 napi_create_uint32(env, static_cast<uint32_t>(eventCode::pullUp), &pullUp); 187 napi_create_uint32(env, static_cast<uint32_t>(eventCode::pullDown), &pullDown); 188 napi_create_uint32(env, static_cast<uint32_t>(eventCode::pullMove), &pullMove); 189 napi_create_uint32(env, static_cast<uint32_t>(eventCode::touch1), &touch1); 190 napi_create_uint32(env, static_cast<uint32_t>(eventCode::touch2), &touch2); 191 napi_create_uint32(env, static_cast<uint32_t>(eventCode::touch3), &touch3); 192 napi_create_uint32(env, static_cast<uint32_t>(eventCode::touch4), &touch4); 193 napi_create_uint32(env, static_cast<uint32_t>(eventCode::touch5), &touch5); 194 // 将js变量设置为对象的静态成员 195 napi_property_descriptor desc[] = { 196 DECLARE_NAPI_STATIC_PROPERTY("right", right), 197 DECLARE_NAPI_STATIC_PROPERTY("left", left), 198 DECLARE_NAPI_STATIC_PROPERTY("back", back), 199 DECLARE_NAPI_STATIC_PROPERTY("forward", forward), 200 DECLARE_NAPI_STATIC_PROPERTY("pullUp", pullUp), 201 DECLARE_NAPI_STATIC_PROPERTY("pullDown", pullDown), 202 DECLARE_NAPI_STATIC_PROPERTY("pullMove", pullMove), 203 DECLARE_NAPI_STATIC_PROPERTY("touch1", touch1), 204 DECLARE_NAPI_STATIC_PROPERTY("touch2", touch2), 205 DECLARE_NAPI_STATIC_PROPERTY("touch3", touch3), 206 DECLARE_NAPI_STATIC_PROPERTY("touch4", touch4), 207 DECLARE_NAPI_STATIC_PROPERTY("touch5", touch5), 208 }; 209 napi_define_properties(env, eventCode, sizeof(desc) / sizeof(desc[0]), desc); 210 // 返回枚举对象 211 return eventCode; 212} 213``` 214## app层 215先编写对应的.d.ts定义文件 216```TypeScript 217declare namespace uart_ctl { 218 enum EventCode { 219 right, 220 left, 221 back, 222 forward, 223 pullUp, 224 pullDown, 225 pullMove, 226 touch1, 227 touch2, 228 touch3, 229 touch4, 230 touch5 231 } 232 233 function onTouch(callback: (event: EventCode) => void): void; 234 235 function closeTouch(): void; 236} 237 238export default uart_ctl; 239``` 240然后在index.ets设置回调函数 241```TypeScript 242.onAppear(() => { 243 uart_ctl.onTouch((event) => { 244 switch (event) { 245 case uart_ctl.EventCode.right: 246 Prompt.showToast({ message: "get event right" }); 247 break; 248 case uart_ctl.EventCode.left: 249 Prompt.showToast({ message: "get event left" }); 250 break; 251 case uart_ctl.EventCode.back: 252 Prompt.showToast({ message: "get event back" }); 253 break; 254 case uart_ctl.EventCode.forward: 255 Prompt.showToast({ message: "get event forward" }); 256 break; 257 case uart_ctl.EventCode.pullUp: 258 Prompt.showToast({ message: "get event pull up" }); 259 break; 260 case uart_ctl.EventCode.pullDown: 261 Prompt.showToast({ message: "get event pull down" }); 262 break; 263 case uart_ctl.EventCode.pullMove: 264 Prompt.showToast({ message: "get event pull move" }); 265 break; 266 case uart_ctl.EventCode.touch1: 267 Prompt.showToast({ message: "get event touch1" }); 268 break; 269 case uart_ctl.EventCode.touch2: 270 Prompt.showToast({ message: "get event touch2" }); 271 break; 272 case uart_ctl.EventCode.touch3: 273 Prompt.showToast({ message: "get event touch3" }); 274 break; 275 case uart_ctl.EventCode.touch4: 276 Prompt.showToast({ message: "get event touch4" }); 277 break; 278 case uart_ctl.EventCode.touch5: 279 Prompt.showToast({ message: "get event touch5" }); 280 break; 281 default: 282 Prompt.showToast({ message: "error" }); 283 break; 284 } 285 }); 286}) 287``` 288### 遇到的问题 289问题1: 290问题描述:在napi层调用hilog打日志,但怎么都看不到日志 291问题原因:OpenHarmony-3.2-Beat5存在bug,native层的所有hilog都看不到。 292解决方案: OpenHarmony系统引入了默认日志级别,修改为D即可 293 294 295问题2: 296问题描述:在callfunc函数中,方舟运行时传来的js_callback是一个无效的对象,当调用napi_call_function传入它时会直接crash 297问题原因:通过打印日志(打印一开始的js回调函数地址,调用napi_call_function时js_callback的地址,真正的js回调函数的地址),发现从传入js回调函数到调用js回调函数这个过程中,地址发生了变化,推测是在这其中的时间触发了gc,而gc移动了js回调函数的位置,而方舟运行时没有更新这个地址,仍然传入了旧的地址,旧的地址指向的内存已经被gc释放,s导致crash 298 299 300解决方案:不使用js保存的js_callback,而是手动将js回调函数的引用存入context中,然后在callfunc函数里再取出来 301 302问题3: 303问题描述: 开机后,使用hdf读取uart数据读取不到,需要使用linux接口操作uart的程序提前打开关闭设备后,才能正常读取数据 304 305## 作品展示 306