本帖最后由 jf_73813179 于 2020-12-28 09:44 编辑
因时间仓促,来不及配备对应的水泵、电磁阀、加药泵等外部设备。本Demo使用W IFi IoT 开发板上的2号扩展板(红绿灯演示功能板),以及 oled屏扩展板进行对应功能演示。
已经快到活动截止时间了,目前项目进度仅仅是实现了 LED点灯、初步学习了线程的创建和复用线程函数、点亮OLED屏幕、配置好MQTT代码环境、华为IoT云端设置和初步连接调试。感觉自己在活动期间学习到很多,但是没有能按预期完成项目,非常遗憾。
智能花园喷灌控制系统后期还会再继续完成,目标实现的功能如下:
1、加入一键配网功能及OLED屏上显示wifi连接状态功能。
2、WiFi联网后自动校时、并实时在屏幕上显示 时钟。
3、 手机端显示每天浇水量及施肥、杀虫记录功能。
4、本地定时设备完成定时浇灌及上报数据至华为云功能
项目地址 https://gitee.com/walker2048/hmos_iot
调试设备可以上报数据,但是真正完整的实现用WifiIot连上华为云,还需要攻关3个难点:
1、时间戳获取(不影响上报属性和连接,但是始终不舒服)
2、HMAC-sha256加密功能(不影响上报属性和连接)
3、pahomqtt更改为线程定时上报
我们来看一下Hi3861里,关于点灯的示范代码(applications\sample\wifi-iot\app\iothardware\led_example.c):其中点灯部分的关键内容如下:
include wifiiot_gpio.h 头文件,以wifiiot_gpio这个层面调用GPIO功能。
1
2
| #include "wifiiot_gpio.h"
#include "wifiiot_gpio_ex.h"
|
LedTask函数就是实际执行闪烁LED功能的代码,通过检查g_ledState来设置对应IO的状态,并调用GpioSetOutputVal函数将IO状态设置好。
看到这里,很多小白朋友就会疑惑,为什么要这么麻烦,我直接调用厂商静态库的hi_gpio_set_output_val函数不可以么?当然可以,但是我们不推荐这么做。如果在app中直接调用厂商静态库函数,那假如同一个app想在另一个模组硬件中使用,是不是要改很多对应的代码呢?
这是考虑兼容性才这样封装,以下是图例说明。
代码组织架构可以参见https://gitee.com/openh ARMony/docs/blob/master/readme/%E5%85%AC%E5%85%B1%E5%9F%BA%E7%A1%80README.md
=================================================================================
我们来设想一下:
案例1、假如我们在app中直接调用厂商hal库函数并调试完毕,如果想用这个app直接在另一个模组中使用,无法使用。
案例2、假如我们调用标准IoT外设控制模块 接口,并在厂商目录新建adapter目录适配HAL层接口。那我们在同一个app的情况下,另一个厂商模组只需要再做一遍适配就能马上使用。同时如果更新了厂商HAL库,也只需要更新一下adapter目录适配函数即可。
这样来说,兼容性和代码的可读性不是提升了很多么?
=================================================================================
接下来,我们来完成LED部分的最终代码,首先我们创建一个hardware_config.h头文件,用来定义3个LED灯的全局变量,方便其他程序引用。
enum LedState { LED_ON = 0, LED_OFF, LED_SPARK, };
extern enum LedState g_ledState_R; extern enum LedState g_ledState_G; extern enum LedState g_ledState_Y;
复制代码
接下来,我们在applications/sample/wifi-iot/app/iothardware/BUILD.gn添加刚刚定义的头文件目录
static_library("led_example") { sources = [ "led_example.c" ]
include_dirs = [ "//utils/native/lite/include", "//kernel/liteos_m/components/cmsis/2.0", "//base/iot_hardware/inte RFaces/kits/wifiiot_lite", "//applications/sample/wifi-iot/app/iothardware" ] }
复制代码
最后,将applications/sample/wifi-iot/app/iothardware/led_example.c文件修改成如下内容。
#include
#include
#include "ohos_init.h" #include "cmsis_os2.h" #include "wifiiot_gpio.h" #include "wifiiot_gpio_ex.h"
#include "hardware_config.h"
#define LED_INTERVAL_TIME_US 300000 #define LED_TASK_STACK_SIZE 512 #define LED_TASK_PRIO 25
static void *LedTask(const char *arg) { (void)arg; while (1) { switch (g_ledState_R) { case LED_ON: GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_10, 1); usleep(LED_INTERVAL_TIME_US); break; case LED_OFF: GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_10, 0); usleep(LED_INTERVAL_TIME_US); break; case LED_SPARK: GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_10, 1); usleep(LED_INTERVAL_TIME_US); GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_10, 0); usleep(LED_INTERVAL_TIME_US); break; default: usleep(LED_INTERVAL_TIME_US); break; } switch (g_ledState_G) { case LED_ON: GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_11, 1); usleep(LED_INTERVAL_TIME_US); break; case LED_OFF: GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_11, 0); usleep(LED_INTERVAL_TIME_US); break; case LED_SPARK: GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_11, 1); usleep(LED_INTERVAL_TIME_US); GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_11, 0); usleep(LED_INTERVAL_TIME_US); break; default: usleep(LED_INTERVAL_TIME_US); break; } switch (g_ledState_Y) { case LED_ON: GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_12, 1); usleep(LED_INTERVAL_TIME_US); break; case LED_OFF: GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_12, 0); usleep(LED_INTERVAL_TIME_US); break; case LED_SPARK: GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_12, 1); usleep(LED_INTERVAL_TIME_US); GpioSetOutputVal(WIFI_IOT_IO_NAME_GPIO_12, 0); usleep(LED_INTERVAL_TIME_US); break; default: usleep(LED_INTERVAL_TIME_US); break; } }
return NULL; }
static void LedExampleEntry(void) { osThreadAttr_t attr;
GpioInit(); IoSetFunc(WIFI_IOT_IO_NAME_GPIO_10, WIFI_IOT_IO_FUNC_GPIO_10_GPIO); GpioSetDir(WIFI_IOT_IO_NAME_GPIO_10, WIFI_IOT_GPIO_DIR_OUT); IoSetFunc(WIFI_IOT_IO_NAME_GPIO_11, WIFI_IOT_IO_FUNC_GPIO_11_GPIO); GpioSetDir(WIFI_IOT_IO_NAME_GPIO_11, WIFI_IOT_GPIO_DIR_OUT); IoSetFunc(WIFI_IOT_IO_NAME_GPIO_12, WIFI_IOT_IO_FUNC_GPIO_12_GPIO); GpioSetDir(WIFI_IOT_IO_NAME_GPIO_12, WIFI_IOT_GPIO_DIR_OUT);
attr.name = "LedTask"; attr.attr_bits = 0U; attr.cb_mem = NULL; attr.cb_size = 0U; attr.stack_mem = NULL; attr.stack_size = LED_TASK_STACK_SIZE; attr.priority = LED_TASK_PRIO;
if (osThreadNew((osThreadFunc_t)LedTask, NULL, &attr) == NULL) { printf("[LedExample] Falied to create LedTask!\n"); } }
SYS_RUN(LedExampleEntry);
复制代码
这样一来,我们就完成了交通指示扩展板上LED的功能部署工作,其他的app需要使用LED的话,只需要引用hardware_config.h头文件,并将对应的led状态赋值就可以了。现在我们可以继续做下一步的OLED显示工作啦。
2、使用 Harmony OS 点亮 OLDE屏
首先,我们找到厂商硬件资料,OLED屏的控制 芯片为 ssd1306。我们百度上找一个demo,或者参考( 发烧友联盟上连老师的教程)。将oled.h和oled.c以及oledfont.h复制到applications\sample\wifi-iot\app\iothardware目录下。然后我们在oled.c头文件定义部分添加以下内容:
#include "ohos_init.h" #include "cmsis_os2.h"
#include "wifiiot_i2c.h" #include "wifiiot_i2c_ex.h"
复制代码
接下来,我们将oled.c源程序中I2C部分调用内容替换成以下内容。(其实就是用鸿蒙定义好的标准接口函数和参数替换掉原有函数)。
u32 my_i2c_write(WifiIotI2cIdx id, u16 device_addr, u32 send_len) { u32 status; WifiIotI2cData es8311_i2c_data;
es8311_i2c_data.sendBuf = g_send_data; es8311_i2c_data.sendLen = send_len; status = I2cWrite(id, device_addr, &es8311_i2c_data); if (status != 0) { printf("===== Error: I2C write status = 0x%x! =====\r\n", status); return status; }
return 0; }
/********************************************** // IIC Write Command **********************************************/ void Write_IIC_Command(unsigned char IIC_Command) { g_send_data[0] = 0x00; g_send_data[1] = IIC_Command;
my_i2c_write(WIFI_IOT_I2C_IDX_0, 0x78, 2); } /********************************************** // IIC Write Data **********************************************/ void Write_IIC_Data(unsigned char IIC_Data) { g_send_data[0] = 0x40; g_send_data[1] = IIC_Data;
my_i2c_write(WIFI_IOT_I2C_IDX_0, 0x78, 2); }
复制代码
以及最后初始化I2C部分,在文件最末端。
void my_oled_demo(void) { //初始化 I2cInit(WIFI_IOT_I2C_IDX_0, 100000); /* baudrate: 100000 */
led_init();
OLED_ColorTurn(0); //0正常显示,1 反色显示 OLED_ displayTurn(0); //0正常显示 1 屏幕翻转显示
OLED_ShowString(8, 16, "hello world", 16);
OLED_Refresh(); } #endif
SYS_RUN(my_oled_demo);
复制代码
===========================================================================================
除了修改源码外,我们还需要修改WiFiIoT的io初始化文件,vendor\hisi\hi3861\hi3861\app\wifiiot_app\init\app_io_init.c
将其中I2C引脚定义部分内容修改为以下内容。
#ifdef CONFIG_I2C_SUPPORT /* I2C IO复用也可以选择3/4; 9/10,根据产品设计选择 */ hi_io_set_func(HI_IO_NAME_GPIO_13, HI_IO_FUNC_GPIO_13_I2C0_SDA); hi_io_set_func(HI_IO_NAME_GPIO_14, HI_IO_FUNC_GPIO_14_I2C0_SCL); #endif
复制代码
并修改makefile文件:vendor\hisi\hi3861\hi3861\build\config\usr_config.mk,启用I2C功能
添加一行I2C功能支持宏定义
这样,就基本上完成OLED部分的 测试功能了,我们可以编译源码后烧录到板子上看一下
显示效果如下图。
3、学习 Harmony OS 的 线程调度功能
其实我们在学习led点灯的时候,就已经使用了Harmony OS的线程功能。现在我们可以查看一下线程功能的源码,看看到底有什么。我们可以看到,在app目录的头文件区域,都包含有以下内容
查找了一下源码(在 命令行运行 find -name cmsis_os2.h),我们找到一个有意思的文件。
kernel\liteos_m\components\cmsis\2.0\cmsis_liteos2.c osThreadId_t osThreadNew(osThreadFunc_t func, void *argument, const osThreadAttr_t *attr) { UINT32 uwTid; UINT32 uwRet; LosTaskCB *pstTaskCB = NULL; TSK_INIT_PARAM_S stTskInitParam;
if (OS_INT_ACTIVE) { return NULL; }
if ((attr == NULL) || (func == NULL) || (attr->priority < osPriorityLow1) || (attr->priority > osPriorityAboveNormal6)) { return (osThreadId_t)NULL; }
(void)memset_s(&stTskInitParam, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); stTskInitParam.pfnTaskEntry = (TSK_ENTRY_FUNC)func; #ifndef LITEOS_WIFI_IOT_VERSION stTskInitParam.uwArg = (UINT32)argument; #else stTskInitParam.auwArgs[0] = (UINT32)argument; #endif stTskInitParam.uwStackSize = attr->stack_size; stTskInitParam.pcName = (CHAR *)attr->name; stTskInitParam.usTaskPrio = OS_TASK_PRIORITY_LOWEST - ((UINT16)(attr->priority) - LOS_PRIORITY_WIN); /* 0~31 */
uwRet = LOS_TaskCreate(&uwTid, &stTskInitParam);
if (LOS_OK != uwRet) { return (osThreadId_t)NULL; }
pstTaskCB = OS_TCB_FROM_TID(uwTid);
return (osThreadId_t)pstTaskCB; }
复制代码
在这段代码里,我们不难看出,其实最终调用的还是LiteOS的标准LOS_TaskCreate函数,并且仔细看一下函数,是可以传递参数的。我们在LiteOS官网可以找到对应线程部分的教程代码,并依葫芦画瓢,把Led模块的代码优化一下,通过传递LedSet结构体的形式,复用线程定义函数,这样我们的代码可以更简洁,更简单一些。
typedef struct LedSet { /* data */ enum LedState state; enum WifiIotIoName pin; };
static void *LedTask(const struct LedSet* arg) { while (1) { switch (arg->state) { case LED_ON: GpioSetOutputVal(arg->pin, 1); usleep(LED_INTERVAL_TIME_US); break; case LED_OFF: GpioSetOutputVal(arg->pin, 0); usleep(LED_INTERVAL_TIME_US); break; case LED_SPARK: GpioSetOutputVal(arg->pin, 1); usleep(LED_INTERVAL_TIME_US); GpioSetOutputVal(arg->pin, 0); usleep(LED_INTERVAL_TIME_US); break; default: usleep(LED_INTERVAL_TIME_US); break; } } return NULL; }
复制代码
然后在创建线程的时候,就可以通过传递不同的结构体,点亮不同的Led灯了。
green_led.state = LED_OFF; green_led.pin = WIFI_IOT_IO_NAME_GPIO_10;
if (osThreadNew((osThreadFunc_t)LedTask, &green_led, &attr) == NULL) { printf("[LedExample] Falied to create LedTask!\n"); }
复制代码
=================================================================================
虽然我们调整了结构体定义,但是这样创建3个线程,在物联网MCU的贫乏资源上浪费CPU和RAM,其实并不大好。led模块还可以通过消息队列来进行控制。我们再来学习一下消息队列的使用吧。
4、上云基础知识学习,并使用MQTT接入华为云
WiFi连接部分以及MQTT部分代码参考连老师的代码。我们现在先学习一下上云的一些理论知识。首先、设备上云并不会将完整的业务模型发布到云上,仅需要将重要的关键控制属性发布到云上。其次、可以利用物模型的服务和事件获取到更多的有用信息。
属性是通过设备影子保存在华为云上,同时应用端app(如手机端)中同步的也是设备影子上的数据。另外在设备离线时,应用端修改设备影子后,设备上线时应同步设备影子数据,与云端保持一致。
设备鉴权注意内容(以TypeScript代码为示范,代码地址https://gitee.com/walker2048/mqttclient/tree/master)
成功上报后的截图:
mqtt.h内容,将服务器信息,设备端信息等独立出来
#define publish_message "{\"publish\":{\"services\":[{\"service_id\":\"SmartPumpServer\",\"properties\":{\"walterPumpSwitch\":\"on\",\"pesticidePumpSwitch\":\"on\",\"manurePumpSwitch\":\"on\",\"manureLevels\":600,\"pesticideLevel\":1000},\"event_time\":\"\"}]}}" #define Hi_cloudHost "a15fbdc9af.iot-mqtts.cn-north-4.myhuaweicloud.com" #define Hi_deviceId "5fc9a576b4ec2202e9a32615_testDevice" //设备ID #define Hi_deviceSecret "" //设备secret #define Hi_clientId "5fc9a576b4ec2202e9a32615_testDevice_0_0_2020120512" //鉴权用ClientID #define Hi_password "1ded5f4731ab1b097edfac633a3f1cea21ab2d14ef02e1c5643fb4e08cd9415e" //鉴权用password
#define publishTopic "$oc/devices/5fc9a576b4ec2202e9a32615_testDevice/sys/messages/up"
void mqtt_test(void);
#endif /* __MQTT_TEST_H__ */
复制代码
mqtt.c实现内容
#include
#include
#include "ohos_init.h" #include "cmsis_os2.h"
#include #include "hi_wifi_api.h" //#include "wifi_sta.h" #include "lwip/ip_addr.h" #include "lwip/netifapi.h"
#include "lwip/sockets.h"
#include "MQTTPacket.h" #include "transport.h" #include "mqtt.h"
int toStop = 0;
int mqtt_connect(void) {
MQTTPacket_connectData data = MQTTPacket_connectData_initializer; int rc = 0; int mysock = 0; unsigned char buf[200]; int buflen = sizeof(buf); int msgid = 1; MQTTString topicString = MQTTString_initializer; int req_qos = 0; char *payload = ""; int payloadlen = strlen(payload); int len = 0; char *host = Hi_cloudHost; int port = 1883;
mysock = transport_open(host, port); if (mysock < 0) return mysock;
printf("Sending to hostname %s port %d\n", host, port);
data.clientID.cstring = Hi_clientId; data.keepAliveInterval = 20; data.cleansession = 1; data.username.cstring = Hi_deviceId; data.password.cstring = Hi_password;
len = MQTTSerialize_connect(buf, buflen, &data); rc = transport_sendPacketBuffer(mysock, buf, len);
/* wait for connack */ if (MQTTPacket_read(buf, buflen, transport_getdata) == CONNACK) { unsigned char sessionPresent, connack_rc;
if (MQTTDeserialize_connack(&sessionPresent, &connack_rc, buf, buflen) != 1 || connack_rc != 0) { printf("Unable to connect, return code %d\n", connack_rc); goto exit; } } else goto exit; /* loop getting msgs on subscribed topic */ topicString.cstring = "$oc/devices/5fc9a576b4ec2202e9a32615_testDevice/sys/properties/report"; /* transport_getdata() has a built-in 1 second timeout, your mileage will vary */ if (MQTTPacket_read(buf, buflen, transport_getdata) == PUBLISH) { unsigned char dup; int qos; unsigned char retained; unsigned short msgid; int payloadlen_in; unsigned char *payload_in; int rc; MQTTString receivedTopic; rc = MQTTDeserialize_publish(&dup, &qos, &retained, &msgid, &receivedTopic, &payload_in, &payloadlen_in, buf, buflen); printf("message arrived %.*s\n", payloadlen_in, payload_in);
rc = rc; }
printf("publishing device message\n"); len = MQTTSerialize_publish(buf, buflen, 0, 0, 0, 0, topicString, (unsigned char *)payload, payloadlen); rc = transport_sendPacketBuffer(mysock, buf, len);
printf("disconnecting\n"); len = MQTTSerialize_disconnect(buf, buflen); rc = transport_sendPacketBuffer(mysock, buf, len); exit: transport_close(mysock);
rc = rc;
return 0; }
void mqtt_test(void) { mqtt_connect(); }
复制代码
5、使用 Harmony OS 的 KV 组件实现本地数据 存储
6、使用 Harmony OS 的 RTC 实时时钟功能实现定时管理
使用lwip的精简校时功能third_party\lwip\src\apps\sntp\sntp.c
功能未经测试,udp_recv函数未实现
#include #include #include "ohos_init.h" #include "cmsis_os2.h"
#include "sntp.h" #include "sntp_opts.h" #include "hi_time.h"
uint32_t sntp_time;
void init_sntp(void) { uint32_t time = hi_get_real_time();
//加入授时中心的IP信息 sntp_setservername(0, "ntp1.aliyun.com"); //设置 SNTP 的获取方式 -> 使用向服务器获取方式 sntp_setoperatingmode(SNTP_OPMODE_POLL); //SNTP 初始化 sntp_init(); }
void sntp_init(void) { SNTP_RESET_RETRY_TIMEOUT();
//创建udp,用于接收udp包,时间数据 int sntp_ PCB = udp_new(); LWIP_ASSERT("Failed to allocate udp pcb for sntp client", sntp_pcb != NULL); if (sntp_pcb != NULL) {
//有数据,处理接收数据,同步到本地,由sntp_recv处理。 udp_recv(sntp_pcb, sntp_set_time, NULL); sntp_request(NULL); } }
void sntp_set_time() { if (sntp_time == 0) { print_log("sntp_set_time: wrong!@@\n"); return; }
print_log("sntp_set_time: c00, enter!\n"); print_log("sntp_set_time: c01, get time = %u\n", sntp_time);
struct tm *time; struct tm *sTime;
sntp_time += (8 * 60 * 60); ///北京时间是东8区需偏移8小时
time = localtime(&sntp_time); /* * 设置 RTC 的 时间 */
hi_set_real_time(time);
print_log("sntp_set_time: c02, decode time: 20%d-%02d-%02d %d:%d:%d\n", time->tm_year, time->tm_mon, time->tm_mday, time->tm_hour, time->tm_min, time->tm_sec);
print_log("sntp_set_time: c03, test get = %u\n", get_timestamp()); print_log("sntp_set_time: c04, set rtc time done\n"); }
复制代码
作者:忙碌的死龙
0
|
|
|
|