มั่ว, ไม่ธรรมดา

อ่านค่า / ส่งค่า netpie ด้วย C, cURL, json-c

การส่งค่า การอ่าน(รับ)ค่า topic หรือตัวแปร ไปยัง netpie ด้วย C โดยใช้ library cURL (ผ่าน REST API) และ json-c (อ่านค่าที่รับจาก netpie ซึ่งเป็น json array)

โครงสร้างของ rest api ปกติที่เราเรียกด้วย curl ก็จะเป็นประมาณนี้

curl -X PUT https://api.netpie.io/topic/ThingsControl/seal/status?retain&auth=แทนค่าด้วย app key:แทนค่าด้วย app secret -d “ON”

https://api.netpie.io/topic/ThingsControl/seal/status?retain&auth=xxxxxxxx:yyyyyy

uri : https://api.netpie.io/topic/

/ThingsControl คือ Application ที่สร้างไว้ใน netpie

/seal/status คือ topic หรือตัวแปร

retain คือการให้คงค่าไว้

auth คือ parameter ที่กำหนดการพิสูจน์ตัวตน (ระบุด้วย app key, app secret)

-d “ON” คือ ค่าที่เราจะส่งไปยังตัวแปร

 

ตัวอย่างที่เขียนด้วย C โดยการใช้ cURL ศึกษาได้จาก https://curl.haxx.se/libcurl/c/example.html และ https://stackoverflow.com/questions/7569826/send-string-in-put-request-with-libcurl stackoverflow ช่วยได้เยอะถ้าหาเจอ ha ha ha 😀

การติดตั้ง cURL บน Raspberry pi

sudo apt-get install libcurl4-openssl-dev

ดูเพิ่มเติมได้ที่นี่ https://bitcontrol.ch/en/2016/02/04/iot-remote-power-switch-part-10/

ตัวอย่าง

/***************************************************************************
 * _ _ ____ _
 * Project ___| | | | _ \| |
 * / __| | | | |_) | |
 * | (__| |_| | _ <| |___
 * \___|\___/|_| \_\_____|
 *
 * Copyright (C) 1998 - 2015, Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.haxx.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ***************************************************************************/
/* <DESC>
 * simple HTTP PUT using the easy interface
 * </DESC>
 */

/*
 Things Control. Control anything you want.
 Copyright (C) 2017 Pornthep Nivatyakul

This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.

You should have received a copy of the GNU General Public License
 along with this program. If not, see <http://www.gnu.org/licenses/>.

ThingsControl Copyright (C) 2017 Pornthep Nivatyakul, kaebmoo@gmail.com, seal@ogonan.com
 This program comes with ABSOLUTELY NO WARRANTY;
 This is free software, and you are welcome to redistribute it
 under certain conditions.




*/

#include <stdio.h>
#include <string.h>
#include <curl/curl.h>
#include <stdlib.h>

#include <fcntl.h>
#ifdef WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>




#if LIBCURL_VERSION_NUM < 0x070c03
#error "upgrade your libcurl to no less than 7.12.3"
#endif




int main(void)
{
 CURL *curl;
 CURLcode res;
 typedef struct {
 char *key;
 char *secret;
 char *param;
 char *uri;
 } app_key;

app_key AppKey;

char *key = "app key";
 char *secret = "app secret";
 char *param = "auth=";
 char *netpie = "https://api.netpie.io/topic";
 char *app_id = "/ThingsControl";
 char *id = "/seal";
 char *topic = "/status?retain&";
 /* https://api.netpie.io/topic/ThingsControl/seal/status?retain&auth=xxxxx:yyyy*/

int alloc = 0;

AppKey.key = malloc(strlen(key)+1);
 AppKey.secret = malloc(strlen(secret)+1);
 alloc += strlen(param) + strlen(key) + strlen(":") + strlen(secret);
 alloc++;
 AppKey.param = malloc(alloc);
 AppKey.uri = malloc(strlen(netpie)+strlen(app_id)+strlen(id)+strlen(topic)+strlen(param)+strlen(key)+strlen(":")+strlen(secret)+1);

strcpy(AppKey.key, key);
 strcpy(AppKey.secret, secret);
 strcpy(AppKey.param, param);

/* In windows, this will init the winsock stuff */
 curl_global_init(CURL_GLOBAL_ALL);

/* get a curl handle */
 curl = curl_easy_init();
 if(curl) {
 /* First set the URL that is about to receive our PUT. This URL can
 just as well be a https:// URL if that is what should receive the
 data. */


 /* printf("Parameter %s\n", AppKey.param); */
 AppKey.param = strcat(strcat(strcat(AppKey.param, AppKey.key),":"), AppKey.secret);
 AppKey.uri = strcat(strcat(strcat(strcat(strcat(AppKey.uri,netpie), app_id), id), topic), AppKey.param);
 printf("Parameter %s, \nuri %s\n", AppKey.param, AppKey.uri);

curl_easy_setopt(curl, CURLOPT_URL, AppKey.uri);

/* Now specify the PUT data */

 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "ON");

/* Perform the request, res will get the return code */
 res = curl_easy_perform(curl);
 printf("return code %d\n", res);
 /* Check for errors */
 if(res != CURLE_OK)
 fprintf(stderr, "curl_easy_perform() failed: %s\n",
 curl_easy_strerror(res));

/* always cleanup */
 curl_easy_cleanup(curl);
 }
 curl_global_cleanup();
 free(AppKey.param);
 free(AppKey.key);
 free(AppKey.secret);
 free(AppKey.uri);

