README.md
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