本帖最后由 jf_73813179 于 2020-12-28 09:44 编辑
因时间仓促,来不及配备对应的水泵、电磁阀、加药泵等外部设备。本Demo使用WiFi 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里,关于点灯的示范代码(applicationssamplewifi-iotappiothardwareled_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/interfaces/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复制到applicationssamplewifi-iotappiothardware目录下。然后我们在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! =====rn", 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初始化文件,vendorhisihi3861hi3861appwifiiot_appinitapp_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文件:vendorhisihi3861hi3861buildconfigusr_config.mk,启用I2C功能
添加一行I2C功能支持宏定义
这样,就基本上完成OLED部分的测试功能了,我们可以编译源码后烧录到板子上看一下
显示效果如下图。
3、学习 Harmony OS 的 线程调度功能
其实我们在学习led点灯的时候,就已经使用了Harmony OS的线程功能。现在我们可以查看一下线程功能的源码,看看到底有什么。我们可以看到,在app目录的头文件区域,都包含有以下内容
查找了一下源码(在命令行运行 find -name cmsis_os2.h),我们找到一个有意思的文件。
kernelliteos_mcomponentscmsis2.0cmsis_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 %dn", 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 %dn", 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 %.*sn", payloadlen_in, payload_in);
rc = rc;
}
printf("publishing device messagen");
len = MQTTSerialize_publish(buf, buflen, 0, 0, 0, 0, topicString, (unsigned char *)payload, payloadlen);
rc = transport_sendPacketBuffer(mysock, buf, len);
printf("disconnectingn");
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_partylwipsrcappssntpsntp.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 = %un", 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:%dn",
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 = %un", get_timestamp());
print_log("sntp_set_time: c04, set rtc time donen");
}
复制代码
作者:忙碌的死龙
0