更新时间:2023-09-06 10:41:15下载pdf
本文详细介绍 SDK 上无线配网功能,实现网关设备接入到涂鸦生态。无线配网仅支持 AP 配网,快速配网(EZ 配网)功能已经下架,后续不再支持。
配网流程的业务逻辑已经在 SDK 内部实现,SDK 定义一套 TuyaOS Kernel Layer(简称 TKL)接口来屏蔽硬件和系统差异,TKL 接口由开发者实现,在 TKL 接口完成底层硬件的操作。本文档将提供 TKL 接口开发指导。
无线配网是把网关类设备连接到接入以太网的 Wi-Fi 路由上,从而连接到涂鸦 IoT 云的行为。需要输入路由器热点名称与密码。
AP 配网也称 热点配网 。其工作原理是,网关设备开启无线 AP,手机连接网关设备的 AP,使得手机和网关设备处于同一局域网下,设备把它的设备信息经过加密处理后,定期在局域网上发送 UDP 广播数据包。
用户在手机 App 上输入路由器的 SSID 和密码后,手机 App 与设备建立 TLS 连接,把配网授权信息以及路由器的 SSID 和密码发送给设备。设备解析出路由器 SSID 和密码后,切换到 Station 模式连接路由器,连接成功后走激活流程,直到配网完成。
AP 配网的流程图如下图所示:
无线配网过程中,SDK 需要获取和切换无线工作模式,开启和关闭 AP,连接路由器,获取无线连接状态,获取无线接口 IP 地址等,需要开发者适配以下 TKL(Tuya kernel layer)接口。
tkl_wifi_init
OPERATE_RET tkl_wifi_init(WIFI_EVENT_CB cb);
SDK 初始化时会调用该接口,该接口的参数是无线连接状态变化的通知回调函数指针,需要应用实时监控无线连接的状态变化。当无线连接状态发生变化时,使用通知回调函数指针把其状态传递给 SDK。
举例,当成功连接到路由器并分配到 IP 地址时,执行 cb(WFE_CONNECTED, NULL)
。当连接路由器失败时,执行 cb(WFE_CONNECT_FAILED, NULL)
。当断开路由器连接时,执行 cb(WFE_DISCONNECTED, NULL)
。
tkl_wifi_start_ap
OPERATE_RET tkl_wifi_start_ap(CONST WF_AP_CFG_IF_S *cfg);
SDK 初始化时,若设备处于未配网状态,则会调用该接口,用于开启设备的 AP,允许手机连接,建立局域网通讯来传输配网信息。应用需要在接口中实现启动 AP 的功能。
tkl_wifi_stop_ap
OPERATE_RET tkl_wifi_stop_ap(VOID_T);
当 SDK 配网结束时,会调用该接口,用于关闭设备的 AP。应用需要在接口中实现关闭 AP 的功能。
tkl_wifi_set_work_mode
OPERATE_RET tkl_wifi_set_work_mode(CONST WF_WK_MD_E mode);
当 SDK 需要切换无线的工作模式时,会调用该接口,通常是在 tkl_wifi_stop_ap
接口之前调用该接口设置 AP 模式,在 tkl_wifi_station_connect
之前调用该接口设置 Station 模式。应用需要在接口中根据 mode
参数把无线切换到对应的工作模式。
tkl_wifi_get_work_mode
OPERATE_RET tkl_wifi_get_work_mode(WF_WK_MD_E *mode);
当 SDK 需要获取无线的工作模式时,会调用该接口。返回的工作模式要求与 tkl_wifi_set_work_mode
接口设置的一致,否则可能会影响配网流程。
tkl_wifi_station_connect
OPERATE_RET tkl_wifi_station_connect(CONST SCHAR_T *ssid, CONST SCHAR_T *passwd);
当 SDK 接收到来自手机 App 的配网信息后,会调用该接口。应用需要在接口中实现连接路由器的功能。
tkl_wifi_station_disconnect
OPERATE_RET tkl_wifi_station_disconnect(VOID_T);
当 SDK 需要断开路由器连接时,会调用该接口。应用需要在接口中实现断开路由器的功能。
tkl_wifi_station_get_status
OPERATE_RET tkl_wifi_station_get_status(WF_STATION_STAT_E *stat);
当 SDK 需要获取当前无线连接状态时,会调用该接口。SDK 需要获取到 WSS_GOT_IP
状态才会走配网流程,所以当设备连接到路由器并且分配到 IP 地址,需要返回 WSS_GOT_IP
。
tkl_wifi_get_ip
OPERATE_RET tkl_wifi_get_ip(CONST WF_IF_E wf, NW_IP_S *ip);
当 SDK 需要获取当前无线接口的 IP 地址时,会调用该接口。应用需要在接口中根据 wf
参数来获取 AP 或 Station 接口的 IP 地址。
本文档使用 wpa_supplicant
、hostapd
、udhcpc
以及 udhcpd
来实现 AP 配网功能。代码仅提供一种实现的思路,开发者需要根据自己的无线芯片进行适配和优化。
使用无线配网接口初始化 SDK,并把无线工作模式设置为 WF_START_AP_ONLY
。
// ...
int main(int argc, char **argv)
{
// ...
#if defined(GW_SUPPORT_WIRED_WIFI) && (GW_SUPPORT_WIRED_WIFI==1)
TUYA_CALL_ERR_RETURN(tuya_iot_wired_wf_sdk_init(IOT_GW_NET_WIRED_WIFI, GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#elif defined(WIFI_GW) && (WIFI_GW==1)
TUYA_CALL_ERR_RETURN(tuya_iot_wf_sdk_init(GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#else
// 有线 SDK,不支持无线配网
return OPRT_COM_ERROR;
#endif
// ...
}
实现 tkl_wifi_init
接口,在接口中实现开启线程监控无线连接状态,并把通知回调函数指针保存到全局变量。无线连接状态发生变化时(主要关心连接和断开状态),使用通知回调函数指针把状态传递给 SDK。
STATIC WIFI_EVENT_CB __wifi_event_cb = NULL;
STATIC BOOL_T __wifi_status(VOID)
{
FILE *fp = NULL;
CHAR_T buf[512] = {0};
WF_STATION_STAT_E stat = 0;
fp = popen("wpa_cli -i " WLAN_DEV " status", "r");
if (fp == NULL) {
return FALSE;
}
while (fgets(buf, SIZEOF(buf), fp)) {
if (!strstr(buf, "wpa_state"))
continue;
char *k = strtok(buf, "=");
char *v = strtok(NULL, "=");
if (v && !strncmp(v, "COMPLETED", strlen("COMPLETED"))) {
tkl_wifi_station_get_status(&stat);
if (stat == WSS_GOT_IP) {
return TRUE;
}
}
}
pclose(fp);
return FALSE;
}
STATIC VOID *__wifi_status_thread(VOID *arg)
{
BOOL_T cur_status = FALSE, lst_status = FALSE;
while (1) {
if (g_wifi_mode != WWM_STATION) {
tkl_system_sleep(500);
continue;
}
cur_status = __wifi_status();
if (cur_status != lst_status) {
PR_DEBUG("wifi connection status changed, %d -> %d", lst_status, cur_status);
if (__wifi_event_cb) {
__wifi_event_cb(cur_status ? WFE_CONNECTED : WFE_DISCONNECTED, NULL);
}
lst_status = cur_status;
}
tkl_system_sleep(1000);
}
}
OPERATE_RET tkl_wifi_init(WIFI_EVENT_CB cb)
{
pthread_t tid;
__wifi_event_cb = cb;
pthread_create(&tid, NULL, __wifi_status_thread, NULL);
return OPRT_OK;
}
实现 tkl_wifi_set_work_mode
接口,在接口中实现根据参数设置无线的工作模式,主要是 AP 模式和 Station 模式。
OPERATE_RET tkl_wifi_set_work_mode(CONST WF_WK_MD_E mode)
{
PR_DEBUG("WiFi set mode: %d", mode);
g_wifi_mode = mode;
switch (mode) {
case WWM_STATION:
exec_command("iwconfig " WLAN_DEV " mode managed", NULL, 0);
break;
case WWM_SOFTAP:
// exec_command("iwconfig " WLAN_DEV " mode master", NULL, 0);
break;
default:
break;
}
return OPRT_OK;
}
实现 tkl_wifi_get_work_mode
接口,在接口中实现获取无线当前的工作模式,要求与 tkl_wifi_set_work_mode
接口设置的一致。为了避免出现状态不一致,直接从全局变量中取值。
OPERATE_RET tkl_wifi_get_work_mode(WF_WK_MD_E *mode)
{
*mode = g_wifi_mode;
PR_DEBUG("WiFi got mode: %d", *mode);
return OPRT_OK;
}
实现 tkl_wifi_start_ap
接口,在接口中根据参数配置 AP 和开启 AP 的功能,必须使用 cfg
参数中的 IP 地址。udhcpd
作为 DHCP 服务器,给手机分配 IP 地址,IP 地址池以及网关 IP 使用 cfg
参数的值。
OPERATE_RET tkl_wifi_start_ap(CONST WF_AP_CFG_IF_S *cfg)
{
OPERATE_RET op_ret = OPRT_OK;
INT_T len = 0;
CHAR_T buf[512] = {0};
CHAR_T cmd[128] = {0};
CHAR_T *ap_conf_fmt =
"interface=%s\n"
"ssid=%s\n"
"country_code=CN\n"
"channel=%d\n"
"beacon_int=100\n"
"max_num_sta=%d\n"
"auth_algs=3\n"
"wpa=%d\n"
"wpa_key_mgmt=WPA-PSK\n"
"wpa_pairwise=TKIP CCMP\n"
"rsn_pairwise=CCMP\n";
CHAR_T *udhcpd_conf_fmt =
"interface %s\n"
"start %s.100\n"
"end %s.200\n"
"opt subnet %s\n"
"opt lease 28800\n"
"opt router %s\n"
"opt dns %s\n"
"opt domain SmartLift\n";
INT_T seg1 = 0, seg2 = 0, seg3 = 0, seg4 = 0;
CHAR_T ip_prefix[12] = {0};
tkl_wifi_station_disconnect();
PR_DEBUG("start ap, ssid: %s, ip: %s", cfg->ssid, cfg->ip.ip);
memcpy(&g_ap_ip, &(cfg->ip), SIZEOF(NW_IP_S));
sscanf(cfg->ip.ip, "%d.%d.%d.%d", &seg1, &seg2, &seg3, &seg4);
snprintf(ip_prefix, SIZEOF(ip_prefix), "%d.%d.%d", seg1, seg2, seg3);
if (cfg->p_len > 0) {
len = snprintf(buf, SIZEOF(buf), ap_conf_fmt, WLAN_DEV, cfg->ssid, cfg->chan, cfg->max_conn, 2);
len += snprintf(buf + len, SIZEOF(buf) - len, "wpa_passphrase=%s\n", cfg->passwd);
} else {
len = snprintf(buf, SIZEOF(buf), ap_conf_fmt, WLAN_DEV, cfg->ssid, cfg->chan, cfg->max_conn, 0);
}
op_ret = save_conf(HOSTAPD_CONF, buf, len);
if (op_ret != OPRT_OK) {
PR_ERR("fail to write %s", HOSTAPD_CONF);
}
len = snprintf(buf, SIZEOF(buf), udhcpd_conf_fmt, WLAN_DEV, ip_prefix, ip_prefix, cfg->ip.mask, cfg->ip.gw, cfg->ip.gw);
op_ret = save_conf(UDHCPD_CONF, buf, len);
if (op_ret != OPRT_OK) {
PR_ERR("fail to write %s", UDHCPD_CONF);
}
snprintf(cmd, SIZEOF(cmd), "ifconfig %s %s netmask %s", WLAN_DEV, cfg->ip.ip, cfg->ip.mask);
exec_command(cmd, NULL, 0);
exec_command("ifconfig " WLAN_DEV " up", NULL, 0);
tkl_system_sleep(1000);
exec_command("hostapd -B -P /run/hostapd.pid " HOSTAPD_CONF, NULL, 0);
exec_command("killall udhcpd", NULL, 0);
exec_command("udhcpd " UDHCPD_CONF, NULL, 0);
return OPRT_OK;
}
实现 tkl_wifi_stop_ap
接口,在接口中实现关闭 AP 的功能。SDK 接收到来自手机 App 发送的配网信息后,使用该接口关闭 AP。
OPERATE_RET tkl_wifi_stop_ap(VOID_T)
{
exec_command("killall udhcpd", NULL, 0);
exec_command("killall hostapd", NULL, 0);
exec_command("ifconfig " WLAN_DEV " down", NULL, 0);
return OPRT_OK;
}
实现 tkl_wifi_station_connect
接口,在接口中实现连接路由器的功能。udhcpc
作为 DHCP 客户端从路由器获取 IP 地址。
OPERATE_RET tkl_wifi_station_connect(CONST SCHAR_T *ssid, CONST SCHAR_T *passwd)
{
OPERATE_RET op_ret = OPRT_OK;
INT_T len = 0;
CHAR_T buf[512] = {0};
CHAR_T *wpa_conf_fmt =
"ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\n"
"update_config=1\n"
"country=CN\n"
"\n"
"network={\n"
"\tssid=\"%s\"\n"
"\tpairwise=CCMP TKIP\n"
"\tkey_mgmt=WPA-PSK\n"
"\tgroup=CCMP TKIP\n"
"\tpsk=\"%s\"\n"
"}\n";
if (!ssid || !passwd) {
PR_WARN("ssid or passwd is null");
return OPRT_INVALID_PARM;
}
tkl_wifi_stop_ap();
PR_DEBUG("ssid: %s, passwd: %s", ssid, passwd);
len = snprintf(buf, SIZEOF(buf), wpa_conf_fmt, ssid, passwd);
op_ret = save_conf(WPA_SUPPLICANT_CONF, buf, len);
if (op_ret != OPRT_OK) {
PR_ERR("fail to write %s", UDHCPD_CONF);
}
exec_command("wpa_supplicant -B -Dnl80211 -i" WLAN_DEV " -c" WPA_SUPPLICANT_CONF, NULL, 0);
exec_command("udhcpc -i " WLAN_DEV " -s /etc/udhcpc/default.script -p /run/udhcpc_wlan0.pid -b", NULL, 0);
return OPRT_OK;
}
实现 tkl_wifi_station_get_status
接口,在接口中实现获取无线连接状态的功能,有可能获取到的是 AP 的 IP 地址,所以需要对 IP 地址进行过滤。只有真正分配到 IP 地址才返回 WSS_GOT_IP
状态。
OPERATE_RET tkl_wifi_station_get_status(WF_STATION_STAT_E *stat)
{
OPERATE_RET op_ret = OPRT_OK;
NW_IP_S ip = {0};
*stat = WSS_IDLE;
op_ret = tkl_wifi_get_ip(WF_STATION, &ip);
if (op_ret != OPRT_OK) {
return op_ret;
}
if ((strlen(ip.ip) > 0) && (strncmp(g_ap_ip.ip, ip.ip, strlen(ip.ip)) != 0)) {
*stat = WSS_GOT_IP;
}
return OPRT_OK;
}
实现 tkl_wifi_get_ip
接口,在接口中实现获取无线接口的 IP 地址功能。
OPERATE_RET tkl_wifi_get_ip(CONST WF_IF_E wf, NW_IP_S *ip)
{
struct ifreq ifr;
int sock = 0;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
PR_ERR("create socket failed");
return OPRT_COM_ERROR;
}
strncpy(ifr.ifr_name, WLAN_DEV, strlen(WLAN_DEV) + 1);
if (ioctl(sock, SIOCGIFADDR, &ifr) == 0)
strncpy(ip->ip, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr), sizeof(ip->ip));
if (ioctl(sock, SIOCGIFBRDADDR, &ifr) == 0)
strncpy(ip->gw, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_broadaddr)->sin_addr), sizeof(ip->gw));
if (ioctl(sock, SIOCGIFNETMASK, &ifr) == 0)
strncpy(ip->mask, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr), sizeof(ip->mask));
close(sock);
PR_DEBUG("WiFi ip->ip: %s", ip->ip);
return OPRT_OK;
}
实现 tkl_wifi_get_mac
接口,在接口中实现获取无线接口的 MAC 地址。
OPERATE_RET tkl_wifi_get_mac(CONST WF_IF_E wf, NW_MAC_S *mac)
{
int i;
int fd = -1;
struct ifreq ifr;
struct sockaddr *addr;
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
PR_ERR("socket failed");
return OPRT_SOCK_ERR;
}
memset(&ifr, 0, SIZEOF(ifr));
strncpy(ifr.ifr_name, WLAN_DEV, SIZEOF(ifr.ifr_name) - 1);
addr = (struct sockaddr *)&ifr.ifr_hwaddr;
addr->sa_family = 1;
if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) {
PR_ERR("ioctl failed");
close(fd);
return OPRT_COM_ERROR;
}
memcpy(mac->mac, addr->sa_data, MAC_ADDR_LEN);
PR_DEBUG("WiFi mac->mac: %02X-%02X-%02X-%02X-%02X-%02X", mac->mac[0], mac->mac[1], mac->mac[2], \
mac->mac[3],mac->mac[4],mac->mac[5]);
close(fd);
return OPRT_OK;
}
其他非必要接口填充空实现。
OPERATE_RET tkl_wifi_scan_ap(CONST SCHAR_T *ssid, AP_IF_S **ap_ary, UINT_T *num)
{ return OPRT_OK; }
OPERATE_RET tkl_wifi_release_ap(AP_IF_S *ap)
{ return OPRT_OK; }
OPERATE_RET tkl_wifi_set_cur_channel(CONST UCHAR_T chan)
{ return OPRT_OK; }
OPERATE_RET tkl_wifi_get_cur_channel(UCHAR_T *chan) { return OPRT_OK; }
OPERATE_RET tkl_wifi_set_sniffer(CONST BOOL_T en, CONST SNIFFER_CALLBACK cb) { return OPRT_OK; }
OPERATE_RET tkl_wifi_set_mac(CONST WF_IF_E wf, CONST NW_MAC_S *mac) { return OPRT_OK; }
OPERATE_RET tkl_wifi_get_connected_ap_info(FAST_WF_CONNECTED_AP_INFO_T **fast_ap_info) { return OPRT_OK; }
OPERATE_RET tkl_wifi_get_bssid(UCHAR_T *mac) { return OPRT_OK; }
OPERATE_RET tkl_wifi_set_country_code(CONST COUNTRY_CODE_E ccode) { return OPRT_OK; }
OPERATE_RET tkl_wifi_set_rf_calibrated(VOID_T) { return OPRT_OK; }
OPERATE_RET tkl_wifi_set_lp_mode(CONST BOOL_T enable, CONST UCHAR_T dtim);
OPERATE_RET tkl_wifi_station_fast_connect(CONST FAST_WF_CONNECTED_AP_INFO_T *fast_ap_info) { return OPRT_OK; }
OPERATE_RET tkl_wifi_station_get_conn_ap_rssi(SCHAR_T *rssi) { return OPRT_OK; }
OPERATE_RET tkl_wifi_send_mgnt(CONST UCHAR_T *buf, CONST UINT_T len) { return OPRT_OK; }
OPERATE_RET tkl_wifi_register_recv_mgnt_callback(CONST BOOL_T enable, CONST WIFI_REV_MGNT_CB recv_cb) { return OPRT_OK; }
OPERATE_RET tkl_wifi_ioctl(WF_IOCTL_CMD_E cmd, VOID *args) { return OPRT_OK; }
TKL 接口不允许阻塞,耗时的任务建议做异步处理。
该内容对您有帮助吗?
是意见反馈该内容对您有帮助吗?
是意见反馈