应用介绍

本Demo利用传感器采集环境的温度和湿度,通过MQTT协议传给服务器。以JS编写了一个MQTT Web客户端程序,接收上述温湿度数据,并将这些数据显示在网页上。用户可以在远程登录网站,查看传感器采集的温湿度数据。

开发环境介绍

  • 正点原子W601 WIFI 物联网开发板

正点原子W601开发板是由正点原子,RTThread和联盛德三方联合开发的一款T物联网开发板。W601芯片是一款支持多功能的SOCW旧芯片,功能强大,外设多,芯片自带Wifi功能。对于使用WiFi开发T功能具有强大的成本优势。

该开发板上带有温度湿度传感器、光照传感器等多种外设,可用keil进行编程。主控芯片W601可与RT Thread OS完美搭配,并且提供完善的与物联网技术相关的开发资料,大大缩短了开发的进度。我在这周一拿到的板子,从零开始鼓捣了五六天就可以做出一个简单的Demo。它在本应用中采集温湿度并上传数据到MQTT服务器。

  • PC1(ubuntu16.4+EMQ X Broker)

作为MQTT服务器。

  • PC2(Win10)

作为MQTT客户端,订阅W601开发板发布的温湿度数据。利用Html和JS语言编辑了一个简单的网页显示温湿度数据,使用者可以在远程登录这个网页实时查看开发板采集的温湿度数据。

实现过程

  • MQTT服务器搭建

请查看我的上一篇文章https://blog.csdn.net/u010495967/article/details/100182943,里面介绍了利用EMQ X搭建MQTT服务器的过程,比较简单。

  • MQTT客户端1

制作了一个简单的网页,从MQTT服务端订阅相关主题,获取并显示温湿度数据。利用windows上的IIS把把它发布在电脑上,供使用者访问。网页如下图所示(时间仓促再加上本人是个菜鸟,所以做的很简陋)。

paho-mqtt提供了一个JS语言的mqtt library,使用WebSocket与mqtt服务器进行通信,上手很简单。建议大家看一下这位大牛的文章来学习mqtt的web应用https://www.thomaslaurenson.com/blog/2018/07/10/mqtt-web-application-using-javascript-and-websockets/

输入服务器IP和端口,点击连接,即可连通服务器并收到数据。因为是使用WebSocket服务通信,所以与EMQ搭建的的MQTT服务器的端口选择8083。

  • MQTT客户端2(正点原子W601开发板)

我首先看了正点原子编写的裸机开发教程,首先学会如何用KEIL编写和烧录程序,这里把教程分享一下。

需要拷贝官方提供的下载算法 W60X_QFlash.FLM文件到 MDK安装目录。

 

这里注意RAM的容量改为0x2000 

开发板部分的内容比较多,主要分为以下几个模块:

温湿度采集:与传感器通信,获取环境的温度和湿度,利用RT-Thread提供的软件包可以很简单地驱动多种多样的传感器。

显示模块:在开发板上的小LCD上显示提示信息和温湿度信息。

联网模块:W601芯片你自带WIFI功能,与RT-Thread操作系统相结合能够十分方便地使用WIFI功能。还值得一提的是W601开发板带有快速配网功能,调用几个API就可以实现利用Web配置联网信息。十分方便,并且大大缩短研发时间,降低研发难度。其过程如下:

应用工作流程:

1.初始化外设、操作系统和WIFI功能,等待用户配置网络。

根据提示信息,手机接入名为softap_73b2的 AP,然后打开浏览器,输入地址192.168.169.1(这个IP可以在程序中设置)就会打开一个网页,如下图。输入开发板将要接入的热点的ssid和密码,点击保存按钮,就可以将wifi的ssid和密码发送给控制芯片了,如果密码正确就会自动接入该热点,开发板成功联网。

联网成功后,开始连接MQTT服务器,MQTT服务器的地址和端口预先写在程序中。连接成功后,会在屏幕上连接的地址等信息,之后开启温湿度采集进程,每隔一段时间采集温湿度数据并上传到MQTT服务器。如下图。

然后,只要在手机或者电脑浏览器上,输入之前做的网页MQTT客户端的网址,就可以实时查看开发板采集到的温湿度数据啦。

