OneNET雲平臺-EDP協議資料傳輸
OneNet真是中移動的良心之作,對比阿里雲和慶科雲,OneNet不但免費而且功能也足夠嵌入式應用,對學生黨而言真是大大的福利,感謝中移動!!!。
一、雲端建立裝置與應用
(1)建立產品:進入開發者中心有個藍色框點選就可以建立產品,自己使用的時候,產品資訊什麼的,自己填完整的就好,這裡需要說明的是裝置接入方式和裝置接入協議,裝置接入方式一般選擇公開協議,除錯之用,沒必要自己定義協議,裝置接入協議選擇有個下拉框可供選擇:
在右邊框框裡有簡單介紹協議的基本功能,在這裡我選擇是EDP協議,可以傳送資料和接收命令,能完成基本功能就好。關於這些協議具體介紹在雲平臺公開協議產品指南里有詳細介紹:
(2)註冊裝置:產品註冊成功後會提示是否生成裝置,這時候就可以填寫自己想要接入裝置資訊,生成之後是醬紫的:
在產品概況介面是如下顯示:
這裡有個產品ID和APIKey,這個產品ID和裝置ID是不同的,但用這個APIKey可以預設訪問產品下的所有裝置,其實還可以為每個裝置申請APIKey的,在裝置管理裡,,點選“新增APIKey”就可以為當前裝置申請APIKey,下面是我申請裝置的完整資訊,有了裝置ID和APIKey,這是訪問該裝置的最直接辦法。
(3)生成應用:為該裝置建立資料流和應用:在資料流模板介面有新增資料流按鈕,新增一個名為temp的資料流,再在應用管理介面新增一個名為test的應用,使用一個折線圖控制元件來顯示當前temp的值。
注意在右邊要為當前折線圖選擇資料流,才能將應用與裝置的資料流繫結。
二、利用EdpProtoDebugger除錯工具進行資料傳輸
將之前的裝置ID和APIKey填進去,預設訊息型別是連線雲操作,訊息子型別是裝置ID和APIKey,表示採用裝置ID和APIKey方式來訪問雲端。
先點選“生成編碼”再點擊發送到裝置雲,會有如下16進位制資料顯示,這些資料是有意義的,在剛才給的下載連結裡有相關EDP協議具體介紹
在這裡,用除錯工具自帶的翻譯器檢視一下接收訊息的意思
連線成功,在開發者中心的裝置管理裡,裝置前的類似燈的按鈕會亮,表示裝置線上。如果五分鐘之內沒有資料傳送與接收,雲端伺服器會自動斷開連線。
接下來是傳輸資料:訊息型別選擇SaveData型別,訊息子型別就選擇資料型別一:JSON格式串,同樣的這些協議具體內容都在文件裡寫清楚,慢慢看就能懂。給個JSON格式串參考:
{
"datastreams": [{
"id": "temp",
"datapoints": [{
"value": "25"
}]
}]
}
將上面的JSON串複製到除錯工具的資料文字框中,然後點選生成編碼,再點擊發送到裝置,會在雲端應用的折線圖看到資料,資料傳送接收成功。
再看下雲端向裝置傳送命令,先將除錯工具連上雲端就什麼都不幹,靜靜地等待雲端命令就好,然後在雲端應用中新增一個旋鈕控制元件來發送命令。
將旋鈕與裝置test繫結,但沒有選擇資料流,因為我們是希望雲端向裝置傳送命令,這個就不是裝置的資料流了。需要注意的這個EDP命令內容,我在這卡了很久表示不理解這到底是個啥(當然,如果你自己已經看懂了這是什麼,這裡就跳過),這裡先留個疑問,等會在程式碼除錯時再回過頭來看會更清楚。
旋轉按鈕時,會向已連線的除錯工具傳送訊息,在除錯工具裡有接收到訊息,會有相應的16進位制資料,用翻譯器大致看下就好:
第0位元組表示命令請求資訊,說明確確實實接收到雲端的命令請求,達到我們的期望。
到這裡,基本上可以完成裝置與OneNet雲的連線、傳送與接收資料。最後是重點,程式碼如何實現?
三、程式碼實現上述功能
也許,有心的同學能發現,其實,OneNet文件中心已經給出了程式碼例項,在git上已經開源SDK,這裡給出連結:https://github.com/cm-heclouds/edp_c
主要說一下程式碼框架,怎麼實現傳送與接收資料的,涉及到簡單socket套接字網路程式設計。
從git上下載到SDK,主要是幾個c檔案和對應的h檔案,包括,cJSON.c、EdpKit.c、Openssl.c和一個簡單的應用例程,這個應用例程是有傳送和接收資料框架的。
首先說一下這幾個檔案的大致作用,cJSON.c其實也是一個開源的軟體,是ANSI-C標準的JSON解析器,只包含一個.c和.h檔案,移植性和效率都是比較好的;Openssl.c同樣是一個開源的庫,是一個安全套接字層密碼庫,說簡單點就是用於資料加密;所以真正屬於OneNET的是EdpKit.c這個檔案,這個也是寫應用程式參考的最多的一個檔案,主要是採用雲平臺公開協議對資料進行打包和解析的一些函式;SDK會提供一個應用例程Main.c,這個例程主要是採用命令列互動式操作,主要是為了測試EdpKit而寫的,也向使用者展示瞭如何使用EdpKit。總體看來,OneNET應用還是比較簡單的。
我首先把這個應用例程做了一些修改,主檔案重新命名為main.c,原始碼在上面CSDN的資源頁可以下載,首先看下Makefile,預設是不需要資料加密功能的,在Makefile中將編譯Openssl的命令遮蔽掉,在main.c中涉及到Openssl的語句都是採用預編譯命令包括的。
連線OneNET雲平臺:主要是利用socket套接字,與基本網路程式設計思路是一樣的:產生套接字->連線伺服器->資料傳輸三個步驟:
int main(int argc, char *argv[])
{
int sockfd, ret=0;
EdpPacket* send_pkg;
pthread_t id_1;
uint8 sys_time=0, temp=0;
char strBuf[25];
/*1、 建立一個與伺服器的socket連線*/
sockfd = Open(SERVER_ADDR, SERVER_PORT);
if(sockfd < 0)
{
printf("Connect Server Faild ! \n");
return -1;
}
/* create a recv thread */
ret=pthread_create(&id_1,NULL,(void *(*) (void *))recv_thread_func, &sockfd); //建立接收執行緒
/*2、產生併發送EDP裝置連線請求的資料包 */
send_pkg = PacketConnect1(DEV_ID, API_KEY);
ret = DoSend(sockfd, (char*)send_pkg->_data, send_pkg->_write_pos);
if((!send_pkg) || (ret <= 0)) //連線失敗,直接退出
{
goto faild;
}
DeleteBuffer(&send_pkg);
while(1)
{
sleep(5);
/*3、產生併發送EDP使用者資料包 */
bzero(strBuf, sizeof(strBuf));
sprintf(strBuf, "%d", sys_time++ % 10);
ret = write_func(sockfd, "sys_time", strBuf);
if (ret < 0)
{
break;
}
}
/*關閉socket連線*/
Close(sockfd);
/* 關閉接收執行緒 */
pthread_join(id_1,NULL);
return 0;
faild:
printf("DoSend faild ! (%d)\n", __LINE__);
DeleteBuffer(&send_pkg);
Close(sockfd);
pthread_join(id_1,NULL);
return -1;
}
資料傳送過程:這部分是OneNET文件中心給出的例程做的簡單修改,
int write_func(int32 socket_fd, char* value_ID, char* val)
{
int32 sockfd = socket_fd;
EdpPacket* send_pkg;
cJSON *save_json;
int32 ret = 0;
char send_buf[100];
/*產生JSON串,其中攜帶了要上傳的使用者資料*/
bzero(send_buf, sizeof(send_buf));
strcat(send_buf,"{\"datastreams\": [{");
// strcat(send_buf,"\"id\": \"sys_time\",");
strcat(send_buf,"\"id\": \"");
strcat(send_buf, value_ID);
strcat(send_buf, "\",");
strcat(send_buf,"\"datapoints\": [");
strcat(send_buf,"{\"value\": \"");
strcat(send_buf, val);
strcat(send_buf,"\"}]}]}");
/*將JSON串封裝成EDP資料包*/
save_json=cJSON_Parse(send_buf);
if(NULL == save_json)
{
printf("[%d]Error before: [%s]\n", __LINE__, cJSON_GetErrorPtr());
return -1;
}else{
/* 解析JSON格式,用作除錯 */
// printf("[%d]cJSON_Print : \n%s\n", __LINE__, cJSON_Print(save_json)); //帶格式輸出
printf("cJSON_Print : %s (%d)\n", cJSON_PrintUnformatted(save_json), __LINE__); //不帶格式輸出
}
send_pkg = PacketSavedataJson(DEV_ID, save_json, kTypeFullJson, 0);
if(NULL == send_pkg)
{
return -1;
}
cJSON_Delete(save_json); //刪除構造的json物件
/*傳送EDP資料包上傳資料*/
ret = DoSend(sockfd, (char*)send_pkg->_data, send_pkg->_write_pos);
if(ret < 0)
{
printf("DoSend faild ! (%d)\n", __LINE__);
// send_pkg = PacketPing();
}
DeleteBuffer(&send_pkg);
return ret;
}
資料接收過程:這一部分是原來SDK的原始碼,基本上沒有修改,只是添加了幾個除錯語句
/*
* 函式名: recv_thread_func
* 功能: 接收執行緒函式
* 引數: arg socket描述符
* 說明: 這裡只是給出了一個從socket接收資料的例子, 其他方式請查詢相關socket api
* 一般來說, 接收都需要迴圈接收, 是因為需要接收的位元組數 > socket的讀快取區時, 一次recv是接收不完的.
* 相關socket api:
* recv
* 返回值: 無
*/
void recv_thread_func(void* arg)
{
int sockfd = *(int*)arg;
int error = 0;
int n, rtn;
uint8 mtype, jsonorbin;
char buffer[4096];
RecvBuffer* recv_buf = NewBuffer();
EdpPacket* pkg;
char* src_devid;
char* push_data;
uint32 push_datalen;
cJSON* save_json;
char* save_json_str;
cJSON* desc_json;
char* desc_json_str;
char* save_bin;
uint32 save_binlen;
unsigned short msg_id;
unsigned char save_date_ret;
char* cmdid;
uint16 cmdid_len;
char* cmd_req;
uint32 cmd_req_len;
EdpPacket* send_pkg;
char* ds_id;
double dValue = 0;
int iValue = 0;
char* cValue = NULL;
char* simple_str = NULL;
char cmd_resp[] = "ok";
unsigned cmd_resp_len = 0;
DataTime stTime = {0};
FloatDPS* float_data = NULL;
int count = 0;
int i = 0;
struct UpdateInfoList* up_info = NULL;
#ifdef _DEBUG
printf("[%s(%d)] recv thread start ...\n", __func__, __LINE__);
#endif
while (error == 0)
{
/* 試著接收1024個位元組的資料 */
n = Recv(sockfd, buffer, sizeof(buffer), MSG_NOSIGNAL);
if (n <= 0)
break;
printf("[%d]recv from server, bytes: %d\n", __LINE__, n);
/* wululu test print send bytes */
hexdump((const unsigned char *)buffer, n);
/* 成功接收了n個位元組的資料 */
WriteBytes(recv_buf, buffer, n);
while (1)
{
/* 獲取一個完成的EDP包 */
if ((pkg = GetEdpPacket(recv_buf)) == 0)
{
printf("[%d]need more bytes...\n", __LINE__);
break;
}
/* 獲取這個EDP包的訊息型別 */
mtype = EdpPacketType(pkg);
printf("EdpPacketType is %d \n", mtype);
#ifdef _ENCRYPT
if (mtype != ENCRYPTRESP){
if (g_is_encrypt){
SymmDecrypt(pkg);
}
}
#endif
/* 根據這個EDP包的訊息型別, 分別做EDP包解析 */
switch(mtype)
{
#ifdef _ENCRYPT
case ENCRYPTRESP: //加密請求響應 (client to server)
UnpackEncryptResp(pkg);
break;
#endif
case CONNRESP: //連線請求響應 (client to server)
/* 解析EDP包 - 連線響應 */
rtn = UnpackConnectResp(pkg);
printf("[%d]recv connect resp, rtn: %d\n", __LINE__, rtn);
break;
case PUSHDATA: //轉發(透傳)資料 (雙向)
/* 解析EDP包 - 資料轉發 */
UnpackPushdata(pkg, &src_devid, &push_data, &push_datalen);
printf("recv push data, src_devid: %s, push_data: %s, len: %d\n",
src_devid, push_data, push_datalen);
free(src_devid);
free(push_data);
break;
case UPDATERESP: //平臺下發當前最新的軟體資訊 (client to server)
UnpackUpdateResp(pkg, &up_info);
while (up_info){
printf("name = %s\n", up_info->name);
printf("version = %s\n", up_info->version);
printf("url = %s\nmd5 = ", up_info->url);
for (i=0; i<32; ++i){
printf("%c", (char)up_info->md5[i]);
}
printf("\n");
up_info = up_info->next;
}
FreeUpdateInfolist(up_info);
break;
case SAVEDATA: //儲存(轉發)資料 (雙向)
/* 解析EDP包 - 資料儲存 */
if (UnpackSavedata(pkg, &src_devid, &jsonorbin) == 0)
{
if (jsonorbin == kTypeFullJson
|| jsonorbin == kTypeSimpleJsonWithoutTime
|| jsonorbin == kTypeSimpleJsonWithTime)
{
printf("[%d]json type is %d\n", __LINE__, jsonorbin);
/* 解析EDP包 - json資料儲存 */
/* UnpackSavedataJson(pkg, &save_json); */
/* save_json_str=cJSON_Print(save_json); */
/* printf("recv save data json, src_devid: %s, json: %s\n", */
/* src_devid, save_json_str); */
/* free(save_json_str); */
/* cJSON_Delete(save_json); */
/* UnpackSavedataInt(jsonorbin, pkg, &ds_id, &iValue); */
/* printf("ds_id = %s\nvalue= %d\n", ds_id, iValue); */
UnpackSavedataDouble(jsonorbin, pkg, &ds_id, &dValue);
printf("[%d]ds_id = %s\nvalue = %f\n", __LINE__, ds_id, dValue);
/* UnpackSavedataString(jsonorbin, pkg, &ds_id, &cValue); */
/* printf("ds_id = %s\nvalue = %s\n", ds_id, cValue); */
/* free(cValue); */
free(ds_id);
}
else if (jsonorbin == kTypeBin)
{/* 解析EDP包 - bin資料儲存 */
UnpackSavedataBin(pkg, &desc_json, (uint8**)&save_bin, &save_binlen);
desc_json_str=cJSON_Print(desc_json);
printf("recv save data bin, src_devid: %s, desc json: %s, bin: %s, binlen: %d\n",
src_devid, desc_json_str, save_bin, save_binlen);
free(desc_json_str);
cJSON_Delete(desc_json);
free(save_bin);
}
else if (jsonorbin == kTypeString ){
UnpackSavedataSimpleString(pkg, &simple_str);
printf("%s\n", simple_str);
free(simple_str);
}else if (jsonorbin == kTypeStringWithTime){
UnpackSavedataSimpleStringWithTime(pkg, &simple_str, &stTime);
printf("time:%u-%02d-%02d %02d-%02d-%02d\nstr val:%s\n",
stTime.year, stTime.month, stTime.day, stTime.hour, stTime.minute, stTime.second, simple_str);
free(simple_str);
}else if (jsonorbin == kTypeFloatWithTime){
if(UnpackSavedataFloatWithTime(pkg, &float_data, &count, &stTime)){
printf("UnpackSavedataFloatWithTime failed!\n");
}
printf("read time:%u-%02d-%02d %02d-%02d-%02d\n",
stTime.year, stTime.month, stTime.day, stTime.hour, stTime.minute, stTime.second);
printf("read float data count:%d, ptr:[%p]\n", count, (FloatDPS*)float_data);
for(i = 0; i < count; ++i){
printf("ds_id=%u,value=%f\n", float_data[i].ds_id, float_data[i].f_data);
}
free(float_data);
float_data = NULL;
}
free(src_devid);
}else{
printf("error\n");
}
break;
case SAVEACK: //儲存確認(server to client)
UnpackSavedataAck(pkg, &msg_id, &save_date_ret);
printf("[%d]save ack, msg_id = %d, ret = %d\n", __LINE__, msg_id, save_date_ret);
break;
case CMDREQ: //雲端傳送命令響應(server to client)
if (UnpackCmdReq(pkg, &cmdid, &cmdid_len, &cmd_req, &cmd_req_len) == 0)
{
printf("[%d]cmdid: %s, req:%s, req_len: %d\n", __LINE__, cmdid, cmd_req, cmd_req_len);
/*
* 使用者按照自己的需求處理並返回,響應訊息體可以為空,此處假設返回2個字元"ok"。
* 處理完後需要釋放
*/
#ifdef _NEEDRESP
cmd_resp_len = strlen(cmd_resp);
send_pkg = PacketCmdResp(cmdid, cmdid_len, cmd_resp, cmd_resp_len);
#ifdef _ENCRYPT
if (g_is_encrypt){
SymmEncrypt(send_pkg);
}
#endif
DoSend(sockfd, (const char*)send_pkg->_data, send_pkg->_write_pos); //將接收的資料上傳
DeleteBuffer(&send_pkg);
#endif
free(cmdid);
free(cmd_req);
}
break;
case PINGRESP: //心跳響應 (client to server)
/* 解析EDP包 - 心跳響應 */
UnpackPingResp(pkg);
printf("recv ping resp\n");
break;
default:
/* 未知訊息型別 */
error = 1;
printf("recv failed...\n");
break;
}
DeleteBuffer(&pkg);
}
}
DeleteBuffer(&recv_buf);
#ifdef _DEBUG
printf("[%s(%d)] recv thread end ...\n", __func__, __LINE__);
#endif
}
主要的程式碼就是這些,直接看除錯結果。
上面的截圖顯示了連線伺服器和資料傳送過程的除錯列印資訊,首先是連線伺服器成功,在main函式中建立了一個接收執行緒,會收到伺服器傳送的響應資訊,與前面用除錯工具列印的資訊是一致的:四個位元組的16進位制數:20 02 00 00;然後是向伺服器傳送資料,在write_func函式中採用拼接字串的方式來產生需要傳送的資料,再利用cJSON_Parse函式將資料打包成JSON串,除錯列印對應輸出無格式JSON串,然後採用PacketSavedataJson函式將JSON串打包成公開協議EDP資料包訊息型別為Savedata。
在while程式碼塊中遮蔽傳送相關的語句,程式碼實現的功能就是連線伺服器,然後就是等待伺服器向裝置傳送命令:
注意上面列印資訊的[490]這一行,這就是接收到伺服器的命令資訊,與上面的除錯工具是類似的。記得前面還有一個問題,在應用中傳送命令時這個EDP的命令內容到底表示什麼含義?
根據這裡的列印資訊可以看到,命令請求是“{setting}22”,我是應用中的旋鈕旋轉到22的值,還記得在應用中旋鈕設定的命令內容嗎?我的設定是“{setting}{V}”,在介紹中說{V}表示萬用字元。通過這個列印資訊,那裡的命令內容就很顯然了,因此,我們在裝置端只要解析這個命令字串就可以實現自己想要操作的裝置功能。
在應用中還有一個控制元件專門用來輸入命令的:
同樣的,先繫結裝置test,下發命令“Hello”,在裝置端會接收到訊息如下:
列印的命令請求正是“Hello”,至此,裝置與OneNET連線和資料傳輸基本功能基本上是實現了,接下來就是真正的可以在專案中運用了。