return 0;
}

 

ตัวหลักหลักก็อยู่ที่

CURL *curl;

curl = curl_easy_init();

curl_easy_setopt(curl, CURLOPT_URL, AppKey.uri); กำหนด URL

curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, “PUT”); บอกให้ PUT

curl_easy_setopt(curl, CURLOPT_POSTFIELDS, “ON”); ค่าที่จะส่งขึ้นไป

res = curl_easy_perform(curl);

ที่นี้มาถึงตอนอ่านค่า

เวลาที่เรียก GET rest api netpie จะส่งค่าคืนมาเป็น json array หน้าตาประมาณนี้

เรียกผ่าน browser

https://api.netpie.io/topic/ThingsControl/seal/status?auth=xxxx:yyyy

[{“topic”:”/ThingsControl/seal/status”,”payload”:”ON”,”lastUpdated”:1497340128,”retain”:true}]

โดย key “topic” ก็เป็นชื่อ topic ตามที่เราสนใจเรียกอ่าน

“payload” ก็เป็นค่าของ topic หรือค่าของตัวแปร

“lastUpdated” เป็นค่าเวลา (epoch time) ที่ตัวแปรถูกกำหนดค่า เช่น 1497340128 อยากรู้ว่าเป็นเวลาเท่าไหร่ ลองเอาไปแปลงค่าดู https://www.epochconverter.com/ ก็จะได้เท่ากับ

GMT: Tuesday, June 13, 2017 7:48:48 AM
Your time zone: Tuesday, June 13, 2017 2:48:48 PM GMT+07:00

“retain” เป็น Boolean บอกว่าตัวแปรมีการคงค่าไว้หรือไม่

ตัวอย่างการเขียนอ่านค่าด้วย C จะมีสองส่วนคือ ส่วนของ rest api ที่เรียกผ่าน cURL ซึ่งก็เรียกผ่าน url ตามปกติ https://api.netpie.io/topic/ThingsControl/seal/status?auth=xxxx:yyyy แต่จะมีการเรียก callback function เพื่อให้มีการเก็บค่าที่ได้มาจาก server มาเก็บไว้ในตัวแปร chuck.memory ด้วย ซึ่งก็จะได้เป็น string แบบ json array มา

