自制鸿蒙Neptune开发板实时更新温湿度到手机

描述

好久不见!最近在研究 OpenHarmony,经过一番折腾,终于打通了南向和北向开发。

如下:

自己做了一个鸿蒙开发板

搞定了 HT30 温湿度计的驱动

通过 UDP 广播数据

让我们一起看看效果吧!

自制的 Neptune 开发板实时更新温湿度到手机!

这个是我自己做的鸿蒙开发板,里面的核心是 Neptune Wi-Fi 蓝牙模块,通过 IIC 通信连接了一块 0.96 寸的 OLED 显示屏以及一个 HT30 温湿度传感器。另外,这块开发板还包括 3 颗 LED 灯,以及相关的串口通信模块等。

看看这块 OLED 显示屏下面写的什么?

嘻嘻是的!Of course,I Still Love You!致敬一下 StarShip!当然,还有 Powered By OpenHarmony!这个必须有!

接下来,给大家介绍一下这个功能的整个实现过程。

设计开发板

开发板的设计参考了瑞和官方 Neptune 开发板的原理图。电源模块和串口通信模块基本没有什么改动。

原理图贡献给大家:

OLED

这里的温度传感器模块用的是 HT30。然后,就是打样板了:

OLED

真的很不容易,被我干翻的板子已经堆成堆了!唉,只能怪自己脑子进水设计失误,加上焊接技术有点弱。

设计应用程序

①关于 HT30 的驱动程序

由于官方提供的例程是 AHT20 的温度传感器的驱动。所以这里还需要针对 HT30 的数据手册对驱动程序做出一些修改。

看了一下数据手册。除了 HT30 的 I2C 的地址和 AHT20 不同,温湿度的数据读取模式也更加复杂,数据的位数也不同。

因此,设计 HT30 的 I2C 的通信时需要注意一下几个方面:

温度数据是由 16bit 的数据位和 8bit 的 CRC 位组成。湿度数据也是一样的。相比之下,AHT20 的温湿度数据都是 20bit,而且没有 CRC 校验。

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!

”, i, HT30_MAX_RETRY);

}

//

if (i 》= HT30_MAX_RETRY) {

printf(“HT30 device always busy!

”);

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

”, humiRaw, *humi, tempRaw, *temp);

return WIFI_IOT_SUCCESS;

}

这里的温度和湿度的转化公式为:

这样驱动程序就设计好了。

②关于 OLED 的驱动

这里用的是 0.92 寸的 OLED 屏幕,这块屏幕在 Hi3861 的代码中是用现成的驱动程序的。所以就不需要自己设计了。

分辨率为 128*64。在官方的驱动程序中,这块 OLED 有两种显示模式:8*16 点阵和 6*8 的点阵。

③选用 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 25enum 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!

”);

GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE0);

osDelay(500);

break;

case LED_OFF:

printf(“ LED_OFF!

”);

GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE1);

osDelay(500);

break;

case LED_SPARK:

printf(“ LED_SPARK!

”);

GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE0);

osDelay(500);

printf(“ LED_SPARK!2

”);

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

”);

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!

”);

goto do_cleanup;

}

// I2C和OLED的初始化。

if (I2cInit(WIFI_IOT_I2C_IDX_0, 200*1000)) {

printf(“HT30 test i2c init failed

”);

}

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

”, retval);

float temp = 0.0, humi = 0.0;

retval = HT30_GetMeasureResult(&temp, &humi);

printf(“HT30_GetMeasureResult: %d, temp = %.2f, humi = %.2f

”, 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!

”);

goto do_cleanup;

}

printf(“send UDP message {%s} %ld done!

”, udpmessage, retval);

// 延时1秒

osDelay(500);

}

do_cleanup:

printf(“do_cleanup.。.

”);

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!

”);

}

// 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!

”);

}

}

APP_FEATURE_INIT(HT30Test);

阅读代码时可以注意一下两点:

在 HT30Test 函数中创建了 2 个线程,分别是 HT30TestTask 和 GpioTask。前者用于温湿度测量,后者用于闪烁 LED 灯。GpioTask 没啥用,只是为了好看而已,各位可以删掉他没有关系。

HT30TestTask 中,最终将温湿度数据以 UDP 的消息发送给 UDP 服务器(也就是手机),而这个数据进行了一次粗包装:一共是 6 位,前 4 位表示温度,后四位表示湿度。

例如,“374267”表示 37.42℃ 和相对湿度 67%。这样,后期鸿蒙应用程序拿到数据后就好处理了。

鸿蒙应用程序的开发

在应用程序端,这里充当了 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();

}

}

});

这段代码比较简单:

需要通过 getGlobalTaskDispatcher 获取全局任务分发器,然后通过异步方法进行网络连接,否则会抛出 NetworkOnMainThreadException 异常。

获得到 UDP 报文数据后,通过字符串裁剪和类型转化等方式将其转换为浮点型或整型,然后显示在 mText 组件上。

总结

我自己做的开发板成本是很低的,温湿度传感器、OLED 屏幕和 Neptune 模组都是以很低的价格在网上购买的,总成本可能不超过 30 元。这个开发板很小,可以握持在手中随身携带。

不过,在软件方面,上面的例子充其量算一个 Demo,实际上还有很多工作需要做:

①这里是直接通过 UDP 将开发板和手机连接在一起的,其中的 IP 地址也是硬写入的。所以如果离开 WiFi 环境,那么手机将不会接收到温湿度信息。

如果开发者希望远程获得温湿度,那么需要服务器进行中转。这个中转技术也不复杂,大家可以思考一下如何实现。

②在应用端,这里的温湿度是写在 MainAbilitySlice 中的。其实这种方式也是有待改进的。

至少需要将相关的业务代码写到服务中,这样的话,我们还可以实现高温预警等功能。如果将其以小卡片的形式显示在桌面就更好啦!同样,大家可以思考一下如何实现。

③这块开发板可以进一步微型化,请大家期待下一个版本!

④在获取温湿度数据的时候,我们用了低重复率和关闭 clock stretching 功能。

其实,真正实用化的时候,根据场景的不同大家需要考虑如何配置一下,提高精度的同时降低功耗!

代码:

https://gitee.com/dongyu1009/neptune-harmony-os-wi-fi-link

视频演示:

https://harmonyos.51cto.com/show/8232

在这里,为大家贡献了实例代码和开发板的原理图!如果希望进一步研究,点击“阅读原文”来一起探究竟吧!责任编辑:haq

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

全部0条评论

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

×
20
完善资料,
赚取积分