代码

首先是网页端的代码。

HTML代码内容很少,js则引入了paho-mqtt的library。

<!DOCTYPE html>


<html>
	<head>
		<title>远程温湿度监控Demo</title>
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript" type="text/javascript"></script>
		<script type="text/javascript" src="js/mqtt.js"></script>
		
	</head>
	<body>
		<div class="wrapper">
			<h1>远程温湿度监控Demo</h1>
			<p>使用W601 WIFI 物联网开发板应用。 </p>
			<p>https://blog.csdn.net/u010495967</p>
			<p>请链接MQTT服务器</p>
			<form id="connection-info-form">
				<b>IP地址:</b>
				<input id="host" type="text" name="host" value=""><br>
				<b>端&nbsp;&nbsp;&nbsp;口:</b>
				<input id="port" type="text" name="port" value="8083"><br>
				<b>订阅主题:</b>
				<input id="topic" type="text" name="topic" value=""><br>
				<br>
				<input type="button" onclick="startConnect()" value="连接">
				<input type="button" onclick="startDisconnect()" value="断开">
			</form>
			</form>
			<div id="messages"></div>
			<div id = "information"></div>
		</div>


	</body>
</html>

JS代码

// Called after form input is processed
function startConnect() {
// Generate a random client ID
clientID = "clientID-" + parseInt(Math.random() * 100);

// Fetch the hostname/IP address and port number from the form
host = document.getElementById("host").value;
port = document.getElementById("port").value;

// Print output for the user in the messages div
document.getElementById("messages").innerHTML = '<span>连接:' + host + ' : ' + port + '</span><br/>';
// document.getElementById("messages").innerHTML += '<span>Using the following client value: ' + clientID + '</span><br/>';

// Initialize new Paho client connection
client = new Paho.MQTT.Client(host, Number(port), clientID);

// Set callback handlers
client.onConnectionLost = onConnectionLost;
client.onMessageArrived = onMessageArrived;

// Connect the client, if successful, call onConnect function
client.connect({
onSuccess: onConnect,
});
}

// Called when the client connects
function onConnect() {
// Fetch the MQTT topic from the form
topic = document.getElementById("topic").value;

// Print output for the user in the messages div
document.getElementById("messages").innerHTML += '<span>订阅: ' + topic + '</span><br/>';

// Subscribe to the requested topic
client.subscribe(topic);
}

// Called when the client loses its connection
function onConnectionLost(responseObject) {
document.getElementById("messages").innerHTML += '<span>ERROR: Connection lost</span><br/>';
if (responseObject.errorCode !== 0) {
document.getElementById("messages").innerHTML += '<span>ERROR: ' + + responseObject.errorMessage + '</span><br/>';
}
}

// Called when a message arrives
function onMessageArrived(message) {
data = JSON.parse(message.payloadString);
console.log(data.temp);
console.log(data.humi);
document.getElementById("information").innerHTML = '<span>温度:' + data.temp + '<br/>湿度:' + data.humi + '</span><br/>';
}

// Called when the disconnection button is pressed
function startDisconnect() {
client.disconnect();
document.getElementById("messages").innerHTML = '<span>断开连接</span><br/>';
}

单片机代码:

内容太多,就不提供所有代码了,只把一些主要功能列出来。

注意:时间仓促,而且本人业务不精,代码写的很差,仅供参考,大家不要学习我的代码。

//main
int main(void)
{
	struct rt_wlan_info info;
	//初始化一个信号量
	rt_sem_init(&board_ip_sem, "board_ip_sem", 0, RT_IPC_FLAG_FIFO);
	//初始化LCD
	lcd_init();
    //创建采集和上传数据进程。
	publish_sensor_data_tid = rt_thread_create("push_data",
                                    publish_sensor_data_entry, RT_NULL,
                                    1024,
                                    10,
                                    10);
    //接入网络
	if(RT_EOK == join_wifi())
	{
    //显示连接信息
		lcd_clear(WHITE);
		rt_wlan_get_info(&info);
		lcd_show_string(0,0,16,"MAC:%02x:%02x:%02x:%02x:%02x:%02x", 
		  info.bssid[0],
          info.bssid[1],
          info.bssid[2],
          info.bssid[3],
          info.bssid[4],
          info.bssid[5]);
	}
	rt_sem_take(&board_ip_sem, NET_READY_TIME_OUT);
	lcd_show_string(0,16,16,"IP:%s", board_ip);
	//连接成功后,开始采集并上传数据
    if (publish_sensor_data_tid == RT_NULL)
    {
        rt_kprintf("create thread failed");
        return -1;
    }

}

