物联网实战:Neptune温湿度计!成本30元,鸿蒙手机知晓家中情况! 原创 精华
大家好!!我是HarmonyOS社区核心讲师、HarmonyOS KOL董昱。
好久不见!最近在研究OpenHarmony,经过一番折腾,终于打通了南向和北向开发:
- 自己做了一个鸿蒙开发板
- 搞定了HT30温湿度计的驱动
- 通过UDP广播数据
让我们一起看看效果吧!
这个是我自己做的鸿蒙开发板,里面的核心是Neptune Wi-Fi蓝牙模块,通过IIC通信连接了一块0.96寸的OLED显示屏以及一个HT30温湿度传感器。另外,这块开发板还包括3颗LED灯,以及相关的串口通信模块等。
看看这块OLED显示屏下面写的什么?
嘻嘻是的!Of course, I Still Love You! 致敬一下StarShip!
当然,还有Powered By OpenHarmony!这个必须有!
接下来,给大家介绍一下这个功能的整个实现过程。
1、设计开发板
开发板的设计参考了瑞和官方Neptune开发板的原理图。电源模块和串口通信模块基本没有什么改动。
原理图贡献给大家!
这里的温度传感器模块用的是HT30。
然后,就是打样板了
真的很不容易,被我干翻的板子已经堆成堆了!唉,只能怪自己脑子进水设计失误,加上焊接技术有点弱。
2、设计应用程序。
2.1 关于HT30的驱动程序
由于官方提供的例程是AHT20的温度传感器的驱动。所以这里还需要针对HT30的数据手册对驱动程序做出一些修改。
看了一下数据手册。除了HT30的I2C的地址和AHT20不同,温湿度的数据读取模式也更加复杂,数据的位数也不同。因此,设计HT30的I2C的通信时需要注意一下几个方面:
(1)温度数据是由16bit的数据位和8bit的CRC位组成。湿度数据也是一样的。相比之下,AHT20的温湿度数据都是20bit,而且没有CRC校验。
(2)HT30可以开启clock stretching模式。这个模式开启与否和重复率的设置这个会影响到转换时间、精度和功耗。
根据这些差异,我自己对AHT20的驱动做出了一些修改,形成了HT30的驱动。
首先,设置一下HT30的地址:
#define HT30_DEVICE_ADDR 0x44
#define HT30_READ_ADDR ((HT30_DEVICE_ADDR<<1)|0x1)
#define HT30_WRITE_ADDR ((HT30_DEVICE_ADDR<<1)|0x0)
然后,设置MSB和LSB。
#define HT30_CMD_MSB 0x24 // 关闭Clock stretching
#define HT30_CMD_LSB 0x16 // 低重复率
这里用的是低重复率和关闭Clock stretching,这是为了测试的时候让代码更加的简单。童鞋们需要根据自己的实际使用情况做出修改。
最后,设计开始测量和接受测量结果的代码。
// 开始测量
uint32_t HT30_StartMeasure(void)
{
uint8_t clibrateCmd[] = {HT30_CMD_MSB, HT30_CMD_LSB}; 设置MSB和LSB
return HT30_Write(clibrateCmd, sizeof(clibrateCmd));
}
// 接收测量结果,拼接转换为标准值
uint32_t HT30_GetMeasureResult(float* temp, float* humi)
{
uint32_t retval = 0, i = 0;
if (temp == NULL || humi == NULL) {
return WIFI_IOT_FAILURE;
}
// 获得的返回数据
uint8_t buffer[HT30_STATUS_RESPONSE_MAX];
memset(&buffer, 0x0, sizeof(buffer));
for (i = 0; i < HT30_MAX_RETRY; i++) {
osDelay(HT30_MEASURE_TIME);
retval = HT30_Read(buffer, sizeof(buffer)); // recv status command result
if (retval == WIFI_IOT_SUCCESS) {
break;
}
printf("HT30 device busy, retry %d/%d!\r\n", i, HT30_MAX_RETRY);
}
//
if (i >= HT30_MAX_RETRY) {
printf("HT30 device always busy!\r\n");
return WIFI_IOT_FAILURE;
}
// 获得温度数据
uint32_t tempRaw = buffer[0];
tempRaw = (tempRaw << 8) | buffer[1];
*temp = tempRaw / (float)HT30_RESOLUTION * 175 - 45;
// 获得湿度数据
uint32_t humiRaw = buffer[3];
humiRaw = (humiRaw << 8) | buffer[4];
*humi = humiRaw / (float)HT30_RESOLUTION * 100;
printf("humi = %04X, %f, temp= %04X, %f\r\n", humiRaw, *humi, tempRaw, *temp);
return WIFI_IOT_SUCCESS;
}
这里的温度和湿度的转化公式为:
这样驱动程序就设计好了。
2.2 关于OLED的驱动。
这里用的是0.92寸的OLED屏幕,这块屏幕在Hi3861的代码中是用现成的驱动程序的。所以就不需要自己设计了。
分辨率为128*64。在官方的驱动程序中,这块OLED有两种显示模式:
8*16点阵和6*8的点阵。
2.3 选用TCP还是UDP连接
Neptune是一款WiFi蓝牙模块,这里就通过WiFI和我们的手机建立连接。连接的方式有两种,分别是TCP和UDP。由于我们的数据并没有敏感数据,而且丢失其实也不会造成太大影响,因此这里选用了更加简单的UDP。UDP实际上是可以进行广播的,如果有多个设备需要接受温湿度数据的话其实不需要单独的建立连接,所以更加适合这个场景。
最后,给大家看下最终的业务代码。
#include "ht30.h"
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "wifiiot_gpio.h"
#include "wifiiot_gpio_ex.h"
#include "wifiiot_i2c.h"
#include "wifiiot_gpio_w800.h"
#include "oled_ssd1306.h"
#include "net_params.h"
#include "wifi_connecter.h"
#include "net_common.h"
#define LED_TASK_STACK_SIZE 512
#define LED_TASK_PRIO 25
enum LedState {
LED_ON = 0,
LED_OFF,
LED_SPARK,
};
enum LedState g_ledState = LED_SPARK;
static void* GpioTask(const char* arg)
{
(void)arg;
while (1) {
switch (g_ledState) {
case LED_ON:
printf(" LED_ON! \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE0);
osDelay(500);
break;
case LED_OFF:
printf(" LED_OFF! \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE1);
osDelay(500);
break;
case LED_SPARK:
printf(" LED_SPARK! \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE0);
osDelay(500);
printf(" LED_SPARK!2 \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE1);
osDelay(500);
break;
default:
osDelay(500);
break;
}
}
return NULL;
}
static void GpioIsr(char* arg)
{
(void)arg;
enum LedState nextState = LED_SPARK;
printf(" GpioIsr entry\n");
GpioSetIsrMask(WIFI_IOT_GPIO_PB_07, 0);
switch (g_ledState) {
case LED_ON:
nextState = LED_OFF;
break;
case LED_OFF:
nextState = LED_ON;
break;
case LED_SPARK:
nextState = LED_OFF;
break;
default:
break;
}
g_ledState = nextState;
}
void HT30TestTask(void* arg)
{
(void) arg;
int times = 0;
uint32_t retval = 0;
WifiDeviceConfig config = {0};
// 准备AP的配置参数, 连接WiFi
strcpy(config.ssid, PARAM_HOTSPOT_SSID);
strcpy(config.preSharedKey, PARAM_HOTSPOT_PSK);
config.securityType = PARAM_HOTSPOT_TYPE;
osDelay(10);
int netId = ConnectToHotspot(&config);
// 建立UDP连接,这里充当了UDP的客户端
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDP socket
struct sockaddr_in toAddr = {0};
toAddr.sin_family = AF_INET;
toAddr.sin_port = htons(PARAM_SERVER_PORT); // 端口号,从主机字节序转为网络字节序
if (inet_pton(AF_INET, PARAM_SERVER_ADDR, &toAddr.sin_addr) <= 0) { // 将主机IP地址从“点分十进制”字符串 转化为 标准格式(32位整数)
printf("inet_pton failed!\r\n");
goto do_cleanup;
}
// I2C和OLED的初始化。
if (I2cInit(WIFI_IOT_I2C_IDX_0, 200*1000)) {
printf("HT30 test i2c init failed\n");
}
OledInit();
OledFillScreen(0x00);
OledShowString(0, 0, "** HarmonyOS! **", 1);
osDelay(400);
OledShowString(0, 1, "** HarmonyOS! **", 1);
OledShowString(0, 2, "****************", 1);
OledShowString(0, 3, "****************", 1);
// 每秒测量一次温湿度数据
while (1) {
retval = HT30_StartMeasure();
printf("HT30_StartMeasure: %d\r\n", retval);
float temp = 0.0, humi = 0.0;
retval = HT30_GetMeasureResult(&temp, &humi);
printf("HT30_GetMeasureResult: %d, temp = %.2f, humi = %.2f\r\n", retval, temp, humi);
times++;
// 将温湿度数据显示在OELD屏幕上
static char line1[32] = {0};
snprintf(line1, sizeof(line1), "** times = [%d]", times);
OledShowString(0, 1, line1, 1);
static char line2[32] = {0};
snprintf(line2, sizeof(line2), "** temp : %.2f", temp);
OledShowString(0, 2, line2, 1);
static char line3[32] = {0};
snprintf(line3, sizeof(line3), "** humi : %d", (int)humi);
OledShowString(0, 3, line3, 1);
// 将温湿度数据作为UDP的消息发送给手机
static char udpmessage[7] = {0};
snprintf(udpmessage, sizeof(udpmessage), "%04d%02d", (int)(temp*100), (int)humi);
// UDP socket 是 “无连接的” ,因此每次发送都必须先指定目标主机和端口,主机可以是多播地址
retval = sendto(sockfd, udpmessage, sizeof(udpmessage), 0, (struct sockaddr *)&toAddr, sizeof(toAddr));
if (retval < 0) {
printf("sendto failed!\r\n");
goto do_cleanup;
}
printf("send UDP message {%s} %ld done!\r\n", udpmessage, retval);
// 延时1秒
osDelay(500);
}
do_cleanup:
printf("do_cleanup...\r\n");
close(sockfd);
}
void HT30Test(void)
{
GpioInit();
GpioSetDir(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_DIR_OUTPUT); // output is 0 PB08 control led
GpioSetDir(WIFI_IOT_GPIO_PB_07, WIFI_IOT_GPIO_DIR_INPUT); // input is PB09
IoSetPull(WIFI_IOT_GPIO_PB_07, WIFI_IOT_GPIO_ATTR_PULLHIGH);
GpioRegisterIsrFunc(WIFI_IOT_GPIO_PB_07, WIFI_IOT_INT_TYPE_EDGE, WIFI_IOT_GPIO_EDGE_FALL_LEVEL_LOW, GpioIsr, NULL);
// 温湿度测量线程
osThreadAttr_t attr;
attr.name = "HT30Task";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 4096;
attr.priority = osPriorityNormal;
if (osThreadNew(HT30TestTask, NULL, &attr) == NULL) {
printf("[HT30Test] Failed to create HT30TestTask!\n");
}
// OLED闪烁线程
osThreadAttr_t attr2;
attr2.name = "HT30Task2";
attr2.attr_bits = 0U;
attr2.cb_mem = NULL;
attr2.cb_size = 0U;
attr2.stack_mem = NULL;
attr2.stack_size = 4096;
attr2.priority = osPriorityNormal;
if (osThreadNew(GpioTask, NULL, &attr2) == NULL) {
printf("[HT30Test] Failed to create HT30TestTask2!\n");
}
}
APP_FEATURE_INIT(HT30Test);
阅读代码时可以注意一下两点:
(1)在HT30Test函数中创建了2个线程,分别是HT30TestTask和GpioTask。前者用于温湿度测量,后者用于闪烁LED灯。GpioTask没啥用,只是为了好看而已,各位可以删掉他没有关系。
(2)HT30TestTask中,最终将温湿度数据以UDP的消息发送给UDP服务器(也就是手机),而这个数据进行了一次粗包装:一共是6位,前4位表示温度,后四位表示湿度。例如,“374267”表示37.42℃和相对湿度67%。这样,后期鸿蒙应用程序拿到数据后就好处理了。
3. 鸿蒙应用程序的开发。
在应用程序端,这里充当了UDP服务器。使用Java 的API进行开发的。
getGlobalTaskDispatcher(TaskPriority.DEFAULT).asyncDispatch(new Runnable() {
@Override
public void run() {
try {
// 要接收的报文
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
// 创建socket并指定端口
DatagramSocket socket = new DatagramSocket(5678);
while (true) {
// 接收socket客户端发送的数据。如果未收到会一致阻塞
socket.receive(packet);
String receiveMsg = new String(packet.getData(),0,packet.getLength());
System.out.println("packet:" + packet.getLength());
System.out.println("packet:" + receiveMsg);
getMainTaskDispatcher().asyncDispatch(new Runnable() {
@Override
public void run() {
long number = Long.parseLong(receiveMsg.substring(0, 6));
float temp = ((float)(number / 100)) / 100;
long humi = number % 100;
mText.setText("温度:" + temp + " 湿度:" + humi);
}
});
}
// 关闭socket
// socket.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
});
这段代码比较简单。
(1)需要通过getGlobalTaskDispatcher获取全局任务分发器,然后通过异步方法进行网络连接,否则会抛出NetworkOnMainThreadException异常。
(2)获得到UDP报文数据后,通过字符串裁剪和类型转化等方式将其转换为浮点型或整型,然后显示在mText组件上。
4. 总结
我自己做的开发板成本是很低的,温湿度传感器、OLED屏幕和Neptune模组都是以很低的价格在网上购买的,总成本可能不超过30元。这个开发板很小,可以握持在手中随身携带。
不过,在软件方面,上面的例子充其量算一个Demo,实际上还有很多工作需要做:
(1)这里是直接通过UDP将开发板和手机连接在一起的,其中的IP地址也是硬写入的。所以如果离开WiFI环境,那么手机将不会接收到温湿度信息。如果开发者希望远程获得温湿度,那么需要服务器进行中转。这个中转技术也不复杂,大家可以思考一下如何实现。
(2)在应用端,这里的温湿度是写在MainAbilitySlice中的。其实这种方式也是有待改进的。至少需要将相关的业务代码写到服务中,这样的话,我们还可以实现高温预警等功能。如果将其以小卡片的形式显示在桌面就更好啦!同样,大家可以思考一下如何实现。
(3)这块开发板可以进一步微型化,请大家期待下一个版本!
(4)在获取温湿度数据的时候,我们用了低重复率和关闭clock stretching功能。其实,真正实用化的时候,根据场景的不同大家需要考虑如何配置一下,提高精度的同时降低功耗!
代码:https://gitee.com/dongyu1009/neptune-harmony-os-wi-fi-link
视频演示:https://harmonyos.51cto.com/show/8232
在这里,为大家贡献了实例代码和开发板的原理图!如果希望进一步研究,来直播课一探究竟吧!
不说了,我先上车了
谢谢支持
真大佬!这也太牛了吧!!!
厉害~~
不错的帖子
这是真的硬核!
“唉,只能怪自己脑子进水设计失误,加上焊接技术有点弱。”
祝早日成为六边形战士!!
好久不见。
董老师 牛逼
自己做的板子不错,再做个好看的壳,直接成品卖了
哈哈哈,争取早日成为一个⚪。
候老师,这个是您启发和教给我的哈哈。
干货,三连!
我的祖传代码 忘了给你 惭愧
支持作者,学习学习~
辛苦了,良品率这么低,这个成本很高啊。
哈哈哈,被动提高成本