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 的 USBType-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®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 制作的项目都能实现、也都可以拥有这些功能。如果你们感兴趣,也可以自己动手试试!
-
物联网
+关注
关注
2909文章
44625浏览量
373228 -
LoRa
+关注
关注
349文章
1689浏览量
231938
发布评论请先 登录
相关推荐
评论