下面实现了连接网络,使用W601的快速联网功能。

static int join_wifi(void)
{
	rt_err_t result = RT_EOK;
	//注册回调函数,WIFI连接成功后,开启MQTT功能
	rt_wlan_register_event_handler(RT_WLAN_EVT_READY, (void (*)(int, struct rt_wlan_buff *, void *))mq_start, RT_NULL);
    //配置WIFI状态
	rt_wlan_set_mode(RT_WLAN_DEVICE_STA_NAME, RT_WLAN_STATION);
	rt_wlan_set_mode(RT_WLAN_DEVICE_AP_NAME, RT_WLAN_AP);
	rt_sem_init(&web_sem, "web_sem", 0, RT_IPC_FLAG_FIFO);
	rt_wlan_start_ap(WIFI_AP_SSID, NULL);
    //显示提示信息
	lcd_show_string(0, 32, 32, "Connect wifi");
	lcd_show_string(0, 64, 32, WIFI_AP_SSID);
	
	rt_thread_mdelay(2000);
    /* 一键配网:APWEB 模式 */
    result = wm_oneshot_start(WM_APWEB, oneshot_result_cb);
    if (result != 0)
    {
        LOG_E("web config wifi start failed");
        return result;
    }
		lcd_show_string(0, 96, 32, "...");
    LOG_D("web config wifi start...");
    result = rt_sem_take(&web_sem, NET_READY_TIME_OUT);
    if (result != RT_EOK)
    {
        LOG_E("connect error or timeout");
        return result;
    };

    /* 配网结束,关闭 AP 热点 */
    if (RT_TRUE == rt_wlan_ap_is_active())
        rt_wlan_ap_stop();

    /* 连接 WiFi */
    result = rt_wlan_connect(ssid, passwd);
    if (result != RT_EOK)
    {
        LOG_E("\nconnect ssid %s key %s error:%d!", ssid, passwd, result);
    };
}

static void oneshot_result_cb(int state, unsigned char *ssid_i, unsigned char *passwd_i)
{
    char *ssid_temp = (char *)ssid_i;
    char *passwd_temp = (char *)passwd_i;

    if (RT_TRUE == rt_wlan_ap_is_active())
        rt_wlan_ap_stop();

    /* 配网回调超时返回 */
    if (state != 0)
    {
        LOG_E("Receive wifi info timeout(%d). exit!", state);
        return;
    }
    if (ssid_temp == RT_NULL)
    {
        LOG_E("SSID is NULL. exit!");
        return;
    }
    LOG_D("Receive ssid:%s passwd:%s", ssid_temp == RT_NULL ? "" : ssid_temp, passwd_temp == RT_NULL ? "" : passwd_temp);

    strncpy(ssid, ssid_temp, strlen(ssid_temp));
    if (passwd_temp)
    {
        strncpy(passwd, passwd_temp, strlen(passwd_temp));
    }
    /* 通知 ssid 与 key 接收完成 */
    rt_sem_release(&web_sem);
}

下面是与MQTT有关的代码,主要是在W601开发板提供的例子上修改的。

static void mqtt_sub_callback(MQTTClient *c, MessageData *msg_data)
{
    *((char *)msg_data->message->payload + msg_data->message->payloadlen) = '\0';
    LOG_D("Topic: %.*s receive a message: %.*s",
          msg_data->topicName->lenstring.len,
          msg_data->topicName->lenstring.data,
          msg_data->message->payloadlen,
          (char *)msg_data->message->payload);

    return;
}

