设备端接入

设备接入

接入方式

设备接入架构图

如上图所示,涂鸦云支持设备以HTTPS、MQTT方式接入涂鸦云。HTTPS接入的场景主要有:获取设备配置信息、获取设备升级信息等。MQTT接入等场景有:订阅设备控制指令、发布设备状态。

名词解释:

  • HTTP/HTTPS 涂鸦的HTTP(ATOP网关) API是基于HTTP或HTTPS协议来调用的,开发者可以根据涂鸦的ATOP协议来封装HTTP请求进行调用。

  • MQTT MQTT是一个物联网传输协议,它被设计用于轻量级的发布/订阅式消息传输。MQTT是专门针对物联网开发的轻量级传输协议。涂鸦云平台支持MQTT方式调用,并提供相应的AppKey与AppSecret供用户使用。

建议:

  • 如果需要实时的控制指令或硬件上报数据类数据,推荐使用MQTT订阅。

调用入口

涂鸦云根据中国企业内外销区域结合海底光缆分布和全球各城市的实测结果,部署覆盖亚、欧、美三个可用区。

调用API的服务域名如下:

可用区 协议 域名 数据安全级别 服务区域
AY http/https/mqtt *.tuyacn.com HTTPS+AES 亚洲
AZ http/https/mqtt *.tuyaus.com HTTPS+AES 美洲
EU http/https/mqtt *.tuyaeu.com HTTPS+AES 欧洲