จากนั้นก็เอามา Parse ด้วย library json-c (http://json.org/ ทำไม ใช้ตัวนี้? ค้นหาด้วย google แล้วมันโผล่ขึ้นมา ก็เลยลองลองเล่นดู ถ้าดูใน json.org จะเห็นว่ามี library เยอะมาก ชอบตัวไหน ใช้ตัวไหนก็ตามลำบากครับท่าน ผม งมงม ตัวนี้แล้วใช้งานลองผิดไปหลายตลบแล้วมันได้ก็เลยใช้ใช้ไป)

ตัวอย่างที่ใช้งม

การติดตั้ง json-c https://github.com/json-c/json-c

ตัวช่วยถ้า compile แล้ว error https://stackoverflow.com/questions/480764/linux-error-while-loading-shared-libraries-cannot-open-shared-object-file-no-s

การแปลงเวลาจาก epoch time มาให้อ่านกันรู้เรื่อง ดูตัวอย่างจากนี่ https://www.epochconverter.com/programming/c

การใช้ json-c ก่อนจะนำค่าที่ได้จากการ parse json เขาก็แนะนำว่าควรจะทดสอบ type ก่อน เพื่อป้องกันข้อผิดพลาดของการกำหนดค่าตัวแปร เช่น ดูว่าข้อมูลเป็น string, int, Boolean ฯลฯ คงจะเนื่องจากภาษา C การกำหนดค่าตัวแปร เข้มงวดกว่าภาษาอื่น ถ้าผิดประเภทก็จะ error ได้ แต่ถ้าเรารู้โครงสร้างของข้อมูลที่ได้รับมาอยู่แล้วเราก็สามารถระบุ function ที่จะใช้เรียกอ่านค่าได้เลยไม่ว่าจะ get_string() get_int() อะไรทำนองนั้น

คำสั่ง compile ก็ประมาณนี้ cc getnetpie.c -lcurl -ljson-c -o getnetpie

/***************************************************************************
 * _ _ ____ _
 * Project ___| | | | _ \| |
 * / __| | | | |_) | |
 * | (__| |_| | _ <| |___
 * \___|\___/|_| \_\_____|
 *
 * Copyright (C) 1998 - 2015, Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.haxx.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ***************************************************************************/
/* <DESC>
 * Shows how the write callback function can be used to download data into a
 * chunk of memory instead of storing it in a file.
 * </DESC>
 */
 
 /*
 Things Control. Control anything you want.
 Copyright (C) 2017 Pornthep Nivatyakul

This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.

You should have received a copy of the GNU General Public License
 along with this program. If not, see <http://www.gnu.org/licenses/>.

ThingsControl Copyright (C) 2017 Pornthep Nivatyakul, kaebmoo@gmail.com, seal@ogonan.com
 This program comes with ABSOLUTELY NO WARRANTY;
 This is free software, and you are welcome to redistribute it
 under certain conditions.




*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <curl/curl.h>
#include <json-c/json.h>

struct MemoryStruct {
 char *memory;
 size_t size;
};




void json_parse(json_object * jobj) {
 enum json_type type;
 json_object_object_foreach(jobj, key, val) {
 type = json_object_get_type(val);
 switch (type) {
 case json_type_string: 
 printf("type: json_type_string, ");
 printf("value: %s\n", json_object_get_string(val));
 break;
 case json_type_int: 
 printf("type: json_type_int, ");
 printf("value: %d\n", json_object_get_int(val));
 break;
 }
 }
 
/*
 enum json_type type;
 json_object_object_foreach(jobj, key, val) {
 type = json_object_get_type(val);
 switch (type) {
 case json_type_int: printf("type: json_type_int, ");
 printf("value: %dn", json_object_get_int(val));
 break;
 }
 }
*/
}




static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
 size_t realsize = size * nmemb;
 struct MemoryStruct *mem = (struct MemoryStruct *)userp;

mem->memory = realloc(mem->memory, mem->size + realsize + 1);
 if(mem->memory == NULL) {
 /* out of memory! */
 printf("not enough memory (realloc returned NULL)\n");
 return 0;
 }

memcpy(&(mem->memory[mem->size]), contents, realsize);
 mem->size += realsize;
 mem->memory[mem->size] = 0;

return realsize;
}

int main(void)
{
 CURL *curl_handle;
 CURLcode res;

struct MemoryStruct chunk;
 int array_len, i;




chunk.memory = malloc(1); /* will be grown as needed by the realloc above */
 chunk.size = 0; /* no data at this point */

curl_global_init(CURL_GLOBAL_ALL);

/* init the curl session */
 curl_handle = curl_easy_init();

/* specify URL to get */
 curl_easy_setopt(curl_handle, CURLOPT_URL, "https://api.netpie.io/topic/ThingsControl/seal/status?auth=xxx:yyy");

/* send all data to this function */
 curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);

/* we pass our 'chunk' struct to the callback function */
 curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk);

/* some servers don't like requests that are made without a user-agent
 field, so we provide one */
 curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0");

/* get it! */
 res = curl_easy_perform(curl_handle);