static void mqtt_sub_default_callback(MQTTClient *c, MessageData *msg_data)
{
    *((char *)msg_data->message->payload + msg_data->message->payloadlen) = '\0';
    LOG_D("mqtt sub default callback: %.*s %.*s",
          msg_data->topicName->lenstring.len,
          msg_data->topicName->lenstring.data,
          msg_data->message->payloadlen,
          (char *)msg_data->message->payload);
    return;
}

static void mqtt_connect_callback(MQTTClient *c)
{
    LOG_I("Start to connect mqtt server");
	  lcd_show_string(0, 32, 16, "Start to connect mqtt server");

}

static void mqtt_online_callback(MQTTClient *c)
{
    LOG_D("Connect mqtt server success");
	  lcd_show_string(0, 32, 16, "Connect mqtt server success ");
	  lcd_show_string(0, 48, 16, MQTT_URI);

    LOG_D("Publish message: Hello,RT-Thread! to topic: %s", sup_pub_topic);
    //mq_publish("Hello,RT-Thread!");
		rt_thread_startup(publish_sensor_data_tid);

}

static void mqtt_offline_callback(MQTTClient *c)
{
    LOG_I("Disconnect from mqtt server");
}

/* 创建与配置 mqtt 客户端 */
static void mq_start(void)
{
    /* 初始 condata 参数 */
    MQTTPacket_connectData condata = MQTTPacket_connectData_initializer;
    static char cid[20] = {0};

    static int is_started = 0;
    if (is_started)
    {
        return;
    }
    /* 配置 MQTT 文本参数 */
    {
        client.isconnected = 0;
        client.uri = MQTT_URI;

        /* 生成随机客户端 ID */
        rt_snprintf(cid, sizeof(cid), "rtthread%d", rt_tick_get());
        rt_snprintf(sup_pub_topic, sizeof(sup_pub_topic), "%s%s", MQTT_PUBTOPIC, cid);
        /* 配置连接参数 */
        memcpy(&client.condata, &condata, sizeof(condata));
        client.condata.clientID.cstring = cid;
        client.condata.keepAliveInterval = 60;
        client.condata.cleansession = 1;
        client.condata.username.cstring = MQTT_USERNAME;
        client.condata.password.cstring = MQTT_PASSWORD;

        /* 配置 mqtt 参数 */
        client.condata.willFlag = 0;
        client.condata.will.qos = 1;
        client.condata.will.retained = 0;
        client.condata.will.topicName.cstring = sup_pub_topic;

        client.buf_size = client.readbuf_size = 1024;
        client.buf = malloc(client.buf_size);
        client.readbuf = malloc(client.readbuf_size);
        if (!(client.buf && client.readbuf))
        {
            LOG_E("no memory for MQTT client buffer!");
            goto _exit;
        }

        /* 设置事件回调 */
        client.connect_callback = mqtt_connect_callback;
        client.online_callback = mqtt_online_callback;
        client.offline_callback = mqtt_offline_callback;
        /* 设置要订阅的 topic 和 topic 对应的回调函数 */
        client.messageHandlers[0].topicFilter = sup_pub_topic;
        client.messageHandlers[0].callback = mqtt_sub_callback;
        client.messageHandlers[0].qos = QOS1;

        /* 设置默认订阅回调函数 */
        client.defaultMessageHandler = mqtt_sub_default_callback;
    }

    /* 启动 MQTT 客户端 */
    LOG_D("Start mqtt client and subscribe topic:%s", sup_pub_topic);
    paho_mqtt_start(&client);
    is_started = 1;

_exit:
    return;
}

static void mq_publish(const char *send_str)
{
    MQTTMessage message;
    const char *msg_str = send_str;
    const char *topic = sup_pub_topic;
    message.qos = QOS1;
    message.retained = 0;
    message.payload = (void *)msg_str;
    message.payloadlen = strlen(message.payload);

    MQTTPublish(&client, topic, &message);

    return;
}

上传数据进程

