小项目背景
鸿蒙系统也听说发布一段时间了,一直没去了解。但是勒,既然是华为发布的项目,还是想找个时间了解下的。刚好在公众号看到免费申请鸿蒙开发板试用,那就顺便学习下吧。
鸿蒙系统的特点是万物互联,那么申请的开发板我也准备用来做一个物联网小项目,至于做啥呢?...思量许久,感觉无论用来物联网控制啥东西,都需要额外成本,那就暂时放弃外设控制的想法,先完成简单的Android手机与开发板通过MQTT互联吧。
简述
本项目主要涉及如下方面:
1. XR806的单片机固件开发
XR806的开发主要涉及鸿蒙LiteOS、XR806本身的SDK包、MQTT,在本贴中,会较详细地讲述。
在MQTT的实现上,我一直在纠结,本人之前开发单片机程序的时候其实是没接触过LWIP和MQTT这部分涉及到嵌入式网络相关的,因此在开发前是有一部分时间去了解MQTT的,目前我看到鸿蒙相关的开发板都是使用基于Paho的开源MQTT(可能是我没找到其他的哈),但是我发现LWIP本身是有实现MQTT客户端的源码的,既然如此,为啥不直接用LWIP本身的呢?MQTT是应用层的协议,因此移植其实特别简单,基于传输层的TCP移植就ok了。不过,如果是使用LWIP本身实现的MQTT,那很可能连移植都不需要去做,因此,我打算直接使用LWIP内部的MQTT客户端实现。
2. Android的App开发
Android的开发也很简单,主要是搭建个简单的界面,并通过开源的MQTT库实现MQTT客户端。本贴中也会讲述这部分内容。
本贴中,Android的MQTT是基于Paho的开源库实现的。
整体框架
如上图,项目实现的MQTT的传输流程便是如此。本贴是在Ubuntu搭建的MQTT服务器,Android手机订阅“sensor_data”主题的消息,并且会发送“sensor_cmd”主题的信息;XR806订阅“sensor_cmd”主题的信息,并且会发送定时发送“sensor_data”的信息,这样就达到一个Android手机用户获取传感器数据并且还能实现控制设备(比如控灯)的目的。
XR806开发
1. 鸿蒙系统的了解
关于鸿蒙的源码如何编译,恩...还是自行查资料吧,其实个人感觉这个编译系统类似android的编译,初学者多踩踩坑是可以很容易上手的。
通过查看鸿蒙的官网资料,在这里说下本人的一些理解:
首先,像XR806这种单片机开发板使用的是鸿蒙的轻量型系统(LiteOS_m),鸿蒙还有小型系统和标准系统,小型系统应该是用在imx6ull这种级别的芯片上,而标准系统就是用在手机芯片这种级别。
既然我们用的是LiteOS_m,那么就把关注范围缩小到LiteOS_m上,主要是LiteOS_m的内容比较少。
我们先看鸿蒙的目录结构,如下:
通过这个目录,我们其实大致可以猜到以下(仅提供参考):
-- applications是存放应用的位置;
-- driver应该是存放驱动的位置,但其实和我们无关,因为该目录存放的是针对小型系统(LiteOS_a)的驱动(HDI接口);
-- base存放鸿蒙系统中无关平台无关硬件的通用基础构成(不包含内核),比如基本外设的中间件接口、关于安全的源码、代码升级相关的源码,当然,可能并不适用于LiteOS_m系统,或者说不全适用;
-- build和developtools存放编译系统源码工具和开发相关工具;
-- device应该是存放具体不同设备的特定源码,也就是不同的设备型号需要适配的部分;
-- vendor应该要存放不同厂商芯片的特定源码,也就是不同系列芯片需要适配的部分;
-- out目录存放编译输出文件;
-- third_party存放第三方源码和库;
理论上,通过鸿蒙系统可以实现:无论哪个系列的芯片,哪个型号的设备,只要适配了鸿蒙系统,那么编写出来的应用程序就是通用的。
这是因为鸿蒙在应用和具体厂商设备之间提供了中间层,因此,对于应用层来说,如果底层适配完成,只需要调用中间层的通用api即可,底层硬件和应用业务逻辑分开。
这里举一个例子,比如说鸿蒙系统LiteOS_m基本外设的使用,我们可以随便打开applications目录下的一个例程,我这里打开目录“OpenHarmony/release_1.0.1/applications/sample/wifi-iot/app/iothardware”下的"led_example"例程,主要相关源码如下:
static void *LedTask(const char *arg)
{
(void)arg;
while (1) {
switch (g_ledState) {
case LED_ON:
IoTGpioSetOutputVal(LED_TEST_GPIO, 1);
usleep(LED_INTERVAL_TIME_US);
break;
case LED_OFF:
IoTGpioSetOutputVal(LED_TEST_GPIO, 0);
usleep(LED_INTERVAL_TIME_US);
break;
case LED_SPARK:
IoTGpioSetOutputVal(LED_TEST_GPIO, 0);
usleep(LED_INTERVAL_TIME_US);
IoTGpioSetOutputVal(LED_TEST_GPIO, 1);
usleep(LED_INTERVAL_TIME_US);
break;
default:
usleep(LED_INTERVAL_TIME_US);
break;
}
}
return NULL;
}
可以看到,控制IO口是通过IoTGpioSetOutputVal函数API实现的,该函数其实就是在“OpenHarmony/release_1.0.1/base/iot_hardware/peripheral/interfaces/kits”目录下的“iot_gpio.h”声明的,在该目录下还能看到其它基本外设API的声明头文件。
那IoTGpioSetOutputVal函数是在哪里定义具体实现的呢?这个就是根据不同设备不同平台其实现是可能不同的,因此该函数的实现应放置在device目录下。例如hi3861的该函数实现放置在“OpenHarmony/release_1.0.1/device/hispark_pegasus/hi3861_adapter/hals/iot_hardware/wifiiot_lite/hal_iot_gpio.c”文件中。
当然,一个系统的适配还包括其它很多东西,比如说系统基本组件(任务、互斥量、信号量、队列等),蓝牙、wifi等具备相关协议栈的适配等等。
2. XR806的适配部分
事实上,在了解完鸿蒙系统后,再来看XR806部分的源码时,我是有点懵的。怎么个懵法呢?我从一个XR806的例程入手。我们看到“OpenHarmony/release_1.0.1/device/xradio/xr806/ohosdemo/iot_peripheral
”这个例子,先从入口函数来看,代码如下:
void PeripheralTestMain(void)
{
printf("\r\nPeripheral Test Start\r\n");
if (OS_ThreadCreate(&g_main_thread, "MainThread", MainThread, NULL,
OS_THREAD_PRIO_APP, 4 * 1024) != OS_OK) {
printf("[ERR] Create MainThread Failed\n");
}
}
SYS_RUN(PeripheralTestMain);
从以上代码可以看到,创建多任务使用的是OS_ThreadCreate函数。但是,看过鸿蒙开发者文档的读者都知道,鸿蒙创建多任务的API其实是LOS_TaskCreate函数,这是怎么回事呢?
其实答案很简单,XR806是在OS_ThreadCreate的函数实现中调用了LOS_TaskCreate,而且不止线程创建的接口,其他系统基础组件的接口实现都对鸿蒙的标准接口进行封装,具体在目录“OpenHarmony/release_1.0.1/device/xradio/xr806/os”下。这样做的目的是什么呢?我猜测可能是为了兼容以前的旧freertos项目。但这样做之后,XR806的应用程序就不能无修改跨芯片(适配完毕的)通用了,所以XR806的应用程序目录也放置在device目录下。
当然,XR806关于外设的接口实现,wifi、蓝牙的接口实现都是基于鸿蒙的标准接口的,具体可查看device相关目录。
另外一方面,XR806还将其他一些实现(即目录"OpenHarmony/release_1.0.1/device/xradio/xr806/xr_skylark")编译成库了,我们在编译时,执行"make menuconfig"其实就是在进行选择编译,可以将一些不需要的实现筛选掉。对于本项目来说,需要实现mqtt需要将lwip编译进来,通过生成的.config和“OpenHarmony/release_1.0.1/device/xradio/xr806/liteos_m”目录下的配置文件确认,lwip已经默认编译。也可以通过编译生成的库文件确实是否有编译。
3. MQTT实现
本贴使用的MQTT基于LWIP,XR806使用的基于wifi的网络,LWIP与wifi之间应该需要做相关适配,当平台已经完成适配,暂无需关心。
因此,MQTT的实现其实很简单:第一,连接wifi热点;第二,连接MQTT服务器。前者已经有一个官方例程了,后者呢,前面已经提过基于LWIP中的MQTT来实现了。
首先将mqtt的实现源码包含进来(因为默认编译生成的lwip库没有包含应用层的实现),BUILD.gn相关源码包含如下:
sources = [
"src/main.c",
"//device/xradio/xr806/xr_skylark/src/net/lwip-2.1.2/src/apps/mqtt/mqtt.c",
]
然后就是找到lwip的官方文档,看是否有对mqtt实现的说明文档,一看还真有,在目录“OpenHarmony/release_1.0.1/device/xradio/xr806/xr_skylark/src/net/lwip-2.1.2/doc”下。直接跟着文档走,复制——粘贴,一个简单的单片机固件即完成。当然,其实过程还是有一些坑需要查看lwip源码才能了解用法,我这里呢直接给出固件源码(比较乱,初版程序,直接堆在一个文件,还好代码量较小),也就不进行解释了,主要是我困了哈,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include "ohos_init.h"
#include "kernel/os/os.h"
#include "lwip/apps/mqtt.h"
#include "wifi_device.h"
#define WIFI_DEVICE_CONNECT_AP_SSID "Android_Sen"
#define WIFI_DEVICE_CONNECT_AP_PSK "250638cks!"
static OS_Thread_t g_main_thread;
static OS_Thread_t g_wifi_thread;
static int inpub_id;
static int isConnected = 0;
static int isMQTTConnected = 0;
static void iot_client_do_connect(mqtt_client_t *client);
static void mqtt_pub_request_cb(void *arg, err_t result);
void iot_publish(mqtt_client_t *client, void *arg);
#define NET_IF_STATE_DBG(netif) \
do { \
if (netif_is_link_up(netif)) { \
printf("netif is up.\n"); \
} else { \
printf("netif is down.\n"); \
} \
} while (0)
extern struct netif *g_wlan_netif;
WifiEvent sta_event;
void Scan_done_deal(int state, int size)
{
if (state == WIFI_STATE_AVALIABLE) {
printf("======== Callback: scan done, the num of bss: %d\n",
size);
}
}
void Connected_deal(int state, WifiLinkedInfo *info)
{
if (state == WIFI_STATE_AVALIABLE) {
isConnected = 1;
printf("======== Callback: connected\n");
} else if (state == WIFI_STATE_NOT_AVALIABLE) {
isConnected = 0;
printf("======== Callback: disconnected\n");
}
}
void wifi_device_connect()
{
printf("\n=========== Event Test Start ===========\n");
sta_event.OnWifiScanStateChanged = Scan_done_deal;
sta_event.OnWifiConnectionChanged = Connected_deal;
if (WIFI_SUCCESS != RegisterWifiEvent(&sta_event)) {
printf("Error: RegisterWifiEvent fail\n");
return;
}
if (WIFI_SUCCESS != EnableWifi()) {
printf("Error: EnableWifi fail\n");
return;
}
OS_Sleep(1);
while (1) {
if (!isConnected) {
if (WIFI_SUCCESS != Scan()) {
printf("Error: Scan fail.\n");
continue;
}
const char ssid_want_connect[] = WIFI_DEVICE_CONNECT_AP_SSID;
const char psk[] = WIFI_DEVICE_CONNECT_AP_PSK;
WifiScanInfo scan_results[30];
unsigned int scan_num = 30;
if (WIFI_SUCCESS != GetScanInfoList(scan_results, &scan_num)) {
printf("Error: GetScanInfoList fail.\n");
continue;
}
WifiDeviceConfig config = { 0 };
int netId = 0;
int i;
for (i = 0; i < scan_num; i++) {
printf("ssid: %s ", scan_results[i].ssid);
printf("securityType: %d\n", scan_results[i].securityType);
if (0 == strcmp(scan_results[i].ssid, ssid_want_connect)) {
memcpy(config.ssid, scan_results[i].ssid,
WIFI_MAX_SSID_LEN);
memcpy(config.bssid, scan_results[i].bssid,
WIFI_MAC_LEN);
strcpy(config.preSharedKey, psk);
config.securityType = scan_results[i].securityType;
config.wapiPskType = WIFI_PSK_TYPE_ASCII;
config.freq = scan_results[i].frequency;
break;
}
}
if (i >= scan_num) {
printf("Error: No found ssid in scan_results\n");
OS_Sleep(3);
continue;
}
if (WIFI_SUCCESS != AddDeviceConfig(&config, &netId)) {
printf("Error: AddDeviceConfig Fail\n");
}
printf("Config Success\n");
if (WIFI_SUCCESS != ConnectTo(netId)) {
printf("Error: ConnectTo Fail\n");
}
OS_Sleep(10);
} else {
OS_Sleep(30);
}
}
}
static void mqtt_incoming_publish_cb(void *arg, const char *topic, u32_t tot_len)
{
printf("Incoming publish at topic %s with total length %u\n", topic, (unsigned int)tot_len);
/* Decode topic string into a user defined reference */
if(strcmp(topic, "print_payload") == 0) {
inpub_id = 0;
} else if(topic[0] == 'A') {
/* All topics starting with 'A' might be handled at the same way */
inpub_id = 1;
} else {
/* For all other topics */
inpub_id = 2;
}
}
static void mqtt_incoming_data_cb(void *arg, const u8_t *data, u16_t len, u8_t flags)
{
printf("Incoming publish payload with length %d, flags %u\n", len, (unsigned int)flags);
if(flags & MQTT_DATA_FLAG_LAST) {
/* Last fragment of payload received (or whole part if payload fits receive buffer
See MQTT_VAR_HEADER_BUFFER_LEN) */
/* Call function or do action depending on reference, in this case inpub_id */
if(inpub_id == 0) {
/* Don't trust the publisher, check zero termination */
if(data[len-1] == 0) {
printf("mqtt_incoming_data_cb: %s\n", (const char *)data);
}
} else if(inpub_id == 1) {
/* Call an 'A' function... */
} else {
printf("mqtt_incoming_data_cb: Ignoring payload...\n");
}
} else {
/* Handle fragmented payload, store in buffer, write to file or whatever */
}
}
static void mqtt_sub_request_cb(void *arg, err_t result)
{
printf("Subscribe result: %d\n", result);
}
static void mqtt_connection_cb(mqtt_client_t *client, void *arg, mqtt_connection_status_t status)
{
err_t err;
if(status == MQTT_CONNECT_ACCEPTED) {
printf("mqtt_connection_cb: Successfully connected\n");
/* Setup callback for incoming publish requests */
mqtt_set_inpub_callback(client, mqtt_incoming_publish_cb, mqtt_incoming_data_cb, arg);
/* Subscribe to a topic named "subtopic" with QoS level 1, call mqtt_sub_request_cb with result */
err = mqtt_subscribe(client, "sensor_cmd", 1, mqtt_sub_request_cb, arg);
if(err != ERR_OK) {
printf("mqtt_subscribe return: %d\n", err);
}
iot_publish(client, NULL);
} else {
printf("mqtt_connection_cb: Disconnected, reason: %d\n", status);
/* Its more nice to be connected, so try to reconnect */
//iot_client_do_connect(client);
}
}
static void iot_client_do_connect(mqtt_client_t *client)
{
struct mqtt_connect_client_info_t ci;
ip_addr_t host_ip_addr;
err_t err;
IP4_ADDR(ip_2_ip4(&host_ip_addr), 192, 168, 8, 105);
/* Setup an empty client info structure */
memset(&ci, 0, sizeof(ci));
/* Minimal amount of information required is client identifier, so set it here */
ci.client_id = "lwip_test";
ci.keep_alive = 5;
/* Initiate client and connect to server, if this fails immediately an error code is returned
otherwise mqtt_connection_cb will be called with connection result after attempting
to establish a connection with the server.
For now MQTT version 3.1.1 is always used */
err = mqtt_client_connect(client, &host_ip_addr, 1883, mqtt_connection_cb, 0, &ci);
/* For now just print the result code if something goes wrong */
if(err != ERR_OK) {
printf("mqtt_connect return %d\n", err);
}
}
/* Called when publish is complete either with sucess or failure */
static void mqtt_pub_request_cb(void *arg, err_t result)
{
if(result != ERR_OK) {
printf("Publish result: %d\n", result);
}
}
void iot_publish(mqtt_client_t *client, void *arg)
{
char pub_payload[10]= {0};
err_t err;
u8_t qos = 2; /* 0 1 or 2, see MQTT specification */
u8_t retain = 0; /* No don't retain such crappy payload... */
int sensor_data = rand() % 100;
sprintf(pub_payload, "%d\n", sensor_data);
err = mqtt_publish(client, "sensor_data", pub_payload, strlen(pub_payload), qos, retain, mqtt_pub_request_cb, arg);
if(err != ERR_OK) {
printf("Publish err: %d\n", err);
}
}
static void MainThread(void *arg)
{
mqtt_client_t *client = mqtt_client_new();
int count = 0;
while (1)
{
if(client != NULL) {
if (isConnected && !isMQTTConnected) {
printf("connect time: %d\n", count++);
iot_client_do_connect(client);
}
OS_Sleep(10);
}
}
}
static void WifiThread(void *arg)
{
wifi_device_connect();
}
void IOTMain(void)
{
printf("\r\nIOT Sensor Start\r\n");
if (OS_ThreadCreate(&g_main_thread, "MainThread", MainThread, NULL,
OS_THREAD_PRIO_APP, 4 * 1024) != OS_OK) {
printf("[ERR] Create MainThread Failed\n");
}
if (OS_ThreadCreate(&g_wifi_thread, "WifiThread", WifiThread, NULL,
OS_THREAD_PRIO_APP, 4 * 1024) != OS_OK) {
printf("[ERR] Create WifiThread Failed\n");
}
}
SYS_RUN(IOTMain);
通过以上的程序,其实可以看到实现的逻辑就是每隔一段时间发布主题为“sensor_data”的消息,因此,我在Ubuntu上搭建好MQTT服务器后,并在Ubuntu上建立一个MQTT客户端订阅“sensor_data”主题的消息,可以定时接收到开发板发送过来的消息。效果如下:
MQTT服务器的搭建相信各位随便搜一下就能解决,就不再赘述。
总结
本篇是对XR806与MQTT服务器连接实现涉及的相关知识点描述,后续会更改程序相关逻辑以更适合物联网项目,同时后续会再发布一篇Android MQTT客户端的贴(有空的话)。