注: 涂鸦云提供Http、Https、Mqtt等多种通信协议,根据业务需求可以灵活选择使用

  • https: a1.tuya(cn/eu/us).com/api.json (APP调用,如:https://a1.tuyacn.com/api.json)
  • http: a.gw.tuya(cn/eu/us).com/gw.json (硬件调用,如:http://a.gw.tuyacn.com/gw.json)
  • mqtt: mq.mb.tuya(cn/eu/us).com(APP调用,如:tcp://mq.mb.tuyacn.com)
  • mqtt: mq.gw.tuya(cn/eu/us).com(硬件调用,如:tcp://mq.gw.tuyacn.com)

协议概述

  • HTTP/HTTPS

    • 调用流程 根据ATOP的协议,通过HTTP/HTTPS调用云服务的详细步骤为:填充参数 > 生成签名 > 拼装HTTP请求 > 发起HTTP请求> 得到HTTP响应 > 解释json结果。

    大致的调用过程如下图所示:

    HTTP调用

    • 公共参数 调用任何一个API都必须传入的公共参数有:
参数名称 参数类型 是否必须 是否签名 参数描述
a String API名称
v String API接口版本
t String 时间戳,格式为数字,大小到秒非毫秒,时区为标准时区,例如:1458010495。API服务端允许客户端请求最大时间误差为540分钟。
sign String API输入参数签名结果,签名算法参照下面的介绍
devId String 设备Id,每次调用注册接口会返回新的devId,设备激活后的API调用都需要设置该参数
uuid String 设备唯一标识符,设备激活前的API调用必须设置该参数

注意:对于非必填参数且参与签名的,在值为空的情况下不参与签名,只有在有值情况下才参与签名。

  • MQTT

    • 调用流程 根据MQTT的协议,通过MQTT与云端交互的详细步骤为:使用MQTT客户端发起连接–>用户名密码连接认证–>Topic订阅–>心跳维持。

    大致的调用过程如下图所示: MQTT MQTT

接入教程

名字解释

名词 描述
uuid 设备唯一标识,在产测的时候由云端生成写入模块
authKey 设备授权码,在产测的时候由云端生成写入模块
hid 设备硬件标识,GPRS类设备由IMEI码表示,其它设备由MAC表示
devId 设备与云端通讯标识,通过云端提供的注册接口获取设备与云端通讯的标识,并且必须通过active接口激活后才可以使用
secKey 设备与云端通讯的密钥,通过云端激活devId时获取
localKey 设备与云端MQTT服务器通讯数据AES加密密钥,通过云端激活devId时同secKey一并返回

HTTP/HTTPS接入方式

业务参数

API调用除了包含公共参数外,每个API本身可能有具体业务相关的参数也需要传入(每个API的业务级参数请考API列表)。业务参数全部通过URL参数data传到服务端,例如API有两个业务参数devId和dps,则URL格式为data={“devId”:“klsdjflkasdjflkjdsalfkjd”,“dps”:{“1”:true}}。

注意:

  • 业务参数在传输之前需要进行加密处理,但是不参与签名。
  • 加密key:激活前相关接口key为设备的authKey取前16个字符,激活后的相关接口key使用secKey 业务参数加密采用AES算法(加密位数:128位,加密模式:ecb模式),AES加密之后还需要进行16进制转换,具体如下:
void AES128_ECB_encrypt(uint8_t* input, const uint8_t* key, uint8_t* output)
{
// Copy input to output, and work in-memory on output
MutexLock(mutex);
BlockCopy(output, input);
state = (state_t*)output;

Key = key;
KeyExpansion();

// The next function call encrypts the PlainText with the Key using AES algorithm.
Cipher();
MutexUnLock(mutex);
}

其它参数

API调用除公共参数,业务参数外,还提供了URL参数other(其他参数)。之所以还需要其他参数,是由于业务参数在传输时需要进行加密,所以一般只将敏感数据放入到业务参数。对于非敏感业务参数可以放到其它参数减少加密的数据,提高解密和加密的效率,另外对于一些接口解密业务参数需要用到的关联信息也需要放到其它参数。其它参数全部放入URL参数other传到服务端,例如API有其它参数token,则格式为other={“token”:“khuyghyt”}。

注意:

  • 其它参数需要参与签名。

请求签名

为了防止API调用过程中被黑客恶意篡改,调用任何一个API都需要携带签名,ATOP服务端会根据请求参数,对签名进行验证,签名不合法的请求将会被拒绝。

注意:对于需要参与签名的参数(具体参考每个具体的API),在值为空的情况下不参与签名,只有在有值情况下才参与签名。

下面详细描述签名算法的每个步骤,假定API请求参数如下:

a = tuya.device.dp.report
v = 1.0
t = 1431078303
devId = klsdjflkasdjflkjdsalfkjd
data = D5601F956DC556546EE584B43F5E5BF88C0D580DE848B10385F1152B5F051F7568A4CE3136FBA36076B866431674CA07A6BAFFBC33AA8F964E32C609B894665A --注意:data是加密后的数据,明文为:{"devId":"klsdjflkasdjflkjdsalfkjd","dps":{"1":true}}
other = {"token":"khuyghyt"}

由于业务参数data不参与签名,所以按参数名称的字典顺序排序后的结果如下:

a = tuya.device.dp.report
devId = klsdjflkasdjflkjdsalfkjd
other = {"token":"khuyghyt"}
t = 1431078303
v = 1.0

将排序好的参数名和参数值拼装在一起,参数间通过||连接,得到的结果为:

a=tuya.device.dp.report||devId=klsdjflkasdjflkjdsalfkjd||other={"token":"khuyghyt"}||t=1431078303||v=1.0

在得到的签名输入字符串之后再拼接上设备与云端通讯的密钥(每次激活后会返回的secKey),假定密钥是qwertu87tyredser,则最后的签名字符串为:

a=tuya.device.dp.report||devId=klsdjflkasdjflkjdsalfkjd||other={"token":"khuyghyt"}||t=1431078303||v=1.0||qwertu87tyredser

最后把拼装好的签名字符串采用utf-8编码,对编码后的字节流进行MD5摘要。

签名特别注意事项:

  • 设备激活前的API调用使用的密钥是产测的时候授予每个设备的授权key(authKey)的前16个字符,设备激活后的API调用使用的密钥是激活后返回的secKey。

  • 签名的MD5摘要算法必须使用下面的MD5摘要算法,与标准MD5算法有差别。

#include <string.h>
#include "md5.h"

unsigned char PADDING[]={0x80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

void md5_init(MD5_CTX *context)
{
context->count[0] = 0;
context->count[1] = 0;
context->state[0] = 0x67452301;
context->state[1] = 0xEFCDAB89;
context->state[2] = 0x98BADCFE;
context->state[3] = 0x10325476;
}
void md5_update(MD5_CTX *context,unsigned char *input,unsigned int inputlen)
{
unsigned int i = 0,index = 0,partlen = 0;
index = (context->count[0] >> 3) & 0x3F;
partlen = 64 - index;
context->count[0] += inputlen << 3;
if(context->count[0] < (inputlen << 3))
context->count[1]++;
context->count[1] += inputlen >> 29;

if(inputlen >= partlen)
{
memcpy(&context->buffer[index],input,partlen);
md5_transform(context->state,context->buffer);
for(i = partlen;i+64 <= inputlen;i+=64)
md5_transform(context->state,&input[i]);
index = 0;
}
else
{
i = 0;
}
memcpy(&context->buffer[index],&input[i],inputlen-i);
}

void md5_final(MD5_CTX *context,unsigned char digest[16])
{
unsigned int index = 0,padlen = 0;
unsigned char bits[8];
index = (context->count[0] >> 3) & 0x3F;
padlen = (index < 56)?(56-index):(120-index);
md5_encode(bits,context->count,8);
md5_update(context,PADDING,padlen);
md5_update(context,bits,8);
md5_encode(digest,context->state,16);
}

void md5_encode(unsigned char *output,unsigned int *input,unsigned int len)
{
unsigned int i = 0,j = 0;
while(j < len)
{
output[j] = input[i] & 0xFF;
output[j+1] = (input[i] >> 8) & 0xFF;
output[j+2] = (input[i] >> 16) & 0xFF;
output[j+3] = (input[i] >> 24) & 0xFF;
i++;
j+=4;
}
}
void md5_decode(unsigned int *output,unsigned char *input,unsigned int len)
{
unsigned int i = 0,j = 0;
while(j < len)
{
output[i] = (input[j]) |
(input[j+1] << 8) |
(input[j+2] << 16) |
(input[j+3] << 24);
i++;
j+=4;
}
}
void md5_transform(unsigned int state[4],unsigned char block[64])
{
unsigned int a = state[0];
unsigned int b = state[1];
unsigned int c = state[2];
unsigned int d = state[3];
unsigned int x[64];
md5_decode(x,block,64);
FF(a, b, c, d, x[ 0], 7, 0xd76aa478);
FF(d, a, b, c, x[ 1], 12, 0xe8c7b756);
FF(c, d, a, b, x[ 2], 17, 0x242070db);
FF(b, c, d, a, x[ 3], 22, 0xc1bdceee);
FF(a, b, c, d, x[ 4], 7, 0xf57c0faf);
FF(d, a, b, c, x[ 5], 12, 0x4787c62a);
FF(c, d, a, b, x[ 6], 17, 0xa8304613);
FF(b, c, d, a, x[ 7], 22, 0xfd469501);
FF(a, b, c, d, x[ 8], 7, 0x698098d8);
FF(d, a, b, c, x[ 9], 12, 0x8b44f7af);
FF(c, d, a, b, x[10], 17, 0xffff5bb1);
FF(b, c, d, a, x[11], 22, 0x895cd7be);
FF(a, b, c, d, x[12], 7, 0x6b901122);
FF(d, a, b, c, x[13], 12, 0xfd987193);
FF(c, d, a, b, x[14], 17, 0xa679438e);
FF(b, c, d, a, x[15], 22, 0x49b40821);


GG(a, b, c, d, x[ 1], 5, 0xf61e2562);
GG(d, a, b, c, x[ 6], 9, 0xc040b340);
GG(c, d, a, b, x[11], 14, 0x265e5a51);
GG(b, c, d, a, x[ 0], 20, 0xe9b6c7aa);
GG(a, b, c, d, x[ 5], 5, 0xd62f105d);
GG(d, a, b, c, x[10], 9, 0x2441453);
GG(c, d, a, b, x[15], 14, 0xd8a1e681);
GG(b, c, d, a, x[ 4], 20, 0xe7d3fbc8);
GG(a, b, c, d, x[ 9], 5, 0x21e1cde6);
GG(d, a, b, c, x[14], 9, 0xc33707d6);
GG(c, d, a, b, x[ 3], 14, 0xf4d50d87);
GG(b, c, d, a, x[ 8], 20, 0x455a14ed);
GG(a, b, c, d, x[13], 5, 0xa9e3e905);
GG(d, a, b, c, x[ 2], 9, 0xfcefa3f8);
GG(c, d, a, b, x[ 7], 14, 0x676f02d9);
GG(b, c, d, a, x[12], 20, 0x8d2a4c8a);


HH(a, b, c, d, x[ 5], 4, 0xfffa3942);
HH(d, a, b, c, x[ 8], 11, 0x8771f681);
HH(c, d, a, b, x[11], 16, 0x6d9d6122);
HH(b, c, d, a, x[14], 23, 0xfde5380c);
HH(a, b, c, d, x[ 1], 4, 0xa4beea44);
HH(d, a, b, c, x[ 4], 11, 0x4bdecfa9);
HH(c, d, a, b, x[ 7], 16, 0xf6bb4b60);
HH(b, c, d, a, x[10], 23, 0xbebfbc70);
HH(a, b, c, d, x[13], 4, 0x289b7ec6);
HH(d, a, b, c, x[ 0], 11, 0xeaa127fa);
HH(c, d, a, b, x[ 3], 16, 0xd4ef3085);
HH(b, c, d, a, x[ 6], 23, 0x4881d05);
HH(a, b, c, d, x[ 9], 4, 0xd9d4d039);
HH(d, a, b, c, x[12], 11, 0xe6db99e5);
HH(c, d, a, b, x[15], 16, 0x1fa27cf8);
HH(b, c, d, a, x[ 2], 23, 0xc4ac5665);


II(a, b, c, d, x[ 0], 6, 0xf4292244);
II(d, a, b, c, x[ 7], 10, 0x432aff97);
II(c, d, a, b, x[14], 15, 0xab9423a7);
II(b, c, d, a, x[ 5], 21, 0xfc93a039);
II(a, b, c, d, x[12], 6, 0x655b59c3);
II(d, a, b, c, x[ 3], 10, 0x8f0ccc92);
II(c, d, a, b, x[10], 15, 0xffeff47d);
II(b, c, d, a, x[ 1], 21, 0x85845dd1);
II(a, b, c, d, x[ 8], 6, 0x6fa87e4f);
II(d, a, b, c, x[15], 10, 0xfe2ce6e0);
II(c, d, a, b, x[ 6], 15, 0xa3014314);
II(b, c, d, a, x[13], 21, 0x4e0811a1);
II(a, b, c, d, x[ 4], 6, 0xf7537e82);
II(d, a, b, c, x[11], 10, 0xbd3af235);
II(c, d, a, b, x[ 2], 15, 0x2ad7d2bb);
II(b, c, d, a, x[ 9], 21, 0xeb86d391);
state[0] += a;
state[1] += b;
state[2] += c;
state[3] += d;
}

调用示例

以tuya.device.dp.report调用为例(假定设备的云端通信密钥为:qwertu87tyredser),具体步骤如下:

1.设置参数值

公共参数:

a = tuya.device.dp.report
v = 1.0
t = 1431078303
devId = klsdjflkasdjflkjdsalfkjd

业务参数:

data={"devId":"klsdjflkasdjflkjdsalfkjd","dps":{"1":true}}
加密后数据
data = D5601F956DC556546EE584B43F5E5BF88C0D580DE848B10385F1152B5F051F7568A4CE3136FBA36076B866431674CA07A6BAFFBC33AA8F964E32C609B894665A

其他参数:

other = {"token":"khuyghyt"}

2.按参数的字典顺序排序

a = tuya.device.dp.report
devId = klsdjflkasdjflkjdsalfkjd
other = {"token":"khuyghyt"}
t = 1431078303
v = 1.0

3.拼接参数名与参数值及通信密钥

a=tuya.device.dp.report||devId=klsdjflkasdjflkjdsalfkjd||other={"token":"khuyghyt"}||t=1431078303||v=1.0||qwertu87tyredser

4.生成签名

md5(上一步生成的签名字符串按UTF-8编码获得字节数组) = 9e4e861940eb1c10b43842e6d6eedea2

5.组装HTTP请求

将所有参数名和参数值采用utf-8进行URL编码(参数顺序可随意,但必须要包括签名参数),然后通过GET或POST发起请求,假定请求中国区,则URL如下:

http://a.gw.tuyacn.com/gw.json?a=tuya.device.dp.report&devId=klsdjflkasdjflkjdsalfkjd&other={"token":"khuyghyt"}&t=1431078303&v=1.0&data=D5601F956DC556546EE584B43F5E5BF88C0D580DE848B10385F1152B5F051F7568A4CE3136FBA36076B866431674CA07A6BAFFBC33AA8F964E32C609B894665A&sign=9e4e861940eb1c10b43842e6d6eedea2

注意事项

  • 所有的请求和响应数据编码皆为utf-8格式,URL里的所有参数名和参数值请做URL编码
  • 请求不同的可用区,请使用相应的域名

注意 为什么要提供多个不同的域名:

  • 不同的域名可以由不同的DNS服务商进行解析,以提供各区域最好的解析稳定性和加速服务。
  • 可以更有效的避免运营商劫持问题。
  • 减少解析次数,可以更稳定的优化部分偏远地区的DNS服务商性能问题。

MQTT方式接入

设备云端MQTT BROKER

  • 通过MQTT主动推送各种指令给联网模块,模块也可以通过MQTT主动上报数据给云端和APP。
  • 连接URL:WIFI设备通过用API(tuya.device.config.get)获取对应可用区的URL

MQTT CLIENT连接参数设置

参数名 设置值 描述
ClientId 设备注册后返回的devId client标识
UserName 设备注册后返回的devId 用户名
Password MD5(设备激活后返回的seckey),算法:取32位中的中间16位,即8-24位 密码
ProtocolVersion 4 协议版本号,4表示MQTT3.1.1版本
CleanSession 1 客户端断开不保持要推送的消息
KeepAlive 必须大于30秒 心跳检测时间间隔
Retain 0 MQTT服务器不保持消息
Qos 1 消息QOS Level设置为1,至少一次
Will Flag 1 设置遗愿消息
Will Qos 1 遗愿消息QOS Level设置为1,至少一次
Will Retain 0 MQTT服务器不保持遗愿消息
Wil Topic tuya/smart/will 遗愿TOPIC
Will Message {“clientId”:“为设备注册后返回的devId”,“deviceType”:“GATEWAY”} 遗愿消息体内容

连接返回值

返回值 16进制 描述
0 0x00 Connection Accepted
1 0x01 Connection Refused: unacceptable protocol version
2 0x02 Connection Refused: identifier rejected
3 0x03 Connection Refused: server unavailable
4 0x04 Connection Refused: bad user name or password
5 0x05 Connection Refused: not authorized

TOPIC规则

每个设备的topic按vid进行区分:
指令接收:smart/device/in/devId,通过订阅该topic接收来自APP和云端的控制指令
上报数据:smart/device/out/devId,通过该topic发送上报数据

MQTT消息格式

MQTT数据交互的数据格式如下

协议版本号(3位) md5签名(16位) aes加密数据(base64编码)
2.1 6358012863580128 1rACuvQlqIHDjpzZF5hqvPLdWu0bd7SKADwzK893

MQTT消息体payload的最终传输数据是 2.163580128635801281rACuvQlqIHDjpzZF5hqvPLdWu0bd7SKADwzK893

MQTT消息加密和签名算法

假设设备对应的localKey为8bb486f35dbc57dd(注意:这里不是secKey)

1、数据加密:aes(二进制转为字符串时“base64编码”)

base64代码示例
char * base64_encode( const unsigned char * bindata, char * base64, int binlength )
{
int i, j;
unsigned char current;

for ( i = 0, j = 0 ; i < binlength ; i += 3 )
{
current = (bindata[i] >> 2) ;
current &= (unsigned char)0x3F;
base64[j++] = base64char[(int)current];

current = ( (unsigned char)(bindata[i] << 4 ) ) & ( (unsigned char)0x30 ) ;
if ( i + 1 >= binlength )
{
base64[j++] = base64char[(int)current];
base64[j++] = '=';
base64[j++] = '=';
break;
}
current |= ( (unsigned char)(bindata[i+1] >> 4) ) & ( (unsigned char) 0x0F );
base64[j++] = base64char[(int)current];

current = ( (unsigned char)(bindata[i+1] << 2) ) & ( (unsigned char)0x3C ) ;
if ( i + 2 >= binlength )
{
base64[j++] = base64char[(int)current];
base64[j++] = '=';
break;
}
current |= ( (unsigned char)(bindata[i+2] >> 6) ) & ( (unsigned char) 0x03 );
base64[j++] = base64char[(int)current];

current = ( (unsigned char)bindata[i+2] ) & ( (unsigned char)0x3F ) ;
base64[j++] = base64char[(int)current];
}
base64[j] = '\0';
return base64;
}

2、防篡改签名:md5(将生成的32位字符串,转为16位字符串,算法:取32位中的中间16位,即8-24位)

将每个参数格式化为"key=val",进行组装(使用key升序),组装后的字符串格式如:k1=v1||k2=v2,然后加上密钥如:k1=v1||k2=v2…kn=vn||key,进行整串字符串的MD5。 参数包括:data、pv 及密钥,其中data是aes加密后的数据,pv是当前模块的通讯协议版本号,密钥是设备注册后返回的localkey

md5前的数据串str:data=YzE/13Vp6p84PA1dV/1rACuvQlqIDsHDjpzZF5hqvPLdWu0bd7SKADwzK893HfHKMl4rdHb5Qc1qPOqfSFVc1ceQGhvwDO7pqCLmArcUpYDSEiSjFCfRKh1hnsbZrXEj||pv=2.1||8bb486f35dbc57dd
sign:md5(str) 结果为:326053f1f965e98d6db781a6010def3b
取32位中的中间16位,即8-24位,结果为 f965e98d6db781a6

MQTT交互数据格式

协议号(不同协议号代表了不同的功能)

协议号 描述
4 设备数据上报
5 设备指令下发
11 云端发起设备移除操作
注意:如果通讯的数据里面有type属性且值为reset_factory,表示设备已经被恢复出厂设置,这个时候需要重新注册设备才可以再激活设备。否则只需要重新再激活设备即可,不需要再次注册设备。
15 用户确认进行设备固件升级,通知设备准备升级固件
16 固件升级进度
17 设备上报状态信息
18 设备向云端请求数据,不同的数据类型通过reqType区分
19 云端向设备下发请求的数据,不同的数据类型通过reqType区分
21 设备重新连接新的可用区

数据交互格式

{
"protocol": 4,
"t": 1459168450,
"data": {
"devId": "002dr00118fe34d9a124",--要上报的设备ID,子设备对应子设备id(下同)
"dps": {
"1": true,
"2":30,
"3":""
}
}
}
{
"protocol": 5,
"t": 1459168450,
"data": {
"devId": "002dr00118fe34d9a124",--要下发的设备ID,子设备对应子设备id(下同)
"dps": {
"1": true,
"2":30,
"3":""
}
}
}
{
"protocol": 11,
"type":"reset_factory", --注意:type有值且值为“reset_factory”表示恢复出厂设置,否则表示移除设备
"t": 1459168450,
"data": {
"devId": "002dr00118fe34d9a124"
}
}
{
"protocol": 15
"t": 1459168450,
"data": {
"devId": "002dr00118fe34d9a124"
}
}
{
"protocol": 17
"t": 1459168450,
"data": {
"devId":"002dr00118fe34d9a124",
"softVer":"设备固件版本号(可以不填)",
"protocolVer":"设备固件通讯协议版本号(可以不填)",
"baselineVer":"设备固件基线版本号(可以不填)",
"mcuVer":"设备MCU版本号(可以不填)"
}
}
{
"protocol": 18
"t": 1459168450,
"data": {
"reqType":"cloud_time" --目前仅支持cloud_time,向云端请求云端时间,设备可以用于校准本地时间
}
}
{
"protocol": 19
"t": 1459168450,
"data": {
"reqType":"cloud_time",--返回请求的reqType,方便比对
"time": 1459168450,
"validTime": 1800
}
}
{
"protocol": 21
"t": 1459168450,
"data": {
"devId": "002dr00118fe34d9a124",
"url":{"apiUrl":"api调用url地址","mqttUrl":"mqtt连接url地址"}
}
}

电话咨询

在线咨询

400-881-8611