======AiCloud 2.0 SDK开发教程====== 以下代码实现最简单的数据接收发送操作。 void ICACHE_FLASH_ATTR aicloud_receive(const d_object_t *object) { d_object_t *data = aicloud_object_create(); if (aicloud_receive_get(object, "switch")) { os_printf("cloud try get hardware value: switch\r\n"); aicloud_add_int(data, "switch", aicloud_io_get_relay_state()); aicloud_send(data); } if (aicloud_receive_int(object, "switch")) { os_printf("cloud try set hardware int value: switch\r\n"); aicloud_io_set_relay_state( (uint8_t)aicloud_value_int(object, "switch") ); aicloud_io_set_led_state(aicloud_io_get_relay_state()); aicloud_add_int(data, "switch", aicloud_io_get_relay_state()); aicloud_send(data); } aicloud_object_delete(data); os_printf("%s: memory left=%d\r\n", __func__, system_get_free_heap_size()); } void ICACHE_FLASH_ATTR main(void) { aicloud_run(); aicloud_on_receive(aicloud_receive); } 发送数据示例, d_object_t *data = aicloud_object_create();//初始化d_object_t对象 aicloud_add_int(data,"switch",0); //key-value, 发送数据点 aicloud_send(data); //发送数据点 aicloud_object_delete(data); //销毁对象,释放资源 ====== AiCloud 结构化数据对象理解 ====== ===== DObject 简介 ===== DObject是aicloud云平台数据对象,包含上报、读、写三种数据操作。 智能硬件向云端、APP收发数据,都是对aicloud数据对象 DObject 的操作。 DObject 在esp8266 SDK中的定义是"d_object_t". ===== DObject接收操作 ===== DObject接收发生在“收到云平台数据”回调函数中,例如“aicloud_on_receive”注册的回调函数“aicloud_receive(const d_object_t* object)”。此时收到的数据只有两种类型,即读数据请求与写数据请求。 ===== DObject读数据请求 ===== 首先判断DObject是否是读数据请求,例程中使用“if (aicloud_receive_get(object, "switch")) ”判断云平台是否请求读取“switch”数据点。如果发生读数据请求,智能硬件需要进行上报“switch”数据点操作。 ===== DObject读数据请求 ===== 首先判断DObject是否是写数据请求,例程中使用“if (aicloud_receive_set(object, "switch")) ”判断云平台是否请求写“switch”数据点。为了方便,使用“if (aicloud_receive_int(object, "switch")) ”判断云平台是否请求写整数类型的“switch”数据点。如果发生写数据请求且数据点的值发生变化,智能硬件需要进行上报“switch”数据点操作。 ===== DObject发送操作 ===== ===== DObject上报数据响应 ===== 若收到了DObject读数据请求,或者数据点数值发生变化,需要发送DObject上报数据响应。 d_object_t * data = aicloud_object_create();//首先,创建一个新的DObject对象。 aicloud_add_int(data, "switch", 0); //然后,添加需要上报的数据点。 aicloud_add_string(data, "message", "hello world"); aicloud_add_bool(data, "on", true); aicloud_add_double(data, "temp", -3.14159265); aicloud_send(data); //接下来,发送DObject对象到云端。 aicloud_object_delete(data); //最后,释放DObject对象。 ===== DObject上报据时机 ===== 例如温度传感器完成温度读取时,开关量被改变时,或者云端请求读取数据时,都应当上报指定的数据。以上规则总结如下: 云端请求读取数据时,则上报数据。 数据本身发生变化时,则上报数据。 ====== 架构理解 ====== 整个aicloud系统架构包含云架构、硬件架构、APP架构以及这些子系统之间的互联关系,主要实现设备与云之间的连接、设备与设备之间的连接、设备与用户之间的连接。 整个aicloud系统包含DOSS系统与使用系统的所有用户。 名词解释 | 名称 | 全称 | 说明 | | aicloud | aicloud | 包含DOSS以及其使用者的整个物联网系统 | | DOSS | aicloud运营支撑系统 | 包含由DMS以及其他基础设施构成的云端、各个平台的硬件以及其固件、各个平台的APP以及它们的互联关系 | | DMS | 设备管理系统 | 实现设备管理与运营的子系统 | {{aicloud:spec:12.png|}} ====== 硬件与云广域网通信协议 ====== 广域网下通过Broker实现智能硬件与APP之间的通信。 Broker(消息中转器)的作用是消息的汇总与分发,通过订阅与发布实现多对多通信。Broker实现在广域网(WAN)环境下的数据透传。 ====== 通信模型 ====== 当一个主题(Topic)被订阅者订阅(Subscribe)后,所有发布(Publish)到这个主题上的消息,将会被订阅者接收到。这就是本文档的基本消息模型,即“订阅-发布”模型。 ==== 基本概念 ==== | 中文 | 英文 | 含义 | | 主题 | Topic | 消息主题,用以集中或分发消息。 | | 订阅 | Subscribe | 接收来自某个主题的消息。若有消息发布到该主题,订阅者将收到消息。 | | 发布 | Publish | 向某个主题发布消息。所有订阅该主题的订阅者将收到此消息。 | ==== 发布与订阅关系 ==== 本文中涉及的订阅与发布者分为以下三类,分别是设备(智能硬件)、APP(用户设备)和云(服务器集群)。 设备、APP与云端订阅发布关系表 | 名称 | **Subscribe****** | **Publish****** | | 设备 | app2dev%%/{%%device_id} | dev2app%%/{%%device_id} | | APP | dev2app%%/{%%device_id} | app2dev%%/{%%device_id} | | 云端 | 关键消息到队列,不订阅任何消息 | 使用规则引擎发布消息 | 注意:所有主题均为含变量主题, {<变量名>} 对应具体设备id ==== 关键词含义说明 ==== | 名称 | 类型 | 含义 | | app2dev | 常量 | 主题:APP到设备,APP向设备发送数据使用此通道。 | | dev2app | 常量 | 主题:设备到APP,设备向APP发送数据使用此通道。 | | ser2dev | 常量 | 主题:云到设备,云端向设备推送通知使用此通道。(保留) | | dev2ser | 常量 | 主题:设备到云,设备向云端发送消息使用此通道。(保留) | | ser2app | 常量 | 主题:云到APP,云端向APP推送业务消息与升级通知使用此通道。(保留) | | app2ser | 常量 | 主题:APP到云,APP向云端发送消息使用此通道。(保留) | | {product_id} | 变量 | 产品的唯一ID,由服务器产生,永远不变。用以标识某一类产品。 | | {device_id} | 变量 | 设备的唯一ID,由服务器产生,永远不变,不允许删除。用以标识某一类产品下的某一个设备。 | | {app_id} | 变量 | 应用(APP)的唯一ID,由服务器产生,永远不变。用以标识某一类应用。(保留) | | {client_id} | 变量 | 客户端的ID,应当尽可能唯一。由客户端产生,长期不变。用以标识某一个应用下的某一个客户端。 | | {user_id} | 变量 | 用户的唯一ID,由服务器产生,永远不变,不允许删除。用以标识某一个用户。 | | {username} | 变量 | 用户的唯一名称,由用户注册。用以标识某一个用户。 | ====== 通信协议 ====== ===== MQTT协议 ===== MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是[[http://www.ibm.com/developerworks/cn/webservices/ws-mqtt/index.html|IBM 开发的一个即时通讯协议]],当前设备与云端、 APP与云端使用经过TLS加密通道的MQTT协议通信。 MQTT是轻量级的发布/订阅协议,适用于一些条件比较苛刻的环境,进行低带宽、不可靠或间歇性的通信。值得一提的是mqtt提供三种不同质量的消息服务: * Qos 0 :“至多一次”,消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。 * Qos 1 :“至少一次”,确保消息到达,但消息重复可能会发生。 * Qos 2 :“只有一次”,确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。 MQTT提供消息保留(Retained)特性,若向某个topic发消息时启用消息保留,则这条消息在服务器重启之前一直存在。旧的保留消息会被新的保留消息覆盖。保留消息会在topic被订阅时立即收到,即使发送方已经离线。 MQTT提供离线遗留消息(Last Will Testament,LWT)特性,这条消息会在broker发现设备离线后发出。一但LWT被设置,在服务器重启前这条消息一直存在。 **MQTT Client ID命名规则 ** MQTT broker同一时间不允许client_id相同的客户端同时登录,但是username与password相同不受影响,可以同时登录。为了避免多个平台下客户端相互冲突,同时为了方便统计,因此client_id必须遵守以下规则 MQTT Client ID在不同客户端下命名规则表 | 客户端 | 规则 | | 设备(智能硬件) | d:{device_id}:{type}:{uuid} | | iOS APP | u:{user_id}:ios:{uuid} | | android APP | u:{user_id}:android:{uuid} | | Web APP | u:{user_id}:web:{uuid} | | 微信 APP | u:{user_id}:wechat:{uuid} | 注意事项: uuid应当保证在应用安装后保持不变,且尽可能唯一,以便跟踪回溯。uuid的长度为12个字符,便于统计引擎分析与处理。 该uuid允许小概率重复,因此数据库设计时该字段并不唯一。 建议在app第一次启动时把unix时间戳*1000+毫秒值将其转换成十六进制字符串,然后截取前12个字节,如果不足12个字节,用随机字符串填充。(加毫秒值考虑到了同一个用户有可能在一秒内同时打开多于一个APP,使用毫秒的话就很难做到在一毫秒内同时打开多个APP,这样对于同一个用户而言就很难不唯一了)生成完毕后保存,并在APP卸载之前保证不变。 ===== 业务流程 ===== APP获取服务端在线设备列表,得到当前用户已绑定设备信息(包含{device_id})。 用户点击设备列表中远程在线的设备时,APP开始执行部署流程,获得登录MQTT broker所必要的基本信息。 APP在后台使用给定的用户名、密码与符合命名规则的client_id连接MQTT broker. APP订阅 dev2app%%/{%%device_id} ,同时APP向 app2dev%%/{%%device_id} 发布读取设备数据点请求,稍后设备将信息上报至APP. APP跳转到控制设备界面,并显示设备最新的状态。 用户通过控制设备界面操作设备的数据点,APP向 app2dev%%/{%%device_id} 发布写设备数据点请求,实现控制设备。 APP端在建立MQTT连接后,若收到设备发来的上报设备数据点消息,则更新UI上的信息。 ===== 数据模型 ===== **JSON**数据格式 JSON格式是1999年《JavaScript Programming Language, Standard ECMA-262 3rd Edition》的子集合,是现代互联网与物联网的 数据交换标准之一。本系统所有数据使用JSON数据格式,本文使用的数据结构皆基于JSON. ===== 基本数据结构 ===== 基本数据结构包含i, d, t三种元素。其中i是包序号,d是元数据,t是UNIX时间戳。 JSON基本结构体根类型说明 | 名称 | 含义 | 类型 | 备注 | | i | 数据序号 | number(int) | 由发送端生成,从0开始自增,不可使用字符串,类型必须为整数 | | d | 数据 | 对象(object)或数组(array) | 设备上报数据或APP写设备时为JSON对象,APP读设备时为JSON数组 | | t | UNIX时间戳 | number(int) | 发送端的UNIX时间戳,必须是UTC时间,类型必须为整数 | 1 **数据元** **i** i是每个数据的流水序列id,用以确认数据包的顺序。该字段由发送端生成,从0开始,每发送一个包自增1. 接收端若响应该数据包,使用i的值作为响应包的流水序列id。 注意:i必须严格为数值类型,不允许为字符串类型。例如,允许的键值对: "i":0 , 不允许的键值对: "i":"0" . 2 **数据元** **d** d是数据载体,可以是JSON对象或者JSON数组。 **当d为object时:** 此时d是一个JSON对象,包含一系列数据点key-value组合。value的类型详细见数据点类型。其代表的含义只有两种情况: 若该数据由设备发送,则是设备在上报数据。 若该数据由APP发送,则是APP在写设备数据点。 **当d为array时:** 此时d是一个JSON数组,包含一系列数据点key所组成的string array. 其代表的含义只有一种情况: 该数据由APP发送,这是APP读设备数据点请求。 3 **数据元** **t** t是发送端的UNIX时间戳,用以确认数据包产生的时间,供统计系统使用。 注意:t必须严格为数值类型,不允许为字符串类型。t必须是采用UTC时间的标准UNIX时间戳,设备与APP的时间戳误差超过15分钟时,涉及加密与认证的操作可能会出现问题。 ===== 数据点 ===== 1 **基本概念** 数据点是从设备抽象出来的基本属性,是操作设备的最小控制单元。电源的开关,温度的数值,显示的文本与图像,都可以是独立的数据点。 2 **数据点类型** 数据点在数据元d中表达,在d为JSON对象时以key-value的形式包含在d中。 其中key是数据点名称,以字符串形式表示。value是数据点的值或内容,不同的数据点类型对应不同的JSON类型。 数据点一共包含以下类型: | 类型 | JSON Value类型 | 示例 | 用途 | 可统计性 | | 布尔 | true, false | "key":true,"key":false | 用以表示开关量 | 统计友好,可以时间为x轴,布尔值为y轴绘制变化曲线 | | 数值 | number | "key":100,"key":- | 用以表示整数与浮点数 | 统计友好,可以时间为x轴,数值为y轴绘制变化曲线 | | 文本 | string | "key":"value" | 用以表示文本或字符串 | 统计不友好,可对其分类 | | 二进制 | array | "key":[10,255,20] | 用以表示短二进制串 | 统计不友好,可对其分类 | | 多媒体 | object | 详细见多媒体类型描述 | 用以表示多媒体数据 | 统计不友好,可对其分类 | 建议设计智能硬件时,尽可能考虑使用统计友好的数据类型,以便统计引擎对其处理。 **布尔类型** 布尔类型只有true和false两个值,具备最高的处理效率,对于统计友好。假定ture为1,false为0,则可以时间为x轴,布尔值为y轴绘制变化曲线。 布尔类型格式示例 { "key":true } **数值类型** 数值类型合并了整数(int)与浮点数(float)类型,对于统计友好。可以时间为x轴,数值为y轴绘制变化曲线。 数字的表示法与其它多数编程语言相似。一个数字包括一个可能带着负号的整数成分,它后面可能跟着一个小数部分或是指数部分。 不允许有八进制及十六进制形式。前面带有0也是被禁止的。 小数部分是一个小数点后跟着一个或多个阿拉伯数字。 指数部分以大写或小写的E开头,E后面可以跟一个正/负号。接着是一个或多个阿拉伯数字。 数字值不能表示成阿拉伯数字数字的序列(如Infinity和Nan是不允许的)。 数值类型格式示例 { "key":123 } **文本类型** 文本类型用来表示长度小于128字节的文本内容,若传输的文本内容可被分为几个类别,则可进行分类统计。可使用过滤关键词、支持向量机(SVM)或人工神经网络等数据挖掘、机器学习方法对文本进行分类。 文本类型格式示例 { "key":"value" } **二进制类型** 二进制类型用来表示长度小于128字节的二进制串,若传输的二进制串可被分为几个类别,则可进行分类统计。可预先指定关键的二进制串,进行匹配分类。二进制串是一种难以进行统计的数据类型,建议在条件许可的情况下使用统计友好的数据类型予以替代。 二进制类型使用JSON数组表示,数组的元素必须是number类型,number的范围是0~255的十进制整数。不允许有八进制及十六进制形式。前面带有0也是被禁止的。二进制流的每一个字节必须转换成十进制的0~255范围内的值后,才可以作为JSON数组的元素。 二进制类型格式示例 { "key":[10,255,20] } **多媒体类型** 多媒体类型是一个JSON对象,其中包含类型名与URI引用。多媒体类型可以表示图像、音频、视频、文件等长二进制流。硬件或APP收到多媒体类型的对象后,需要对类型进行判读,如果是支持的类型,则对URI内容进行解析。URI指向的可以是文件流,也可以是RTMP视频流,亦可以是Websocket,或其他自定义协议以便扩展数据类型。 其中,类型名用来表示资源的类型。URI用以表示资源的位置。 URI应满足RFC3986. 多媒体类型格式说明 | 名称 | 含义 | 类型 | 备注 | | type | 数据类型 | string | 表示多媒体引用的类型,例如mp3,jpg,mp4,dat等 | | uri | 引用地址 | string | 表示引用的多媒体数据流地址,用户需要使用自己的程序处理该地址 | 多媒体类型格式示例 { "key":{ "type":"mp3", "uri":"http://www.aicloud.com/demo.mp3" } } ===== 请求类型 ===== **读** 当APP需要读取设备数据点内容时,应当向设备发送读请求。设备收到读请求后,上报所请求的数据点数据。 APP读设备时,d的类型是包含数据点名称的JSON数组,数组的每个元素都是String类型。设备收到请求后,上报APP需要读 取的数据点参数。若JSON数组为空数组,设备不上报任何数据。 APP读智能插座switch数据点示例 { "i":1, "d":["switch"], "t":1464714257 } APP读智能灯r,g,b,cw,iw数据点示例 { "i":2, "d":["switch","r","g","b","cw","iw"], "t":1464714257 } **写** 当APP需要设置设备数据点内容时,应当向设备发送写请求。设备收到写请求后,执行相关控制操作。执行完毕后,设备上报改变的数据点的数据。 APP写设备时,d的类型是包含数据点设置信息的JSON对象。所有设置信息为 "key":value 形式。设备收到请求后,根据实际情况执行相应操作。若JSON对象为空对象,设备不执行任何操作。若key不存在,设备不执行任何操作。 APP写智能插座switch数据点示例 { "i":3, "d":{ "switch":1 }, "t":1464714257 } APP写智能灯r,g,b,cw,iw数据点示例 { "i":4, "d":{ "switch":1, "r":255, "g":255, "b":255, "cw":255, "iw":255 }, "t":1464714257 } 不同白光的解释 | 关键词 | 英文 | 中文 | 色温 | | CW | Cool White | 冷白光 | 7000K | | DW | Daylight White | 日光白 | 6000K | | NW | Nature White | 自然白 | 4000K | | IW | Incand White | 太阳光 | 2700K | **上报** 设备的数据点因为环境、人为因素发生变化时,上报变化的数据点数据。这种情形包含但不限于环境变化、物理开关变化及APP写数据点引起的变化。 设备上报数据时,d的类型是包含数据点状态信息的JSON对象。所有状态信息为 "key":value 形式。 智能插座上报状态示例 { "i":3, "d":{ "switch":1 }, "t":1464714257 } 智能灯上报状态示例 { "i":4, "d":{ "switch":1, "r":255, "g":255, "b":255, "cw":255, "iw":255 }, "t":1464714257 } ====== 硬件与APP局域网通信协议 ====== 局域网下通过UDP与TCP协议实现智能硬件与APP之间的通信。 由于UDP与TCP各自的特点互补,因此使用以下两种方式实现局域网内APP与硬件通信。 **UDP socket** 优点:支持广播和组播特性,是无连接的通信方式。 缺点:不支持加密,不保证包可靠到达(可能重包或丢包)。 适用范围: 适合应用于智能硬件与APP在局域网内的网络发现与简单通讯。 **TCP socket** 优点:支持TLS加密,保证包到达且仅到达一次,是有连接的通信方式。缺点:不支持广播,且允许APP连接数受智能硬件RAM 限制。 适用范围: 适合应用于智能硬件与APP在局域网内的复杂通讯,例如APP控制智能硬件。 ===== 业务流程 ===== ==== 通信流程 ==== *1.设备入网 第一次使用设备时需要将设备接入无线局域网。为设备设置当前用户的Wi-Fi SSID与密码,使得设备加入相应的Wi-Fi.设备入网的方式主要有: **智能连接** 通过smartconfig/esptouch/airkiss等技术,在用户不用来回切换Wi-Fi的条件下配置设备入网,简单易行。 **AP配网** 用户连接设备释放的Wi-Fi AP,将配置信息直接发送至设备。APP需要处理AP配置成功包。 **Web配网** 用户连接设备释放的Wi-Fi AP,通过浏览器打开网页,将配置信息发送至设备。 **声音配网** 设备需要特殊的硬件支持。用户通过操作APP使得扬声器发出包含编码的声音配置设备入网。 **NFC配网** 设备需要特殊的硬件支持。用户通过手机的NFC功能将配置信息发送至设备。 **蓝牙配网** 设备需要特殊的硬件支持。用户通过手机的蓝牙功能将配置信息发送至设备。 *2.发现设备 在APP对局域网中未绑定的设备进行任何操作前,需要先发现设备。 当APP端打开设备列表页的同时,APP向局域网中发送特定的UDP广播包,每秒一次。同时APP持续监听局域网中设备发来的UDP包。 局域网中的设备收到此包后,回应UDP数据包至APP,该数据包中包含设备的基本信息。 APP收到设备应答的UDP包后,检查设备是否是已绑定的设备。如果是,则设置设备在线状态为局域网在线;如果不是,将其添加到未绑定设备列表。所有列表需要根据设备id进行去重处理。 APP收到设备信息后,同时记录设备的IP地址与端口号。 APP需要处理的包有:智能连接成功包,设备启动成功通知包。 *3.绑定设备 用户首先要和设备进行绑定,才可获得设备的操作权限。 APP向设备所在的IP地址(由发现设备流程得到)发起绑定请求(UDP),将时间戳 ts 发送至设备,设备若 未获得网络时间,将使用此时间戳授时。若设备已获得网络时间,将忽略此时间戳。 设备进行权限验证,若用户将设备设置为可绑定状态,设备回应APP的绑定请求,其中包含设备id、设备的二阶密码、 设备的access_key( device_access_key )与设备的时间戳( device_ts )。 APP需要使用这些参数向云端的用户绑定接口发起请求(#1-2-),执行绑定的业务逻辑。 设备的时间戳可能和APP的时间戳不同,但相差时间大于15分钟可能造成绑定失败。 *4.连接设备(广域网控制无此步骤) 绑定成功后,APP在局域网内可向设备发起TCP连接,以便登录设备并控制设备。若设备直接断开连接,有可能设备端连接的APP数量已达上限,此时APP应当转换为广域网连接模式。 *5.登录设备(广域网控制无此步骤) 在APP与设备成功建立TCP连接后,APP向设备发起登录请求。登录时要求APP发送APP的时间戳、设备id与设备的二阶密码。若登录失败,设备会直接断开连接。 *6.控制设备 APP成功登录设备后,APP才可向设备发送控制指令并接收设备上报的数据。若长达30秒未和设备产生通讯,需要发送心跳包维持连接。收到设备发送的心跳包时,需要立刻回应心跳包。 ==== 安全策略 ==== *1.绑定设备安全策略 智能硬件可以由一个物理开关或逻辑开关决定其是否可被绑定。具体可能由物理按键,拨动开关,设备内的定时器,或APP上的虚拟开关决定。当设备处于可绑定的状态时,设备响应APP的绑定请求。当设备处于不可绑定状态时,设备不会响应APP的绑定请求。 *2.连接设备安全策略 连接成功后需要在3秒内发起登录设备请求,否则连接会被设备断开。若短时间反复连接失败超过一定次数,则设备在此期间会处于攻击保护模式,拒绝本局域网所有连接。 *3.登录设备安全策略 登录必须在连接起3秒内完成,且要求要求APP发送APP的时间戳、设备id与设备的二阶密码。若登录失败,设备会直接断开连接。APP的时间戳与设备时间戳相差达15分钟以上时,登录失败。 *4.控制设备安全策略 若APP未成功登录设备即发送设备控制请求,设备将立刻断开和APP的连接。当且仅当APP成功登录设备后,才可以向设备发送控制指令并接收设备上报的数据。若长达60秒设备未接收到APP发送的任何数据(包含心跳包),设备将断开和APP的连接。 ===== 数据帧 ===== 一个完整的数据帧包含定长数据头(header)与变长数据体(body)。 | 定长数据头 | 变长数据体 | | 28 Bytes | sizeof(body) Bytes | 定长数据头长度恒定为28字节,包含7个 uint32_t 类型的值。 | magic | type | body_length | command | sequence | checksum | flag | | 4 Bytes | 4 Bytes | 4 Bytes | 4 Bytes | 4 Bytes | 4 Bytes | 4 Bytes | 定长数据头C语言结构体定义如下 //定长数据头 typedef struct DataHead_t { uint32_t magic; // Magic Code(魔法数),恒等于0xAA33CC55 uint32_t type; // 数据帧body类型, 默认为1(JSON) uint32_t body_length; // 数据体长度(不含数据头) uint32_t command; // 命令, 表示数据帧执行的操作 uint32_t sequence; // 请求包流水号, 递增, 回传 uint32_t checksum; // 包体校验, 预留 uint32_t flag; // 请求包的一些标志, 预留 } __attribute__((aligned(1), packed)) DataHead_t; **变长数据体** 变长数据体目前是JSON格式,其长度必须与定长数据头中定义的长度相等。 ===== 局域网端口定义 ===== 监听端口 | 类型 | 角色 | UDP 端口 | TCP 端口 | | 设备 | 服务端 | 12476 | 12518 | | APP | 客户端 | 12517 | 随机 | 注意: 设备与APP使用随机的发送端口(目的是降低应用间端口冲突的可能)与固定的监听端口。 ===== UDP服务 ===== ==== SoftAP方式Wi-Fi配网 ==== 在智能硬件加入某个Wi-Fi局域网之前,必须获取该Wi-Fi局域网的基本信息,例如SSID与密码。 SoftAP方式Wi-Fi配网是指由智能硬件释放出自身的Wi-Fi热点,手机APP连接此热点后,由APP通过UDP广播将要连接的Wi-Fi配置信息发送至智能硬件。智能硬件收到信息后,连接至要连接的Wi-Fi局域网并断开当前手机APP的Wi-Fi连接。 === 请求 === 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | UDP 广播 | APP到设备 | 2001 | JSON | 数据体示例 SoftAP方式Wi-Fi配网请求 { "ssid": "Wi-Fi_Name", "password": "Wi-Fi_Password" } === 应答 === 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | UDP 单播 | 设备到APP | 3001 | JSON | 数据体示例 SoftAP方式Wi-Fi配网应答 { "product_id": "pnTSD3ZsRNVgvNn6YRC2Z5", "device_id": "JiEbsXMdn2W5uZtMm6fmr6", "mac": "001122334455" } 注意:若设备未经量产测试完成云端注册,由于设备未获得设备ID,因此设备不会回应该请求。 ==== Smartlink配网, Airkiss配网或WPS配网成功 ==== 当设备通过智能连接、Airkiss或WPS配网成功加入无线局域网后,将向整个局域网发送UDP广播。局域网中所有的APP收到此广播后,可以获悉设备配置成功。 === 应答 === 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | UDP 广播 | 设备到APP | 3002 | JSON | 数据体示例 Smartlink配网, Airkiss配网或WPS配网成功应答 { "product_id": "pnTSD3ZsRNVgvNn6YRC2Z5", "device_id": "JiEbsXMdn2W5uZtMm6fmr6", "mac": "001122334455" } 注意:若设备未经量产测试完成云端注册,由于设备未获得设备ID,因此设备不会发送配置成功数据包。 ==== 设备成功加入Wi-Fi局域网 ==== 当设备由于突然断电或Wi-Fi受到干扰等原因造成网络中断,设备重新加入Wi-Fi局域网中时,将会发送设备入网成功广播。局域网中的APP收到此广播时,将此设备加入未绑定设备列表或局域网在线设备列表。 === 应答 === 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | UDP 广播 | 设备到APP | 3003 | JSON | 数据体示例 设备加入Wi-Fi局域网成功应答 { "product_id": "pnTSD3ZsRNVgvNn6YRC2Z5", "device_id": "JiEbsXMdn2W5uZtMm6fmr6", "mac": "001122334455" } 注意:若设备未经量产测试完成云端注册,则该设备不可被发现。 绑定设备 用户必须与设备建立绑定关系后才可以控制设备。绑定设备时,由APP向设备所在的IP地址使用UDP协议发送绑定请求包,若设备处于可绑定状态,设备会向APP所在IP地址发送绑定应答包。若设备处于不可绑定状态,设备会忽略APP的绑定请求。绑定应答包中包含设备ID、设备密码以及其他与绑定有关的设备信息,APP收到绑定信息后,向云端发起绑定请求,云端完成绑定操作。绑定成功后,APP应当刷新来自云端的设备列表,用户点击列表中的在线设备条目完成Broker部署并跳转到APP控制设备界面。 设备是否可绑定一般由设备上的物理开关、按键、定时器或逻辑开关决定,且经过用户授权后(例如按下某按键),设备方可与用户绑定。对于球泡灯这样没有物理按键的设备,默认可被绑定,除非用户在APP中禁用该设备的绑定功能(控制设备内的某个逻辑开关,比如绑定开关数据点)。 请求 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | UDP单播 | APP到设备 | 2005 | JSON | 数据体示例 由APP发送的绑定设备请求 { "ts" : 1465541792 } 应答 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | UDP单播 | 设备到APP | 3005 | JSON | 数据体示例 由设备发送的绑定设备应答 { "device_id":"JiEbsXMdn2W5uZtMm6fmr6", "password":"0a1704dee5ed7200fcea5f627f6d1fd1", "access_key":"8e0bb23839955bde1346b6e9395347ff", "ts":1465541793 } 注意:若设备未经量产测试完成云端注册,则该设备不可被绑定。 ===== TCP服务 ===== === 登录设备 === 在进行任何操作之前(包含发送控制指令或者接收数据上报)必须先登录设备,否则设备将立即断开TCP连接。 [[#page2|登录设备前需根据时间戳计算签名(]][[#page2| signature ),计算完毕后将时间戳与签名发送至设备进行验证,签名算法见硬件端TCP]][[#page2|登录签名策略,以下是例子。]] === 请求 === 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | TCP | APP到设备 | 2101 | JSON | 数据体示例 由APP发送的登录设备请求 { "signature":"0a1704dee5ed7200fcea5f627f6d1fd1", "ts":1465541793 } === 应答 === 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | TCP | 设备到APP | 3101 | JSON | 数据体示例 由设备发送的登录成功应答 { "success": true } 由设备发送的登录失败应答 { "success": false, "error_code": 1001, "message": "SIGNATURE INCORRECT" } 注意,登录失败或者连接后1500ms内未完成登录,设备将断开TCP连接。 === TCP心跳 === 当APP与设备之间长时间没有发生通信,设备或APP需要发送心跳包给对方。APP的心跳包超时时间一般设置为120s,发送心跳包的间隔为50秒。当APP在心跳包超时后仍然没有接收到来自设备的心跳包,它们之间的连接将会主动断开。当设备在心跳包超时后仍然没有收到APP的心跳包,设备也会主动断开连接。请注意心跳包命令只能在成功登录设备后发送。 ===请求=== 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | TCP | APP到设备 | 2102 | JSON | 数据体示例 由APP发送的心跳包 { "ts":1465541793 } ===应答=== 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | TCP | 设备到APP | 3102 | JSON | 数据体示例 由设备发送的心跳包 { "ts":1465541793 } ===传输业务数据=== APP与设备通过云端的中转或者局域网的直接连接可以相互传输业务数据。业务数据的具体格式取决于实际产品的定义,例如厂商定义的数据点结构或者透传自定义数据。其中按业务可划分为控制设备,读取设备数据和设备上报自身数据等。 TCP业务数据格式具体参见[[#page2|请求类型]],其数据格式与请求方法与广域网通信相同,以下进行举例说明。 ===请求=== 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | TCP | APP到设备 | 2103 | JSON | 数据体示例 由APP发送的业务数据 { "i":2, "d":["r","g","b","cw","iw"], "t":1464714257 } ===应答=== 数据头参数 | 方式 | 方向 | 指令 | 数据类型 | | TCP | 设备到APP | 3103 | JSON | 数据体示例 由设备发送的业务数据 { "i":4, "d":{ "r":255, "g":255, "b":255, "cw":255, "iw":255 }, "t":1464714257 } ====== AiCloud SDK接口函数 ====== ===== AiCloud SDK连接API ===== **aicloud_run** | 函数名 | int aicloud_run(void) | | 输入 | 无 | | 返回 | 返回一个int,0代表成功,非0代表失败 | | 功能 | 创建aicloud的连接任务,包括内网、广域网、以及相应的网络管理 | **aicloud_on_connected** | 函数名 | void aicloud_on_connected(aicloud_callback_t callback) | | 输入 | aicloud_callback_t | | 返回 | 无 | | 功能 | 注册一个连接上aicloud的用户处理函数 | **aicloud_on_disconnected** | 函数名 | void aicloud_on_disconnected(aicloud_callback_t callback) | | 输入 | aicloud_callback_t | | 返回 | 无 | | 功能 | 注册一个与aicloud断开连接的用户处理函数 | **aicloud_on_receive** | 函数名 | void aicloud_on_receive(aicloud_receive_callback_t callback) | | 输入 | aicloud_receive_callback_t | | 返回 | 无 | | 功能 | 注册一个负责接收处理的用户函数 | **aicloud_on_receive_raw** | 函数名 | void aicloud_on_receive_raw(aicloud_receive_raw_callback_t callback) | | 输入 | aicloud_receive_raw_callback_t | | 返回 | 无 | | 功能 | 注册一个负责接收原始数据处理的用户函数 | **aicloud_on_sent** | 函数名 | void aicloud_on_sent(aicloud_callback_t callback) | | 输入 | aicloud_callback_t | | 返回 | 无 | | 功能 | 注册一个发送数据处理的用户函数 | **aicloud_send** | 函数名 | bool aicloud_send(const cJSON* object) | | 输入 | const cJSON* | | 返回 | 返回bool ,用于表示是否发送成功 | | 功能 | 用于发送一个cJSON格式的对象 | **aicloud_on_state_changed** | 函数名 | void aicloud_on_state_changed(aicloud_state_callback_t callback) | | 输入 | aicloud_state_callback_t | | 返回 | 无 | | 功能 | 用于注册一个网络状态监听的处理函数 | **aicloud_state** | 函数名 | aicloud_state_t aicloud_state(void) | | 输入 | void | | 返回 | aicloud_state_t | | 功能 | 用于获取与aicloud的连接状态 | ===== aicloud ESP8266 SDK 通用API ===== **aicloud_system_recovery** | 函数名 | void aicloud_system_recovery(void) | | 输入 | 无 | | 返回 | 无 | | 功能 | 恢复出厂 | **aicloud_force_smartlink** | 函数名 | void aicloud_force_smartlink(void) | | 输入 | 无 | | 返回 | 无 | | 功能 | 强制进入连接smartlink模式 | **aicloud_force_aplink** | 函数名 | void aicloud_force_aplink(void) | | 输入 | 无 | | 返回 | 无 | | 功能 | 强制进入ap模式 | **aicloud_set_ssid_prefix** | 函数名 | void aicloud_set_ssid_prefix(const char* sPrefix) | | 输入 | const char* sPrefix | | 返回 | 无 | | 功能 | 设置ap的前缀 | **aicloud_check_update** | 函数名 | void aicloud_check_update(void) | | 输入 | 无 | | 返回 | 无 | | 功能 | 检查固件更新 | **aicloud_time_ready** | 函数名 | bool aicloud_time_ready(void) | | 输入 | 无 | | 返回 | 返回bool, 时间获取是否成功 | | 功能 | 用于查询是否成功获取时间 | ===== aicloud SDK JSON API ===== **aicloud_is_get_object** | 函数名 | bool aicloud_is_get_object(const cJSON* object) | | 输入 | const cJSON* object 一个cJSON对象 | | 返回 | 返回bool, 是否是array对象 | | 功能 | 判断输入的cJSON对象是否是一个array对象 | **aicloud_is_set_object** | 函数名 | bool aicloud_is_set_object(const cJSON* object) | | 输入 | const cJSON* object 一个cJSON对象 | | 返回 | 返回bool, 是否是object对象 | | 功能 | 判断输入的cJSON对象是否是一个object对象 | **aicloud_receive_get** | 函数名 | bool aicloud_receive_get(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回bool, object(array)对象是否有key键值 | | 功能 | 判断输入的cJSON(array)对象是否有key键 | **aicloud_receive_set** | 函数名 | bool aicloud_receive_set(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回bool, object(object)对象是否有key键值 | | 功能 | 判断输入的cJSON(object)对象是否有key键 | **aicloud_receive_bool** | 函数名 | bool aicloud_receive_bool(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回bool, object(object)对象key键值是否是bool | | 功能 | 判断输入的cJSON(object)对象是否有key键值是否是bool | **aicloud_receive_double** | 函数名 | bool aicloud_receive_double(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回bool, object(object)对象key键值是否是double | | 功能 | 判断输入的cJSON(object)对象是否有key键值是否是double | **aicloud_receive_int** | 函数名 | bool aicloud_receive_int(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回bool, object(object)对象key键值是否是int | | 功能 | 判断输入的cJSON(object)对象是否有key键值是否是int | **aicloud_receive_string** | 函数名 | bool aicloud_receive_string(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回bool, object(object)对象key键值是否是string | | 功能 | 判断输入的cJSON(object)对象是否有key键值是否是string | **aicloud_value_bool** | 函数名 | bool aicloud_value_bool(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回bool, object(object)对象key键值bool的值 | | 功能 | 判断输入的cJSON(object)对象是否有key键值bool的值 | **aicloud_value_double** | 函数名 | double aicloud_value_double(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回double, object(object)对象key键值double的值 | | 功能 | 判断输入的cJSON(object)对象是否有key键值double的值 | **aicloud_value_int** | 函数名 | int aicloud_value_int(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回int, object(object)对象key键值int的值 | | 功能 | 判断输入的cJSON(object)对象是否有key键值int的值 | **aicloud_value_string** | 函数名 | char *aicloud_value_string(const cJSON *object, const char *key) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个键值key | | 返回 | 返回char*, object(object)对象key键值string的值 | | 功能 | 判断输入的cJSON(object)对象是否有key键值string的值 | **aicloud_add_bool** | 函数名 | bool aicloud_add_bool(cJSON *object, const char *key, bool value) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个比较的key bool value 一个要设置的bool值 | | 返回 | 返回bool, 表示是否设置成功 | | 功能 | 设置cJSON对象的key键的bool值 | **aicloud_add_double** | 函数名 | bool aicloud=_add_double(cJSON *object, const char *key, double value) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个比较的key double value 一个要设置的double值 | | 返回 | 返回bool, 表示是否设置成功 | | 功能 | 设置cJSON对象的key键的double值 | **aicloud_add_int** | 函数名 | bool aicloud_add_int(cJSON *object, const char *key, int value) | | 输入 | const cJSON* object 一个cJSON对象 const char * key 一个比较的key int value 一个要设置的int值 | | 返回 | 返回bool, 表示是否设置成功 | | 功能 | 设置cJSON对象的key键的int值 | **aicloud_add_string** | 函数名 | bool aicloud_add_string(cJSON *object, const char *key, const char* value) | | 输入 | const cJSON* object 一个cJSON对象 \\const char * key 一个比较的key \\const char value 一个要设置的 \\const char 值 | | 返回 | 返回bool, 表示是否设置成功 | | 功能 | 设置cJSON对象的key键的const char*值 | **aicloud_object_create** | 函数名 | cJSON *aicloud_object_create(void) | | 输入 | 无 | | 返回 | 返回cJSON //object//对象 //cJSON// object 一个被创建的cJSON对象 | | 功能 | 创建一个cJSON对象 | **aicloud_object_delete** | 函数名 | void aicloud_object_delete(cJSON* object) | | 输入 | const cJSON* object 一个要删除的cJSON对象 | | 返回 | 无 | | 功能 | 删除一个cJSON对象 | ===== 云端对硬件HTTP应用接口 ===== ==== 设备注册 ==== 请求示例 curl -X POST -H "Content-Type: application/json" "http://api.vcd.io:4567/v1/device/register accessKey=8e0bb23839955bde1346b6e9 395347ff&signature=e38a3d553bd8f607181c94a64f51eb85&ts=1464714257" -d %%'{%%"productId":"sa4nUnfQisGTZH3EmE8JEE", "mac":"wwwddsads aww2"}' 返回示例 200 { "success": true, "data": { "id": "sa4nUnfQisGTZH3EmE8JEE", "password": "f7ab8e790ee446b102cb70b64b68f7e9" } } 返回示例 500 { "success": false, "errorCode": 500, "message": "内部错误" } 请求参数 | 参数 | 类型 | 必须 | 默认 | 说明 | | productId | string | 是 | 无 | 产品id | | mac | string | 是 | 无 | 设备mac地址 | 返回参数 | 参数 | 类型 | 说明 | | id | string | 设备id | | password | string | 设备密码,密码类型为[[#page2|一阶密码]] | ====修改设备名称==== 请求示例 curl -X POST -H "Content-Type: application/json" "http://api.vcd.io:4567/v1/device/deploy accessKey=8e0bb23839955bde1346b6e939 5347ff&ts=1465541792&signature=ad36b179e3d1fb9f8fb368c4b9e99010" -d '{"deviceId":"sa4nUnfQisGTZH3EmE8JEE", "deviceName":"卧室灯", "deviceLogo":"http://sss/xxs.jpg"}' 返回示例 200 { "success": true } 返回示例 500 { "success": false, "errorCode": 500, "message": "内部错误" } 请求参数 | 参数 | 类型 | 必须 | 默认 | 说明 | | deviceId | string | 是 | 无 | 设备id | | deviceName | string | 是 | 无 | 卧室灯 | | deviceLogo | string | 否 | 无 | 设备图片logo URL | 返回参数 | 参数 | 类型 | 说明 | | 无 | 无 | 无 | ==== 设备部署 ==== 请求示例 curl -X POST -H "Content-Type: application/json" "http://api.vcd.io:4567/v1/device/deploy accessKey=8e0bb23839955bde1346b6e939 5347ff&ts=1465541792&signature=ad36b179e3d1fb9f8fb368c4b9e99010" -d %%'{%%"deviceId":"sa4nUnfQisGTZH3EmE8JEE", "password":"0a1704d ee5ed7200fcea5f627f6d1fd1", "protocol":"mqtts"}' 返回示例 200 { "success": true, "data": { "server": "mqtts://1:8883", "username": "JiEbsXMdn2W5uZtMm6fmr6", "password": "EpGzRRYGu6V3xJxF7VMDZ5", "publicKey": "", "publicKeyVersion": "", "signature": "30210fafc0d640fd4773ca093a478f8c" } } 返回示例 500 { "success": false, "errorCode": 500, "message": "内部错误" } 请求参数 | 参数 | 类型 | 必须 | 默认 | 说明 | | deviceId | string | 是 | 无 | 设备id | | password | string | 是 | 无 | 设备密码,密码类型为[[#page2|二阶密码]] | | protocol | string | 否 | mqtts | 设备接入协议,可选mqtts、mqtt、wss、ws | 返回参数 | 参数 | 类型 | 说明 | | server | string | 设备接入服务器 | | username | string | 设备接入用户名 | | password | string | 设备接入密码,密码类型为[[#page2|零阶密码]],该密码为服务器生成的随机短uuid | | publicKey | string | pem key经过base64的字符串 | | publicKeyVersion | string | 当前证书版本 | | signature | string | 使用同样的[[#page2|签名策略]]对返回值进行计算得到的签名,供客户端验证(防止 DNS 劫持风险,可以不做) | ==== 设备OTA==== 请求示例 curl -X POST -H "Content-Type: application/json" "http://api.vcd.io:4567/v1/device/ota accessKey=8e0bb23839955bde1346b6e939534 7ff&signature=e38a3d553bd8f607181c94a64f51eb85&ts=1464714257" -d '{"deviceId":"sa4nUnfQisGTZH3EmE8JEE", "password":"684b3afe4c ff2f5d9c5e33d019c04048","firmwareId":"sa4nUnfQisGTZH3EmE8JEE","version":""}' 返回示例 200 { "success": true, "data": { "upgrade": true, "version": "", "url": "http://dms.aicloud.com:38080/dms/firmware/download.htm firmwareFileId=sa4nUnfQisGTZH3EmE8JEE", "md5": "" } } 返回示例 500 { "success": false, "errorCode": 500, "message": "内部错误" } 请求参数 | 参数 | 类型 | 必须 | 默认 | 说明 | | deviceId | string | 是 | 无 | 设备id | | password | string | 是 | 无 | 设备密码,密码类型为[[#page2|二阶密码]] | | firmwareId | string | 是 | 无 | 固件id | | version | string | 是 | 无 | 当前固件版本号 | 返回参数 | 参数 | 类型 | 说明 | | upgrade | bool | 是否需要升级 | | version | string | 新固件的版本号 | | url | string | 新固件的下载地址 | | md5 | string | 新固件的md5值,用户文件校验 | 1.固件的下载地址有`有效期`,默认15分钟。15分钟内没有完成升级的,需重新请求此接口或许新的下载地址。 2.固件版本号的规则是 x.x.x , 其中x表示数字 ==== 设备解除绑定 ==== 设备解除绑定,解除设备所绑定的所有用户。 请求示例 curl -X POST -H "Content-Type: application/json" "http://api.vcd.io:4567/v1/device/unbind accessKey=8e0bb23839955bde1346b6e939 5347ff&signature=e38a3d553bd8f607181c94a64f51eb85&ts=1464714257" -d '{"deviceId":"14666251168079293", "password":"684b3afe4cff 2f5d9c5e33d019c04048"}' 返回示例 200 { "success": true, "data": { "result": "SUCCESS" } } 返回示例 500 { "success": false, "errorCode": 500, "message": "内部错误" } 请求参数 | 参数 | 类型 | 必须 | 默认 | 说明 | | deviceId | string | 是 | 无 | 设备id | | password | string | 是 | 无 | 设备密码,密码类型为[[#page2|二阶密码]] | 返回参数 | 参数 | 类型 | 说明 | | result | string | 结果 |