大目熊 · 2022年01月04日

【XR806开发板试用】基于MQTT实现手机与XR806互联——XR806开发板开发部分

小项目背景

鸿蒙系统也听说发布一段时间了,一直没去了解。但是勒,既然是华为发布的项目,还是想找个时间了解下的。刚好在公众号看到免费申请鸿蒙开发板试用,那就顺便学习下吧。

鸿蒙系统的特点是万物互联,那么申请的开发板我也准备用来做一个物联网小项目,至于做啥呢?...思量许久,感觉无论用来物联网控制啥东西,都需要额外成本,那就暂时放弃外设控制的想法,先完成简单的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的开源库实现的。

整体框架
1641134347-1.png

如上图,项目实现的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的内容比较少。

我们先看鸿蒙的目录结构,如下:
1641134347-1.png
通过这个目录,我们其实大致可以猜到以下(仅提供参考):

-- 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”主题的消息,可以定时接收到开发板发送过来的消息。效果如下:
screenshot-from-2022-01-03-01-57-57.png
MQTT服务器的搭建相信各位随便搜一下就能解决,就不再赘述。

总结

本篇是对XR806与MQTT服务器连接实现涉及的相关知识点描述,后续会更改程序相关逻辑以更适合物联网项目,同时后续会再发布一篇Android MQTT客户端的贴(有空的话)。

推荐阅读
关注数
13823
内容数
139
全志XR806开发板相关的知识介绍以及应用专栏。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息