概述
腾讯云物联网开发平台 IoT Explorer(腾讯云物联网平台 = 云端的设备管理后台)是腾讯云提供的物联网一站式开发平台,配套微信小程序「腾讯连连」给设备配网(配网 = 把 Wi-Fi 账号密码"告诉"开发板)和控制设备。本教程通过官方 qcloud_demo 示例工程,把 Ai-WB2 开发板接入腾讯云 IoT Explorer:使用腾讯连连小程序蓝牙辅助配网,配网成功后开发板自动连接 Wi-Fi 并接入云平台,按 KEY 键控制板载 LED 亮灭并上报属性(上报 = 设备→云端发数据),也可以从小程序/控制台下发指令(下发 = 云端→设备发指令)远程开关灯。
用大白话讲:腾讯云物联网平台就像「微信服务器」,产品 ID、设备名称、设备密钥是平台发的「账号密码」;配网就是通过微信小程序「腾讯连连」把家里 Wi-Fi 的账号密码"说"给开发板听(走蓝牙通道,不用接线);之后你按开发板上的按键,灯的状态就会像「发朋友圈」一样实时上报到云端,云端的设备「替身」同步更新;反过来,在小程序里点开关,指令也会像「发私信」一样下发到开发板把灯点亮。
本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本
release_bl_iot_sdk_1.6.40)的官方示例applications/iot-solution/qcloud_demo编写,代码可在本地 SDK 中直接找到。
这一步在云端"办账号":创建产品和设备,拿到设备登录平台用的三元组(产品 ID/设备名称/设备密钥)。
- 打开腾讯云官网,注册并登录账号(新用户需完成实名认证)。
- 搜索进入「物联网开发平台 IoT Explorer」控制台(首次进入需开通服务,公共实例免费)。
- 新建项目(如
WB2_Demo)→ 在项目下新建产品:产品类别选「标准品类」→ 智慧生活 → 电工照明 → 灯;联网方式选 Wi-Fi,配网方式选「标准 BLE 辅助」(本教程用蓝牙配网),并勾选「设备数据协议」的数据模板。 - 「设备调试 → 新建设备」,输入设备名称(如
wb2_light),创建成功后看到设备密钥(DeviceSecret)——只在创建时显示一次,务必保存。 - 在产品「设备列表」中可看到 产品 ID(ProductId) 与设备名称(DeviceName)——这两样随时可查,不用特意保存。
💡 接入云平台需要三元组:产品 ID + 设备名称 + 设备密钥(密钥只在创建设备时显示一次);示例代码还预留了**产品密钥(ProductSecret)**用于动态注册(可选功能,本教程用不到)。这些值下一步要填进代码。
硬件确认:本示例用开发板 GPIO 控制灯与按键,接线如下(默认已按 Ai-WB2-32S-Kit 配置):
| 外设 | 引脚 |
|---|---|
| LED(灯) | GPIO14 |
| KEY(按键,按一下上报一次开关状态) | GPIO8 |
| 重新配网键(长按约 5 秒清除 Wi-Fi 配置) | GPIO4 |
打开终端,进入官方 qcloud_demo 示例工程目录:
cd ~/Ai-Thinker-WB2/applications/iot-solution/qcloud_demo
说明:
cd是「进入目录」的命令,进入官方示例工程;后面所有make命令都要在这个目录下执行。
📌 该工程由多个源文件组成,完整代码章节只展示
main.c,其余文件可在官方工程中找到:
| 文件 | 作用 |
|---|---|
sdk_app_qcloud/main.c |
主程序:Wi-Fi 连接 + 判断配网/上云流程,本教程主要看的文件 |
sdk_app_qcloud/light_data_template_sample.c |
数据模板核心:属性上报/下发控制、LED 与按键逻辑 |
sdk_app_qcloud/qcloud_wifi_config_sample.c |
配网流程(含配网方式开关宏 WIFI_PROV_*)与 Start_Qcloud_Demo() |
sdk_app_qcloud/ble_qiot_ble_device.c / ble_qiot_ble_service.c |
蓝牙辅助配网服务(腾讯连连小程序配网通道) |
components/qcloud_iot_c_sdk/ |
腾讯官方 IoT SDK(IOT_Template_* 系列接口所在),三元组在这里配置 |
打开 components/qcloud_iot_c_sdk/platform/HAL_Device_freertos.c,把示例三元组换成你在控制台拿到的值:
/* product Id */
static char sg_product_id[MAX_SIZE_OF_PRODUCT_ID + 1] = "你的产品ID";
/* device name */
static char sg_device_name[MAX_SIZE_OF_DEVICE_NAME + 1] = "你的设备名称";
/* device secret of PSK device */
static char sg_device_secret[MAX_SIZE_OF_DEVICE_SECRET + 1] = "你的设备密钥";
⚠️ 该文件按配网方式分了两组三元组(
WIFI_PROV_SOFT_AP_ENABLE组与WIFI_PROV_BT_COMBO_CONFIG_ENABLE组),只编译你当前配网方式对应的一组。本教程默认蓝牙配网,改蓝牙组(#elif WIFI_PROV_BT_COMBO_CONFIG_ENABLE分支下的三个变量)即可。若开启动态注册,还需把DEV_DYN_REG_ENABLED分支下的sg_product_secret换成你的产品密钥。
确认配网方式:打开 sdk_app_qcloud/qcloud_wifi_config_sample.c 顶部,默认是蓝牙辅助配网(WIFI_PROV_BT_COMBO_CONFIG_ENABLE 1,其余 WIFI_PROV_* 为 0),保持默认即可:
#define WIFI_PROV_BT_COMBO_CONFIG_ENABLE 1 ///< wifi provisioning method: bt combo config, need Wechat Applets
本教程使用官方示例工程,无需新写代码,sdk_app_qcloud/main.c 完整代码已移至文末,见:
📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(
applications/iot-solution/qcloud_demo/sdk_app_qcloud/main.c)完全一致。数据模板上报/下发逻辑在light_data_template_sample.c,配网逻辑在qcloud_wifi_config_sample.c,其余文件见官方工程。
代码要点:
| 代码 | 作用 |
|---|---|
led_gpio_init(LED_PIN) / key_init(KEY_PIN) |
初始化 LED 与按键引脚,不初始化灯不会亮、按键按了没反应 |
easyflash_init() |
初始化掉电保存库,配网信息存在这里,不初始化读不到已保存的 Wi-Fi |
wifi_info_read() |
从掉电保存区读出 Wi-Fi 账号密码,没有就自动进入配网模式(LED 闪烁) |
Q_Cloud_Config_Net_Start() |
启动腾讯连连配网流程(蓝牙通道),配网是设备上云的第一步 |
Start_Qcloud_Demo() |
配网/联网成功后启动云平台数据模板 Demo,设备正式接入云端 |
IOT_Template_Report(...) |
把属性(如开关状态)上报到云端,不调用云上看不到设备状态 |
在工程目录执行编译:
make -j8
说明:
make是「编译」命令,把代码变成开发板能运行的固件(烧进开发板的程序);-j8表示用 8 个 CPU 核并行编译,更快。
编译成功后生成固件 build_out/sdk_app_qcloud.bin。
⚠️ 若提示
riscv64-unknown-elf-gcc: command not found,说明工具链权限未配置,先执行cd toolchain/riscv/Linux && . chmod755.sh再重新编译。
开发板保持 USB 连接,确认串口设备号后执行烧录:
make flash p=/dev/ttyUSB0 b=921600
说明:
make flash是「烧录」命令,把编译好的固件写进开发板芯片。p=后面是串口设备号(Linux 下常为/dev/ttyUSB0,Windows 下是COM3之类,以你电脑实际为准,可用ls /dev/ttyUSB*查看),b=921600是烧录波特率(串口传数据的速度),保持默认即可。
⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。
烧录完成后开发板自动重启,因为还没配过网,会自动进入配网模式(板载 LED 闪烁)。打开串口监视器(串口调试助手软件,波特率选 921600,波特率 = 串口传数据的"语速",两边必须一致)可以看到配网相关日志。
第一步:小程序配网。手机微信搜索「腾讯连连」小程序 → 进入「添加设备」→ 小程序自动扫描附近的设备 → 扫到后按提示选择你家 Wi-Fi、输入密码完成配网(配网 = 把 Wi-Fi 账号密码"告诉"开发板)。
第二步:等待上云。配网成功后开发板自动连接路由器并接入云平台,串口打印:
[APP] [EVT] GOT IP ...
Cloud Device Construct Success
Register data template propertys Success
第三步:按键上报。按开发板 KEY 键(GPIO8),LED 亮灭切换,串口打印 data template reporte success;打开腾讯连连小程序,能看到设备在线,灯的开关状态跟着变化。
第四步:云端下发。在腾讯连连小程序/控制台里点击开关,开发板 LED 同步点亮/熄灭,串口打印收到下发的控制消息。
出现 Cloud Device Construct Success 且腾讯连连小程序里设备在线、开关状态同步,即为成功;如果只看到 GOT IP 而没出现 Cloud Device Construct Success,说明还没成功,对照文末「常见问题与踩坑提示」排查。
代码执行流程
例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):
本文 API 汇总
Start_Qcloud_Demo()
配网/联网成功后启动云平台数据模板 Demo:创建 bl_qcloud_main 任务接入腾讯云(自研接口,源码在 sdk_app_qcloud/qcloud_wifi_config_sample.c)。
参数:
- 无
返回值:无
Q_Cloud_Config_Net_Start()
启动腾讯连连配网流程(默认蓝牙辅助配网),配网成功后把 Wi-Fi 账号密码存入掉电保存区(自研接口,源码在 sdk_app_qcloud/qcloud_wifi_config_sample.c)。
参数:
- 无
返回值:无
IOT_Template_Construct(pParams, pMqttClient)
构造数据模板客户端:解析三元组、创建 MQTT 连接(腾讯官方 SDK 接口,头文件 components/qcloud_iot_c_sdk/include/exports/qcloud_iot_export_data_template.h)。
参数:
pParams:TemplateInitParams结构体指针(区域、超时等,示例为{ "china", NULL, NULL, NULL, 2000, 1000, 1, 1, { 0 } })pMqttClient:已有 MQTT 客户端句柄,无则传NULL(由本函数内部创建)
返回值:成功返回客户端句柄(void *);失败返回 NULL
IOT_Template_Start_Yield_Thread(pClient)
启动独立的收发线程,自动循环调用 IOT_Template_Yield 维持心跳与消息收发,之后主循环无需再手动 Yield。
参数:
pClient:IOT_Template_Construct返回的客户端句柄
返回值:成功返回 QCLOUD_RET_SUCCESS(0);失败返回负值错误码
IOT_Template_Register_Property(pClient, pProperty, callback)
注册数据模板属性(如灯开关 light_switch),并绑定下发控制回调——云端下发修改该属性时,回调会被调用。
参数:
pClient:客户端句柄pProperty:DeviceProperty结构体指针(属性键名、类型、数据指针)callback:下发控制回调(示例为OnControlMsgCallback)
返回值:成功返回 QCLOUD_RET_SUCCESS(0);失败返回负值错误码
IOT_Template_JSON_ConstructReportArray(handle, jsonBuffer, sizeOfBuffer, count, pReportDataList)
把待上报的属性列表拼装成 JSON 报文(上报 = 设备→云端发数据),供 IOT_Template_Report 使用。
参数:
handle:客户端句柄jsonBuffer:输出缓冲区(示例为sg_data_report_buffer[2048])sizeOfBuffer:缓冲区大小count:属性个数pReportDataList:DeviceProperty *数组,存放待上报的属性
返回值:成功返回 QCLOUD_RET_SUCCESS(0);失败返回负值错误码
IOT_Template_Report(handle, pJsonDoc, sizeOfBuffer, callback, userContext, timeout_ms)
把属性 JSON 报文上报到云端,云端收到后回调 callback 确认结果。
参数:
handle:客户端句柄pJsonDoc:属性 JSON 报文sizeOfBuffer:报文长度callback:上报应答回调(示例为OnReportReplyCallback)userContext:透传数据,无则传NULLtimeout_ms:超时毫秒数(示例QCLOUD_IOT_MQTT_COMMAND_TIMEOUT)
返回值:成功返回 QCLOUD_RET_SUCCESS(0);失败返回负值错误码
IOT_Template_ControlReply(handle, pJsonDoc, sizeOfBuffer, replyPara)
回复云端的下发控制消息——收到控制指令必须应答,否则云端会认为设备没收到而保留消息。
参数:
handle:客户端句柄pJsonDoc:应答报文缓冲区sizeOfBuffer:缓冲区大小replyPara:sReplyPara结构体指针(处理结果code与超时等)
返回值:成功返回 QCLOUD_RET_SUCCESS(0);失败返回负值错误码
IOT_Template_Yield(pClient, timeout_ms)
让 SDK 处理与云端的消息收发与心跳保活,必须在主循环反复调用(示例循环 200ms 调一次),断线时返回 QCLOUD_ERR_MQTT_ATTEMPTING_RECONNECT。
参数:
pClient:客户端句柄timeout_ms:超时毫秒数,示例200
返回值:成功返回 QCLOUD_RET_SUCCESS(0);重连中返回 QCLOUD_ERR_MQTT_ATTEMPTING_RECONNECT
📌 数据模板的属性(
light_switch、color、brightness等)要与控制台产品「数据模板」里定义的属性一致;示例用按键中断 + 信号量通知上报任务的方式触发上报,主循环负责收发与应答。
完整代码
以下为 sdk_app_qcloud/main.c 完整源码,与官方示例(applications/iot-solution/qcloud_demo/sdk_app_qcloud/main.c)完全一致(数据模板逻辑在 light_data_template_sample.c,配网逻辑在 qcloud_wifi_config_sample.c,其余文件见官方工程):
📜 点击展开 sdk_app_qcloud/main.c 完整代码
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <aos/kernel.h>
#include <aos/yloop.h>
#include <lwip/tcpip.h>
#include <bl_sys.h>
#include <hal_wifi.h>
#include <blog.h>
#include <hal_sys.h>
#include <easyflash.h>
#include <wifi_mgmr_ext.h>
#include "FreeRTOS.h"
#include "task.h"
#include <hal_hwtimer.h>
#define KEY_PIN (8)
#define LED_PIN (14)
#define restore_key (4)
extern void key_init(uint8_t pin);
extern void led_gpio_init(uint8_t pin);
extern void restore_key_init(uint8_t pin);
static wifi_conf_t conf =
{
.country_code = "CN",
};
extern char sta_ssid[33];
extern char sta_passwd[65];
extern hw_timer_t* qcloud_demo_handle;
extern void Start_Qcloud_Demo(void);
static void wifi_sta_connect(char* ssid, char* password)
{
wifi_interface_t wifi_interface;
wifi_interface = wifi_mgmr_sta_enable();
wifi_mgmr_sta_connect(wifi_interface, ssid, password, NULL, NULL, 0, 0);
}
static bool wifi_info_read()
{
size_t len;
ef_get_env_blob("ssid", sta_ssid, sizeof(sta_ssid), &len);
if (strlen(sta_ssid) ==0) {
blog_error("[NV] get sta_ssid fail:%d ", len);
return false;
}
else {
blog_info("sta_ssid:%s", sta_ssid);
}
ef_get_env_blob("pwd", sta_passwd, sizeof(sta_passwd), &len);
if (strlen(sta_passwd) ==0) {
blog_error("[NV] get sta_passwd fail:%d ", len);
}
else {
blog_info("sta_passwd:%s", sta_passwd);
}
return true;
}
static void event_cb_wifi_event(input_event_t* event, void* private_data)
{
extern bool q_cloud_IsConnected;
static bool wifi_info_isSet = false;
switch (event->code) {
case CODE_WIFI_ON_INIT_DONE:
{
blog_info("[APP] [EVT] INIT DONE %lld", aos_now_ms());
wifi_mgmr_start_background(&conf);
}
break;
case CODE_WIFI_ON_MGMR_DONE:
{
blog_info("[APP] [EVT] MGMR DONE %lld", aos_now_ms());
if (wifi_info_read())
{
wifi_info_isSet = true;
wifi_sta_connect(sta_ssid, sta_passwd);
}
else
{
extern void Q_Cloud_Config_Net_Start(void);
Q_Cloud_Config_Net_Start();
}
}
break;
case CODE_WIFI_ON_MGMR_DENOISE:
{
blog_info("[APP] [EVT] Microwave Denoise is ON %lld", aos_now_ms());
}
break;
case CODE_WIFI_ON_SCAN_DONE:
{
blog_info("[APP] [EVT] SCAN Done %lld", aos_now_ms());
wifi_mgmr_cli_scanlist();
}
break;
case CODE_WIFI_ON_SCAN_DONE_ONJOIN:
{
blog_info("[APP] [EVT] SCAN On Join %lld", aos_now_ms());
}
break;
case CODE_WIFI_ON_DISCONNECT:
{
blog_info("[APP] [EVT] disconnect %lld, Reason: %s",
aos_now_ms(),
wifi_mgmr_status_code_str(event->value));
q_cloud_IsConnected = 0;
}
break;
case CODE_WIFI_ON_CONNECTING:
{
blog_info("[APP] [EVT] Connecting %lld", aos_now_ms());
}
break;
case CODE_WIFI_CMD_RECONNECT:
{
blog_info("[APP] [EVT] Reconnect %lld", aos_now_ms());
}
break;
case CODE_WIFI_ON_CONNECTED:
{
blog_info("[APP] [EVT] connected %lld", aos_now_ms());
}
break;
case CODE_WIFI_ON_PRE_GOT_IP:
{
blog_info("[APP] [EVT] connected %lld", aos_now_ms());
}
break;
case CODE_WIFI_ON_GOT_IP:
{
blog_info("[APP] [EVT] GOT IP %lld", aos_now_ms());
blog_info("[SYS] Memory left is %d Bytes", xPortGetFreeHeapSize());
if (wifi_info_isSet)
{
qcloud_demo_handle = hal_hwtimer_create(100, Start_Qcloud_Demo, 1);
}
else
{
q_cloud_IsConnected = 1;
}
}
break;
case CODE_WIFI_ON_EMERGENCY_MAC:
{
blog_info("[APP] [EVT] EMERGENCY MAC %lld", aos_now_ms());
hal_reboot(); // one way of handling emergency is reboot. Maybe we should also consider solutions
}
break;
case CODE_WIFI_ON_PROV_CONNECT:
{
blog_info("[APP] [EVT] [PROV] [CONNECT] %lld", aos_now_ms());
blog_info("connecting to %s:%s...", sta_ssid, sta_passwd);
wifi_sta_connect(sta_ssid, sta_passwd);
}
break;
case CODE_WIFI_ON_PROV_DISCONNECT:
{
blog_info("[APP] [EVT] [PROV] [DISCONNECT] %lld", aos_now_ms());
}
break;
case CODE_WIFI_ON_AP_STA_ADD:
{
blog_info("[APP] [EVT] [AP] [ADD] %lld, sta idx is %lu", aos_now_ms(), (uint32_t)event->value);
}
break;
case CODE_WIFI_ON_AP_STA_DEL:
{
blog_info("[APP] [EVT] [AP] [DEL] %lld, sta idx is %lu", aos_now_ms(), (uint32_t)event->value);
}
break;
default:
{
blog_info("[APP] [EVT] Unknown code %u, %lld", event->code, aos_now_ms());
/*nothing*/
}
break;
}
}
/**
* @brief 直接串口打印系统任务管理器
*/
void show_task_state_task(void* param)
{
char* pcWriteBuffer = malloc(1024);
for (;;)
{
blog_info("=============================================");
blog_info("name \t\tstatus \tprio \tfree \tpid");
vTaskList((char*)pcWriteBuffer);
blog_info("%s", pcWriteBuffer);
blog_info("\n=============================================");
blog_info("=====================================");
blog_info("FreeHeapSize is %d bytes", xPortGetFreeHeapSize());
blog_info("MinimumEverFreeHeapSize is %d bytes", xPortGetMinimumEverFreeHeapSize());
blog_info("=====================================");
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
static void proc_main_entry(void* pvParameters)
{
easyflash_init();
aos_register_event_filter(EV_WIFI, event_cb_wifi_event, NULL);
hal_wifi_start_firmware_task();
/*Trigger to start Wi-Fi*/
aos_post_event(EV_WIFI, CODE_WIFI_ON_INIT_DONE, 0);
vTaskDelete(NULL);
}
int main(void)
{
led_gpio_init(LED_PIN);
key_init(KEY_PIN);
restore_key_init(restore_key);
bl_sys_init();
xTaskCreate(proc_main_entry, (char*)"main_entry", 1024, NULL, 15, NULL);
// xTaskCreate(show_task_state_task, (char *)"status_show", 512, NULL, 8, NULL);
tcpip_init(NULL, NULL);
blog_info("qcloud demo running");
return 0;
}常见问题与踩坑提示
⚠️ 一直没出现 Cloud Device Construct Success(连不上云平台)
原因:三元组没改全或改错组(HAL_Device_freertos.c 里 softAP 组与蓝牙组各有一套,默认只编译蓝牙组)、产品品类与代码属性不符
解决:核对蓝牙组(WIFI_PROV_BT_COMBO_CONFIG_ENABLE 分支)的 sg_product_id/sg_device_name/sg_device_secret 与控制台一致(注意区分大小写);产品品类建议选「智慧生活 → 电工照明 → 灯」,数据模板需包含开关等属性
⚠️ 腾讯连连小程序扫描不到设备 / 配网失败
原因:开发板没进配网模式(LED 未闪烁)、手机蓝牙/定位权限未开、或产品配网方式没选「标准 BLE 辅助」
解决:确认烧录后 LED 是否闪烁(闪烁 = 在配网模式);小程序授权蓝牙与定位权限;控制台产品「配网方式」勾选「标准 BLE 辅助」;手机靠近开发板再试
⚠️ 配网成功后一直 Connecting 不 GOT IP(连不上路由器)
原因:配网时 Wi-Fi 密码输错、路由器是 5GHz 频段、或信号过弱
解决:长按 GPIO4 键约 5 秒清除已保存的 Wi-Fi 配置,重新配网;确认路由器开 2.4GHz;开发板靠近路由器;可先单独跑 连接 Wi-Fi 验证联网
⚠️ 按键后小程序里开关状态没变化
原因:按键对应 GPIO8 没接好、或上报的属性与产品数据模板不一致
解决:确认 KEY 按键接 GPIO8、LED 接 GPIO14;检查上报 JSON 里的属性名与「产品 → 数据模板」中的定义一致
⚠️ 串口找不到设备 / 打不开
原因:USB 转串口驱动未装、权限不足,或数据线只能充电不能传数据
解决:Linux 用 lsusb/dmesg 查看设备,权限不足执行 sudo usermod -aG dialout $USER 后重新登录,或 sudo chmod 666 /dev/ttyUSB0;Windows 装驱动后到设备管理器查 COM 口;换一根能传数据的线
⚠️ 烧录一直等待 / 失败
原因:未进入下载模式、波特率不对、或串口号填错
解决:烧录时按提示长按 EN 键进入下载模式;确认 p=/dev/ttyUSB0 换成你实际的串口;换 USB 口或数据线重试
运行自检
腾讯连连小程序配网成功、串口出现 Cloud Device Construct Success,按 KEY 键串口打印 data template reporte success,且小程序里设备在线、开关状态与 LED 同步变化(云端下发也能点亮 LED),即接入腾讯云物联网平台验证通过。
遇到问题?
如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