/* check for errors */
 if(res != CURLE_OK) {
 fprintf(stderr, "curl_easy_perform() failed: %s\n",
 curl_easy_strerror(res));
 }
 else {
 /*
 * Now, our chunk.memory points to a memory block that is chunk.size
 * bytes big and contains the remote file.
 *
 * Do something nice with it!
 */
 printf("%s\n", chunk.memory);
 printf("%lu bytes retrieved\n", (long)chunk.size);

json_object *jobj;
 json_object *array;
 int stringlen = 0;
 enum json_tokener_error jerr;
 enum json_type jtype;
 struct json_tokener *tok;
 tok = json_tokener_new();
 char *type_str;
 
 time_t now;
 struct tm ts;
 char lastUpdated[80];

do {
 stringlen = strlen(chunk.memory);
 jobj = json_tokener_parse_ex(tok, chunk.memory, stringlen);
 } while ((jerr = json_tokener_get_error(tok)) == json_tokener_continue);

if (jerr != json_tokener_success)
 {
 fprintf(stderr, "Error: %s\n", json_tokener_error_desc(jerr));
 // Handle errors, as appropriate for your application.
 }
 if (tok->char_offset < stringlen) // XXX shouldn't access internal fields
 {
 fprintf(stderr, "Error: tok->char_offset < stringlen");
 // Handle extra characters after parsed object as desired.
 // e.g. issue an error, parse another object from that point, etc...
 }
 jtype = json_object_get_type(jobj);
 switch(jtype) {
 case json_type_null:
 type_str = "NULL";
 break;
 case json_type_boolean:
 type_str = "BOOLEAN";
 break;
 case json_type_double:
 type_str = "DOUBLE";
 break;
 case json_type_int:
 type_str = "INT";
 break;
 case json_type_string:
 type_str = "STRING";
 printf("%s\n", json_object_get_string(jobj));
 break;
 case json_type_object:
 type_str = "OBJECT";
 break;
 case json_type_array:
 type_str = "ARRAY";
 break;
 }
 printf("Type %s\n", type_str);




printf("new_obj.to_string()=%s\n", json_object_to_json_string(jobj));

printf("array length %d\n", json_object_array_length(jobj));
 for (i=0; i < json_object_array_length(jobj); i++) {
 printf("%s\n", json_object_to_json_string(json_object_array_get_idx(jobj, i))); 
 }

array = json_object_object_get(json_object_array_get_idx(jobj,0), "topic");
 printf("payload : %s\n", json_object_to_json_string(array));
 
 array = json_object_object_get(json_object_array_get_idx(jobj,0), "payload");
 printf("payload : %s\n", json_object_to_json_string(array));
 
 array = json_object_object_get(json_object_array_get_idx(jobj,0), "lastUpdated");
 now = json_object_get_int(array);
 ts = *localtime((const time_t *) &now);
 strftime(lastUpdated, sizeof(lastUpdated), "%a %Y-%m-%d %H:%M:%S %Z", &ts);
 printf("payload : %d, %s\n", json_object_get_int(array), lastUpdated);

array = json_object_object_get(json_object_array_get_idx(jobj,0), "retain");
 printf("payload : %d\n", json_object_get_boolean(array));


 /*
 jobj = json_object_object_get(jobj, "payload");
 printf("topic : %s\n", json_object_to_json_string(jobj));
 */

}

/* cleanup curl stuff */
 curl_easy_cleanup(curl_handle);

free(chunk.memory);

/* we're done with libcurl, so clean it up */
 curl_global_cleanup();

return 0;
}

run program ก็จะได้ประมาณนี้

pi@register:~/thingscontrol $ ./getnetpie 
[{"topic":"/ThingsControl/seal/status","payload":"ON","lastUpdated":1497340128,"retain":true}]
94 bytes retrieved
Type ARRAY
new_obj.to_string()=[ { "topic": "\/ThingsControl\/seal\/status", "payload": "ON", "lastUpdated": 1497340128, "retain": true } ]
array length 1
{ "topic": "\/ThingsControl\/seal\/status", "payload": "ON", "lastUpdated": 1497340128, "retain": true }
payload : "\/ThingsControl\/seal\/status"
payload : "ON"
payload : 1497340128, Tue 2017-06-13 14:48:48 +07
payload : 1
pi@register:~/thingscontrol $

ความที่ netpie ส่งมาเป็น array กว่าจะงมหาวิธีแงะได้ ลองอยู่หลายรอบ เพราะตัวอย่างที่ค้นเจอ มันมักจะ Parse json แบบ { “topic”:”value”, “status”,”ON” } อะไรงี้ แถม array ไม่มีชื่อ object แบบในตัวอย่างอีก โห กูเหนื่อย 😉 เริ่มมา ปิ๊ง ปิ๊ง หลังจากเจออันนี้ https://stackoverflow.com/questions/10164741/get-jsonarray-without-array-name  และนี่ http://www.jsontest.com/#validate ก็เลยรอดมาได้

เริ่มจากไม่รู้เรื่องเลยก็ใช้เวลาประมาณ 2 วัน 🙂 วันแรกก็ งมการใช้ cURL วันที่สองก็งม parse json หวังว่าท่านจะต่อยอดไปได้เร็วขึ้น

ทำไม ไม่ใช้ python, JavaScript?

อันนั้นมันมีตัวอย่างเยอะละ ที่สำคัญคือ กูเขียนไม่เป็น ha ha ผมมันคนโบราณ ใช้ C แบบถึกถึก นี่แหละ

ปล. code ตัวอย่าง จะมีที่ไม่เกี่ยวข้องอยู่พอสมควร ซึ่งผมใช้ในการ debug ตัว program และเป็นตัวอย่างการใช้งานต่าง ๆ