lora® 和 lorawan® 已经成为了物联网世界的重要技术,也向人们提供了诸多易于使用的远程通信解决方案。在这过程中电脑设备却被忽略了,我们会发现带有 lora® 模块的笔记本电脑很少见。
现在这种局面陆续得到了改善,在一些解决方案中,已经开始出现用于笔记本电脑的 lora® 模块了。最近笔者利用瑞科慧联的低代码开发平台 rui3 制作了一个 lora® usb 适配器,它可以直接连接到笔记本电脑或树莓派上。大多数时候,这个适配器可以作为收发器用于家居场景种;但它也作为一个方便测试的平台,比如:远程用笔记本电脑发送命令、记录结果等等。
使用瑞科慧联的模块化硬件开发平台 wisblock,让这样的应用开发变得更加简单。笔者通过 wisblock 制作了两种适配器,一种是使用计算机上的自定义软件来管理 lora® 模块的 at 固件,另一种是直接在 lora® 模块上完成大部分工作。在这两种适配器中,电脑都是作为终端来使用。今天要介绍的是后一种适配器,主要就是使用 rui3 为 lora® 通信模块 rak4631-r 制作一个简单的自定义固件。
一、前期准备硬件1、选择 rak4631-r(不同国家或地区对应频率的频段不同)。
(注意,这里我们也可以使用另一款通信模块 rak3172,因为他们均支持 rui3 编译,只要有自己所需要的功能就行。因为 rak3172 不支持蓝牙和硬件加密,但该项目需要加密 lora® 数据包,而且将 aes128 添加到代码中也超出了本文的范围,所以这里我们选择了 rak4631-r。)
2、底板:本例中,我们选择了 rak19003,它具有最小的封装尺寸 30 mm x 35 mm。
3、usb 电缆(适用于 rak19003 的 usb type-c)。
软件1、arduino ide。
2、终端应用程序,例如笔者最喜欢的 coolterm。当然 arduino ide 的串行终端,也能完成开发。
工作模式lora® 适配器基本上需要两种工作模式:传输模式和设置模式。而 at 固件本质上是单模模式的,即它们总是处于设置模式。在设置模式中,甚至发送和接收都是命令。与此相反,默认的传输模式充当 lora® 模块和 usb 端口之间的桥梁:“无论一端输入任何内容,都将从另一端输出”。只有当用户发出特殊字符串时,适配器才会在传输和设置模式之间切换。 笔者见过一些 lora® 模块为此提供一两个引脚来实现这一点,可以设置引脚高低电平从硬件上切换这两种模式,但这样的操作对电脑来说是不可能的。因此,用户可以使用不太可能出现的特殊字符串去切换这两种模式。然而在调制解调器时代,“$$$”经常作为特殊的字符串去使用,所以我们也可以使用该字符串实现。
二、工作流程在常规的 lora® 应用程序中,工作流程通常如下:
初始化串口设置 wire,然后设置 lora® 模块(引脚分配等)设置 lora® 配置(sf、bw、频率等)
本文使用到 rui3,因此可直接去掉第二点,因为 api 已经配置完成、电池也配置好了。在 rui 的 api 中,lorawan® 是提供了 lora 选项区域帮助用户配置 lora®。并且 lora® 模块在 rak4631-r 中是预先连通的,所以只需调用 lorawan® 的几行 api 设置所需的配置,就可以检查结果:
bool rslt = api.lorawan.nwm.set(0); if (!rslt) { // do something } rslt = api.lorawan.pfreq.set(myfreq); if (!rslt) { // do something } rslt = api.lorawan.psf.set(sf); if (!rslt) { // do something } rslt = api.lorawan.pbw.set(bw); if (!rslt) { // do something } // etc etc etc...
通过检查,已经设置完成了,结果与 api 设定的配置是一致的。
然后设置 lora® 回调:接收和传输。这里让用户能够以异步方式将“管理这些事件的代码”单独管理运行,而不是在主 loop() 代码中循环运行。
最后一行是为了将 lora® 模块设置为了永久监听模式。
api.lorawan.registerprecvcallback(recv_cb); api.lorawan.registerpsendcallback(send_cb); rslt = api.lorawan.precv(65534);
最后,就可以在 setup() 中完成自己的需求了。例如:让 oled 检查状态,或设置 led的状态(电路板上有 2 个可用,1 个绿色和 1 个蓝色)等。到这一步一切都准备好了,一起来看看接下来会发生什么?
三、loop()在 loop() 中,循环检查串行端口是否有字符传入,并对其进行相应的操作。稍后我会详细介绍这一点。接着还需要检查 lora® 模块,如果有接收到数据包,则将接收数据包中的内容打印到串口上。这是两个部分之间的桥梁。在其他框架中,这通常与串口相同。接着 lora® 模块循环监听,如果有内容,直接读取。这个功能 rui3 中并不包含,需要在上面声明的 void recv_cb(rui_lora_p2p_recv_t data) 函数中自己实现并进行,在将 lora® 模块接收的原始数据发送到 serial 之前,可以在这个函数中决定如何处理原始数据。例如:如果需要 json 数据,可以将其解析之后在打印到串口。同样,如果数据是加密的,或者希望它是加密的,就可以在进一步处理之前在那进行解密。回调函数代码如下所示:
void recv_cb(rui_lora_p2p_recv_t data) { uint16_t ln = data.buffersize; char plaintext[ln + 1] = {0}; char buff[92]; sprintf(buff, incoming message, length: %d, rssi: %d, snr: %d, data.buffersize, data.rssi, data.snr); serial.println(buff); if (needaes) { // do we need to decrypt the data? int rslt = aes.process((char*)data.buffer, ln, myiv, mypwd, 16, plaintext, aes.decryptflag, aes.ecbmode); if (rslt < 0) { serial.printf(error %d in process ecb decrypt\n, rslt); return; } } else { // no? just copy the data memcpy(plaintext, data.buffer, ln); } // the easiest way to know whether the data is a json packet is to try and decode it :-) staticjsondocument doc; deserializationerror error = deserializejson(doc, plaintext); if (!error) { jsonobject root = doc.as(); // using c++11 syntax (preferred): for (jsonpair kv : root) { sprintf(buff, * %s: %s, kv.key().c_str(), kv.value().as()); serial.println(buff); } return; // end for json messages } // there was an error, so this is not a json packet – not well-formed anyway. // print it as a plain message serial.println(message:); serial.println(plaintext); }
四、tx(发送)发送同样也有一个回调函数,当数据发送完成时可调用。用户也可以在那里添加东西,但它在正常使用中基本上是为了确保 lora® 模块返回到监听模式中:
void send_cb(void) { // tx callback serial.println(tx done!); issending = false; // flag used to determine whether we're still sending something or we're free to send. api.lorawan.precv(65534); }
该回调函数需要快速的执行并使 lora® 模块返回到监听模式,不需要在其中加入长延时等待。
五、设置模式当用户发送 $$$(后缀为 \n)时,代码会切换到设置模式。这部分稍微复杂一些,发送命令这一段会重复被使用,所以为了使用方便,大部分都是复制粘贴后,对该段进行更改其函数名,并为每个命令添加合适的代码。因此我们需要一个统一的命令结构,如下所示:
int cmdcount = 0; struct mycommand { void (*ptr)(char *); // function pointer char name[12]; char help[48]; };
(cmdcount 马上就会派上用场)。命令的结构由指针函数、函数名和命令描述三部分组成。
下图是声明了一个命令数组:
mycommand cmds[] = { {handlehelp, help, shows this help.}, {handlep2p, p2p, shows the p2p settings.}, {handlefreq, fq, gets/sets the working frequency.}, {handlebw, bw, gets/sets the working bandwidth.}, {handlesf, sf, gets/sets the working spreading factor.}, {handlecr, cr, gets/sets the working coding rate.}, {handletx, tx, gets/sets the working tx power.}, {handleaes, aes, gets/sets aes encryption status.}, {handlepassword, pwd, gets/sets aes password.}, {handleiv, iv, gets/sets aes iv.}, {handlejson, json, gets/sets json sending status.}, };
到目前为止一切都顺利。所以在 setup() 函数启动时,会计算可用命令的数量,以便知道我们有多少个命令。cmdcount = sizeof (cmds)/ sizeof (mycommand): 这在 evalcmd 函数中用于遍历命令,cmdcount 即为最终统计到的命令个数。
void evalcmd(char *str, string fullstring) { uint8_t ix, iy = strlen(str); for (ix = 0; ix = 'a' && c <= 'z') str[ix] = c + 32; } serial.print(evaluating: `); serial.print(fullstring.c_str()); serial.println(`); for (int i = 0; i < cmdcount; i++) { if (strcmp(str, cmds[i].name) == 0) { // call the function cmds[i].ptr((char*)fullstring.c_str()); return; } } }
在此之后,添加命令和处理它们的调用就非常容易了。让我们来看看 handlehelp (char*) 命令:
void handlehelp(char *param) { serial.printf(available commands: %d\n, cmdcount); for (int i = 0; i < cmdcount; i++) { sprintf(msg, . %s: %s, cmds[i].name, cmds[i].help); serial.println(msg); } }
char *param 参数可能需要也可能不需要,因此默认发送,每个命令都可以自由使用或者直接忽略它。例如:handlefreq() 命令便要使用该参数:
void handlefreq(char *param) { if (strcmp(fq, param) == 0) { // no parameters sprintf(msg, p2p frequency: %.3f mhz\n, (myfreq / 1e6)); serial.print(msg); sprintf(msg, fq: %.3f mhz\n, (myfreq / 1e6)); displayscroll(msg); return; } else { // fq xxx.xxx set frequency float value = atof(param + 2); if (value 960.0) { // sx1262 freq range 150mhz to 960mhz // your chip might not support all... sprintf(msg, invalid frequency value: %.3f\n, value); serial.print(msg); return; } myfreq = value * 1e6; api.lorawan.precv(0); // turn off reception while we're doing setup sprintf(msg, set p2p frequency to %3.3f: %s mhz\n, (myfreq / 1e6), api.lorawan.pfreq.set(myfreq) ? success : fail); serial.print(msg); api.lorawan.precv(65534); sprintf(msg, new freq: %.3f, value); displayscroll(msg); return; } }
一切操作之后有了现在的结果,编码历时几个小时,就得到了一个功能齐全的 lora® usb 适配器。但实际上没有用这么多时间,因为笔者重用了以前项目中的 commands.h 代码,并且暂时跳过 aes 加密部分,把它留在示例项目中是因为它相对比较复杂,且通常不是简单项目的一部分。通常可以在项目正常运行后再添加 aes,这样就不必担心其他东西会受影响。但是,就像 commands.h 一样,笔者已经从其他项目准备好 aes 文件,所以对它的实现也只是复制粘贴工作。
六、扩展功能蔓延(feature creep)一直都是困扰开发人员的问题,但现在我们暂时可以先忽略这一点。一起来看看这个项目可以有哪些扩展:
1、oled 显示屏
由于引脚配置,显示屏要在底板背面添加,但添加起来也是很方便。学习一些如何关闭屏幕的编程代码,可以帮助节省能源和保护屏幕;
2、rtc 实时时钟
可以在 json 数据包或类似 cayenne lpp 的格式中为数据包添加时间戳;
3、gnss 模块
用户可以将 gps 坐标添加到数据包中,而且如果已经在家中设置了收发器的坐标,还可以使用它们的自动计算距离(haversine 公式)的功能。
4、固件的 ble uart 路由
添加这个功能很简单。一旦设置了 ble,代码就与串行代码几乎相同了。这样操作之后,它就不仅仅是一个用于电脑的 usb lora® 适配器了,加上电池它可以成为手机无线 lora® 适配器。
以上这些,这个使用 rui3 制作的项目都能实现、也都可以拥有这些功能。如果你们感兴趣,也可以自己动手试试!
变频器对电机有什么影响
特斯拉降价后宝马牧马人涨价
如何看待4G套餐下架的现象?
基于Palladium解决方案实现硬件加速器的设计
南卡runner Pro骨传导成为华强北爆款产品,官方也无能为力
如何使用RUI3制作一款用于电脑的多功能LoRa®适配器
机器视觉定位和视觉检测系统的区别
基于BOA和nRF24L01的智能家居系统
VR为什么还是没流行起来
噪音干扰的特点和基本原理
数据采集网关助力数字乡村物联网系统搭建
如何应对日益复杂的物理和网络攻击,有什么网络安全解决方案
小米7概念设计曝光:还有哪些是你没有想到的?
针对工业应用中智能传感器和执行器点对点三线式接口的通信标准
梦芯科技喜获中国卫星导航定位创新应用奖“金奖”
开口互感器在越南美的工业云系统中的应用
芯存中国,千城行动——2023佰维工控存储全国巡展首站即将登陆成都!
猝不及防!Google人工智能助理开始“说人话”了!
美高森美提供的SmartFusion2 SoC FPGA双轴电机控制套件带有模块化电机控制IP集和参考设计
6通道与单通道球幕投影系统比较