如何使用RUI3制作一款用于电脑的多功能LoRa®适配器

描述

LoRa® 和 LoRaWAN® 已经成为了物联网世界的重要技术,也向人们提供了诸多易于使用的远程通信解决方案。在这过程中电脑设备却被忽略了,我们会发现带有 LoRa® 模块的笔记本电脑很少见。

 

现在这种局面陆续得到了改善,在一些解决方案中,已经开始出现用于笔记本电脑的 LoRa® 模块了。最近笔者利用瑞科慧联的低代码开发平台 RUI3 制作了一个 LoRa® USB 适配器,它可以直接连接到笔记本电脑或树莓派上。大多数时候,这个适配器可以作为收发器用于家居场景种;但它也作为一个方便测试的平台,比如:远程用笔记本电脑发送命令、记录结果等等。

 

LoRa

 

使用瑞科慧联的模块化硬件开发平台 WisBlock,让这样的应用开发变得更加简单。笔者通过 WisBlock 制作了两种适配器,一种是使用计算机上的自定义软件来管理 LoRa® 模块的 AT 固件,另一种是直接在 LoRa® 模块上完成大部分工作。在这两种适配器中,电脑都是作为终端来使用。今天要介绍的是后一种适配器,主要就是使用 RUI3 为 LoRa® 通信模块 RAK4631-R 制作一个简单的自定义固件。

一、前期准备

  • 硬件

1、选择 RAK4631-R(不同国家或地区对应频率的频段不同)。

 

LoRa(注意,这里我们也可以使用另一款通信模块 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<200> 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 < iy; ix++) { char c = str[ix]; // lowercase the keyword if (c >= '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 < 150.0 || 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

 

一切操作之后有了现在的结果,编码历时几个小时,就得到了一个功能齐全的 LoRa® USB 适配器。但实际上没有用这么多时间,因为笔者重用了以前项目中的 Commands.h 代码,并且暂时跳过 AES 加密部分,把它留在示例项目中是因为它相对比较复杂,且通常不是简单项目的一部分。通常可以在项目正常运行后再添加 AES,这样就不必担心其他东西会受影响。但是,就像 Commands.h 一样,笔者已经从其他项目准备好 AES 文件,所以对它的实现也只是复制粘贴工作。

 

LoRa

六、扩展

功能蔓延(feature creep)一直都是困扰开发人员的问题,但现在我们暂时可以先忽略这一点。一起来看看这个项目可以有哪些扩展:

1、OLED 显示屏

由于引脚配置,显示屏要在底板背面添加,但添加起来也是很方便。学习一些如何关闭屏幕的编程代码,可以帮助节省能源和保护屏幕;

2、RTC 实时时钟

可以在 JSON 数据包或类似 Cayenne LPP 的格式中为数据包添加时间戳;

3、GNSS 模块

用户可以将 GPS 坐标添加到数据包中,而且如果已经在家中设置了收发器的坐标,还可以使用它们的自动计算距离(Haversine 公式)的功能。

4、固件的 BLE UART 路由

添加这个功能很简单。一旦设置了 BLE,代码就与串行代码几乎相同了。这样操作之后,它就不仅仅是一个用于电脑的 USB LoRa® 适配器了,加上电池它可以成为手机无线 LoRa® 适配器。

 

以上这些,这个使用 RUI3 制作的项目都能实现、也都可以拥有这些功能。如果你们感兴趣,也可以自己动手试试!

 

LoRa

 

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分