static void publish_sensor_data_entry(void *parameter)
{
	int ret = RT_EOK;
	rt_device_t temp_dev, humi_dev;
	struct rt_sensor_data temp_dev_data, humi_dev_data;
	
	LOG_D("Temperature and Humidity Sensor Testing Start...");
    /* 寻找并打开温度传感器 */
    temp_dev = rt_device_find(TEMP_DEV);
    if (temp_dev == RT_NULL)
    {
        LOG_E("can not find TEMP device: %s", TEMP_DEV);
        ret = RT_ERROR;
    }
    else
    {
        if (rt_device_open(temp_dev, RT_DEVICE_FLAG_RDONLY) != RT_EOK)
        {
            LOG_E("open TEMP device failed!");
            ret = RT_ERROR;
        }
    }
    /* 寻找并打开湿度传感器 */
    humi_dev = rt_device_find(HUMI_DEV);
    if (humi_dev == RT_NULL)
    {
        LOG_E("can not find HUMI device: %s", HUMI_DEV);
        ret = RT_ERROR;
    }
    else
    {
        if (rt_device_open(humi_dev, RT_DEVICE_FLAG_RDONLY) != RT_EOK)
        {
            LOG_E("open HUMI device failed!");
            ret = RT_ERROR;
        }
    }
		if(RT_EOK != ret)
		{
			lcd_show_string(0, 64, 32, "Sensor Error");
			return;
		}
		while(1)
	{
	rt_device_read(temp_dev, 0, &temp_dev_data, 1);
	lcd_show_string(0, 64, 32, "%d.%d C  ", (int)(temp_dev_data.data.temp / 10), (int)(temp_dev_data.data.temp % 10));

	rt_device_read(humi_dev, 0, &humi_dev_data, 1);
	lcd_show_string(0, 96, 32, "%d.%d %%", (int)(humi_dev_data.data.humi / 10), (int)(humi_dev_data.data.humi % 10));
	mq_publish(data_to_json(temp_dev_data.data.temp, humi_dev_data.data.humi));
	rt_thread_mdelay(5000);
	}
}

 

生成JSON格式数据,在通信过程中,采用JSON数据格式,RT-Thread提供了cJSON软件包,开发者可以很轻松地生成JSON格式数据。

static char* data_to_json(int temp, int humi)
{
	
	char *p;
	char temp_str[8]="";
	char humi_str[8]="";
	json_root = cJSON_CreateObject(); /* 创建根索引 json 对象 */
    json_sub = cJSON_CreateObject();  /* 创建子索引 json 对象 */
    json_array = cJSON_CreateArray(); /* 创建数组 json 对象 */

    /* 判断创建 cjson 是否创建成功 */
    if (RT_NULL == json_root || RT_NULL == json_sub || RT_NULL == json_array)
    {
        LOG_E("Fail to creat cjson");
        rt_free(json_root);
        rt_free(json_sub);
        rt_free(json_array);
        return RT_NULL;
    }
		
	rt_sprintf(temp_str, "%d.%d C", (int)(temp / 10), (int)(temp % 10));
	rt_sprintf(humi_str, "%d.%d %%", (int)(humi / 10), (int)(humi % 10));
	
	  cJSON_AddStringToObject(json_root, "temp", temp_str); 
    cJSON_AddStringToObject(json_root, "humi", humi_str);

    /* 生成无格式 json 字符串 */
    p = cJSON_PrintUnformatted(json_root);

    cJSON_Delete(json_root);
    return p;
}

总结

这个开发板拿到手学习了几天,听说刚发售有活动,鼓励大家发博文,所以就做了一个简单的小应用出来献丑,请大家多多批评。

这个开发板用下来,我感觉它功能强大,学习资料丰富,而且价格便宜。联盛德,RTThread和正点原子三家公司也提供了丰富的软件开发包,帮助我们在研发过程中降低时间成本,减少研发难度,很适合用来学习物联网以及开发项目样机。下图是开发板的手册目录的一部分,可以看出有许多与物联网相关的学习例程,非常值得学习与研究。

参考资料

RT-THREAD W60X SDK 开发手册

W601开发指南V1.0裸机版本

http://www.openedv.com/thread-299779-1-1.html

https://www.thomaslaurenson.com/blog/2018/07/10/mqtt-web-application-using-javascript-and-websockets/

https://www.eclipse.org/paho/

更多推荐