Things Control

ThingsControl – control anything as you want.

ผมเริ่มหัดเขียน php, c เพื่อคุยกับ netpie เมื่อประมาณต้นเดือนมิถุนายน และหัดเขียน app inventor เมื่อราวราว 30 พฤษภาคม 2560

จนวันนี้ ใช้เวลาประมาณ 1 เดือนเต็มเต็ม งานที่ทำอยู่ก็สำเร็จในเบื้องต้น รวมเวลาสั่งของ (hardware) มาลองด้วย

แรงบันดาลใจก็มาจาก sonoff หรือ smart switch ที่บริษัท จีนทำขายไปทั่วโลกในราคาไม่แพง นอกเหนือจากนั้นอีกแรงขับหนึ่งก็คือ ความหวงวิชาของหน่วยงานรัฐ หรือแม้แต่คนในวงการเดียวกันเอง ถึงตอนนี้ ผมก็ยังคิดว่า ทำไมจะต้องมาเสียเวลาไป 1 เดือนเพื่อทำในสิ่งที่ คนอื่นก็ทำกันอยู่แล้ว และต้องมาทำซ้ำซ้ำกัน จริงอยู่นะ ว่า พอมาทำจริงจริงมันก็ไม่ได้ง่ายนัก ถ้าจะทำเพื่อให้ใช้งานได้จริง ไม่ใช่ทำแค่ ปิด เปิด หลอดไฟได้ แบบที่ ก็ทำกันมาหลายปี เวลาหัดทำ หัดใช้งาน ใหม่ใหม่ มันก็มีความยากอยู่พอสมควร แต่ถ้าจะคิดในมุม เรื่องพื้นฐานแบบนี้ ถ้าเผยแพร่ได้ มันจะช่วยให้วงการมันพัฒนาไปได้อีกมาก เพราะสาระของเรื่องนี้ มันไม่ได้อยู่ตรงนี้แล้ว มันต้องต่อยอดออกไปอีก ไม่ว่าจะเป็นเรื่อง big data หรือ machine learning ลำพังแค่จะทำตัวสร้างข้อมูลยังจะหวงกัน ก็ไม่ต้องไปถึงไหนกัน

ครับ ถ้าท่านหลงมาเจอ ก็ลองไปดูที่นี่ เผื่อจะสนใจ https://github.com/kaebmoo/thingscontrol

มันคืออุปกรณ์เปิด ปิด อุปกรณ์ไฟฟ้า ที่สั่งงานผ่าน smart phone (android) สั่งผ่าน internet โดยสามารถตั้งเวลาควบคุมการ เปิด ปิด หรือระยะเวลาที่ให้ทำงาน ได้

 

อ่าน/เขียน ข้อมูลไปยัง netpie ด้วย app inventor

screen_read_write_netpie_appinventor

 

blocks(1)

ใช้ผ่าน web component ที่อยู่ในหมวด connectivity ซึ่งใช้เรียก REST API ของ netpie โดยกำหนด uri คือ https://api.netpie.io/topic/ชื่อแอพที่สร้างในnetpie/ชื่อtopic?retain&authen=key:secret

เช่น https://api.netpie.io/topic/sControl/sensor/light?retain&auth=key:secret

ตรง key และ secret ใส่ key และ secret ที่ได้จากการสร้างแอพพลิเคชั่นจาก netpie

application_netpie

ลองส่งข้อมูลขึ้นด้วยการใส่ค่าใน textbox แล้วกดปุ่ม [post to netpie] โปรแกรมจะเรียก web method PUT Text เพื่อส่งค่า

ลองอ่านค่าจาก netpie ด้วยการกดปุ่ม [get value from netpie] โปรแกรมจะเรียก web method GET

ค่าที่ได้จาก netpie จะหน้าตาประมาณนี้

[{“topic”:”/sControl/sensor/light”,”payload”:”ํYES”,”lastUpdated”:1498810051,”retain”:true}]

จากนั้นจะมีการตรวจค่าใน Event GotText ก็ทำการตรวจสอบว่าสามารถอ่านค่าได้หรือไม่ (responseCode = 200) แล้วก็ทำการอ่านค่าโดยแปลงจาก json มาใส่ใน List (item) เพื่อใช้ค้นหา key ที่เราสนใจ ซึ่งในที่นี้จะอยู่ใน key ชื่อ payload

 

ตรง if else ที่ตรวจสอบค่าว่าเป็น PUT, หรือ GET เพื่อไม่ให้เกิด error เนื่องจากเวลา PUT netpie จะส่งค่า JSON กลับมาไม่เหมือนกับตอน GET จะได้แสดงผลถูก

ในตัวอย่างนี้มีการใช้ clock ทุก 5 วินาที (อย่าลืมตั้งค่า TimerInterval = 5000) เพื่ออ่านค่า ดังนั้นก็ไม่จำเป็นต้องกดปุ่ม [get value from netpie] ก็ได้ เพราะโปรแกรมจะอ่านค่าจาก netpie มาทุกทุก 5 วินาที เพื่อมาแสดงผล

ส่วนถ้าจะยัด microgear ลงไปใน component ของ app inventor นี่ อันนี้ขอผ่าน ฝีมือยังไม่ถึง รอเซียนทั้งหลายเขาทำมาละกัน 😛

download code

more information

ปล. app inventor มีออกชุดพัฒนาสำหรับ iot แล้วนะ ลองดูที่นี่ http://iot.appinventor.mit.edu

ใช้งาน bluetooth บน Raspberry Pi

ติดตั้งแบบ download source code และ compile เอง (ใช้เวลานานมากพอสมควรตอน compile)

อ่านที่ https://learn.adafruit.com/install-bluez-on-the-raspberry-pi/installation

download BlueZ Package http://www.bluez.org/download/

cd ~

wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.45.tar.xz

tar xvf bluez-5.45.tar.xz

cd bluez-5.45

ติดตั้ง Lib ที่เกี่ยวข้อง


sudo apt-get update
sudo apt-get install -y libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev

ทำการ compile


./configure

make

sudo make install

systemctl status bluetooth

sudo systemctl start bluetooth

sudo systemctl stop bluetooth

sudo systemctl enable bluetooth

sudo systemctl disable bluetooth

Enable Bluetooth Low Energy Features


sudo nano /lib/systemd/system/bluetooth.service

Enable the experimental features by adding –experimental to the ExecStart line.

sudo systemctl daemon-reload

sudo systemctl restart bluetooth

อันนี้อีกวิธี แบบไม่ต้อง compile เอง

https://github.com/EnableTech/raspberry-bluetooth-demo
sudo apt-get install pi-bluetooth
sudo apt-get install bluetooth bluez

sudo apt-get install python-bluez

sudo hciconfig hci0 piscan

sudo hciconfig hci0 name 'Device Name' [change your device name to something else you fancy]

sudo python /usr/share/doc/python-bluez/examples/simple/inquiry.py

Running the Bluetooth Server run the rfcomm-server example:

ก่อนใช้คำสั่งนี้ เพื่อ run rfcomm-server
อ่าน https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=133263

run คำสั่ง sudo sdptool add SP แล้วเพิ่ม -C เข้าไปในไฟล์ /etc/systemd/system/dbus-org.bluez.service หลัง ‘bluetoothd’. จากนั้น Reboot. ไม่งั้นมันจะขึ้น error ประมาณนี้

Traceback (most recent call last):
File "/usr/share/doc/python-bluez/examples/simple/rfcomm-server.py", line 20, in <module>
profiles = [ SERIAL_PORT_PROFILE ],
File "/usr/lib/python2.7/dist-packages/bluetooth/bluez.py", line 176, in advertise_service
raise BluetoothError (str (e))
bluetooth.btcommon.BluetoothError: (2, 'No such file or directory')

sudo python /usr/share/doc/python-bluez/examples/simple/rfcomm-server.py

ลอง download โปรแกรม https://play.google.com/store/apps/details?id=es.pymasde.blueterm&hl=en

มาลองส่งข้อมูลจากโทรศัพท์ android ไปยัง raspberry pi ผ่าน bluetooth

เปิดโปรแกรมกด เรียกเมนู เพื่อค้นหาอุปกรณ์ตามที่เราตั้งชื่อ แล้วทำการเชื่อมต่อ ต่อได้ก็ลองพิมพ์ข้อความดูผลที่เกิดขึ้น

อ่านค่า / ส่งค่า 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 และเป็นตัวอย่างการใช้งานต่าง ๆ

 

 

อ่านค่า (topic) จาก netpie ด้วย PHP rest api

จาก

อันดับแรกไป download httpful มาจากนี่ http://phphttpclient.com/downloads/httpful.phar

ถ้ายังไม่มี lib curl สำหรับ php ต้องติดตั้งก่อน (ดูตัวอย่าง https://stackoverflow.com/questions/6382539/call-to-undefined-function-curl-init)

sudo apt-get install php5-curl

ลองส่งข้อมูลขึ้นไปบน netpie (ถ้ายังไม่มี curl ก็ติดตั้งก่อน)

curl -X PUT "https://api.netpie.io/topic/APPID/TOPIC/debug?retain&auth=KEY:SECRET" -d ":-)"

โดยแทนค่า APPID ด้วยชื่อ APP ของเราที่สร้างใน netpie
แทนค่า TOPIC ด้วยชื่อที่ต้องการ เช่น Room
แทนค่า KEY ด้วยค่า key ที่อยู่ใน APP ที่สร้างขึ้นใน netpie
แทนค่า SECRET ด้วยค่า secret ที่อยู่ใน APP ที่สร้างขึ้นใน netpie

ถ้ายังไม่มีก็ไปสมัครใช้งานก่อนที่ netpie.io

debug ก็เป็น topic หรือตัวแปรตัวหนึ่ง ในที่นี้เรากำหนดค่าให้มัน เท่ากับ “:-)” ด้วยคำสัั่ง -d “:-)”

retain คือการให้ netpie รับข้อมูลไปแล้วคงค่าไว้

auth= คือ parameter ที่จะต้องส่งไปให้ netpie ตรวจสอบยืนยันตัวตนด้วยค่า key และ secret

<!--?php 

        include "httpful.phar";
        // กำหนด uri
        $uri = "https://api.netpie.io/topic/APPID/TOPIC/debug?auth=KEY:SECRET";
        
        // สั่งให้เรียก uri โดยใช้คำสั่ง GET เพื่อขออ่านค่าจาก netpie
        $response = \Httpful\Request::get($uri)->send();
        
        // แปลงค่าที่ได้จาก netpie จาก json มาเป็น array
        $result = json_decode($response->body, true);

        // ลองพิมพ์ค่าที่ได้ออกมาดู 
        // netpie จะเก็บค่าของตัวแปรไว้ใน json ที่ชื่อ payload
        echo "debug = " . $result[0]['payload'] . "\n";
        $debug = $result[0]['payload'];
        
        echo $response . "\n" ;
        sleep(1);
?-->

ถ้าต้องการอ่านค่าอื่น ก็ทำซ้ำใหม่ตั้งแต่การกำหนด uri โดยแค่เปลี่ยนจากตัวแปร debug ไปเป็นอื่น ๆ เช่น switch, pump เป็นต้น

$uri = "https://api.netpie.io/topic/APPID/TOPIC/switch?auth=KEY:SECRET";

อย่าลืมลองส่งค่าขึ้นไปก่อน

curl -X PUT "https://api.netpie.io/topic/APPID/TOPIC/switch?retain&auth=KEY:SECRET" -d "ON"

ระหว่างการ อ่านค่าตัวแปรแต่ละครั้งควรมีการตั้งค่าเวลาระหว่างการอ่านอย่างน้อย 1 วินาทีด้วยคำสั่ง sleep(1) (ตามข้อกำหนด) ถ้าไม่จำเป็นต้องอ่านค่าแบบทันทีทันใดก็ตั้งให้นานกว่านั้นได้ เพราะบางครั้งถ้าระบบบริการของ netpie มีปริมาณการส่งข้อมูลมากมาก อาจเกิดการล่าช้าในการปรับปรุงค่าตัวแปรได้ ทำให้การอ่านค่าบางครั้งจะไม่ได้ค่าที่เป็นปัจจุบันเนื่องจากระบบอยู่ระหว่างการประมวลผล

ตัวอย่างผลลัพธ์

pi@register:/var/www/html $ php read.netpie.data.php 
HH = 06
[{"topic":"/ThingsControl/seal/HH","payload":"06","lastUpdated":1496741674,"retain":true}]
MM = 30
[{"topic":"/ThingsControl/seal/MM","payload":"30","lastUpdated":1496741674,"retain":true}]
Enable = true
[{"topic":"/ThingsControl/seal/Enable","payload":"true","lastUpdated":1496740819,"retain":true}]
On Timer = 20
[{"topic":"/ThingsControl/seal/OnTimer","payload":"20","lastUpdated":1496741673,"retain":true}]
Sun = false
[{"topic":"/ThingsControl/seal/Sun","payload":"false","lastUpdated":1496740819,"retain":true}]
Mon = 1
[{"topic":"/ThingsControl/seal/Mon","payload":"1","lastUpdated":1496741676,"retain":true}]
Tue = false
[{"topic":"/ThingsControl/seal/Tue","payload":"false","lastUpdated":1496741676,"retain":true}]
Wed = 3
[{"topic":"/ThingsControl/seal/Wed","payload":"3","lastUpdated":1496741670,"retain":true}]
Thu = false
[{"topic":"/ThingsControl/seal/Thu","payload":"false","lastUpdated":1496740808,"retain":true}]
Fri = 5
[{"topic":"/ThingsControl/seal/Fri","payload":"5","lastUpdated":1496741667,"retain":true}]
Sat = false
[{"topic":"/ThingsControl/seal/Sat","payload":"false","lastUpdated":1496741668,"retain":true}]
pi@register:/var/www/html $

*** ทดสอบการใช้งานบน raspberry pi 3

แถมตัวอย่างเขียนด้วย Python (อย่าลืมแทนค่า KEY และ SECRET ในตัวอย่าง ด้วยค่าที่ได้จาก netpie) พึ่งหัดเขียน python วันแรก เอาแค่นี้ไปก่อน 😛 ไว้ใช้เป็นเมื่อไหร่จะมาเขียนต่อ   ตัวอย่างใน web ก็มีหลายอันอยู่ เช่น http://raspberrypi-thailand.blogspot.com/2016/07/raspiberry-pi-netpie.html

import microgear.client as client
import logging
import time

appid = "ThingsControl"
gearkey = "KEY"
gearsecret = "SECRET"

client.create(gearkey,gearsecret,appid,{'debugmode': True})

def connection():
    #print "Now I am connected with netpie"
    logging.debug("Now I am connected with netpie")

def subscription(topic,message):
    #print topic + " " + message
    logging.debug(topic + " " + message)

def disconnect():
    logging.debug("disconnected")

client.setalias("ThingsControl")
client.on_connect = connection
client.on_message = subscription
client.on_disconnect = disconnect
client.subscribe("/seal/Sun")
time.sleep(1)
client.subscribe("/seal/Sat")
time.sleep(1)

client.subscribe("/seal/HH")
time.sleep(1)

client.subscribe("/seal/MM")

client.connect()

จบ

RTC Setup on Raspberry Pi (x300 expansion board)

First enable I2C

pi@register:~ $ sudo raspi-config

RTC_2

RTC_3

RTC_4.PNG

check that the RTC has been detected.

pi@register:~ $ sudo i2cdetect -y 1

The RTC should appear on channel 68.

RTC_1

check file /etc/modprobe.d/raspi-blacklist.conf 

remove or comment line where the I2C is black listed

#blacklist i2c-bcm2708

edit file /boot/config.txt

change dtoverlay=w1-gpio to dtoverlay=i2c-rtc,ds3231

edit /lib/udev/hwclock-set with

pi@register:~ $  sudo nano /lib/udev/hwclock-set

locate three line and  remark it with #

if [ -e /run/systemd/system ] ; then
exit 0
fi

RTC_5

RTC_6

save and exit

reboot your raspberry pi : sudo reboot

set date time : sudo date MMDDHHMMYYYY.SS (MM=Month, DD=Date, HH=Hour, MM=Minute, YYYY=Year, SS=Second example sudo date 05300957201704 = 2017 May 30 09:57:04)

write the system time to expansion board

sudo hwclock -w

read the time to verify

sudo hwclock -r

 

ใช้ Raspberry Pi ทำ Wifi Access Point, Transparent Proxy (web ด้วย)

โจทย์มีว่า ต้องการใช้ผู้ใช้งาน ใช้งานผ่าน wifi

Raspberry Pi 3 มี Wifi ในตัวและมี Ethernet port อีก 1 มี USB อีก 4 ช่อง

การเชื่อมต่อออกเน็ตก็สามารถทำผ่านการเชื่อมทาง Ethernet port ซึ่งก็น่าจะธรรมดาทั่วไป แต่ไม่สะดวกตรงที่ต้องเดินสายเชื่อมต่อ อยู่ที่บ้านก็คงไม่มีประเด็น แต่ถ้าจะเอาไปไหนมาไหนด้วย หรือเอาไปใช้งานในที่ต่าง ๆ เอาไปจัดงานครั้งคราว การเชื่อมต่อผ่าน wifi เพื่อออกอินเทอเน็ตน่าจะสะดวกกว่า ดังนั้นก็จะใช้ wifi ที่มีมาใน pi ต่อออกเน็ต พอใช้ต่อออกเน็ตแล้วจะ share internet ต่ออย่างไร

วิธีที่ 1 share ผ่านทาง  Ethernet port ด้วยการหา wifi access point มาต่ออีกตัว เอาสายต่อจาก WAN Port ของ access point มาที่  Ethernet port ของ pi ระหว่าง port นี้จะตั้งเป็น static ip address ก็ได้ หรือไม่ก็ให้ pi แจก IP ไปให้ access point

ผู้ใช้งานก็เกาะกับ wifi access point ข้อมูลก็วิ่งผ่านออกไปทาง pi ออกเน็ตไปทาง wifi ของ pi

แบบนี้ ลงโปรแกรมพวก แจก IP เช่น dnsmasq หรือ isc-dhcp ก็จบ ที่ใช้ dhcp ก็เพื่อจะได้ไม่ต้องมาตั้งค่า IP ให้ WAN port ของ access point จะได้ง่าย

/etc/dnsmasq.conf

interface=eth0
listen-address=172.24.1.1
bind-interfaces
server=8.8.8.8
domain-needed
bogus-priv
dhcp-range=interface:eth0,172.24.1.10,172.24.1.250,12h

/etc/network/interfaces

allow-hotplug eth0
iface eth0 inet static
address 172.24.1.1
netmask 255.255.0.0

ใช้คำสั่ง iptables เพื่อควบคุมการส่งผ่านข้อมูล

sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
sudo iptables -A FORWARD -i wlan0 -o eth0 -m state –state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT

อย่าลืมแก้ไขในไฟล์ sudo nano /etc/sysctl.conf ลบเครื่องหมาย # หน้า net.ipv4.ip_forward=1 ออก หรือ พิมพ์ net.ipv4.ip_forward=1 เพิ่มเข้าไปถ้าไม่มี ถ้าอยากให้ทำงานทันทีโดยไม่ต้อง reboot เครื่อง ให้พิมพ์ sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"

จากนั้นใช้คำสั่ง sudo sh -c "iptables-save > /etc/iptables.ipv4.nat" เพื่อเก็บคำสั่งของ iptables ไว้ แล้วเอาไปใส่ไว้ใน rc.local เพื่อที่เวลาเปิดเครื่องใหม่ มันจะได้ทำงานทุกครั้ง (ไม่ต้องมาพิมพ์ใหม่) แก้ไขไฟล์ rc.local ด้วยคำสั่ง sudo nano /etc/rc.local เพิ่มคำสั่งนี้เข้าไปก่อนบรรทัด exit 0

iptables-restore < /etc/iptables.ipv4.nat 

เสร็จแล้วก็

sudo service dnsmasq start

ยังไม่พอใจ แบกเอา access point ไปด้วยมันเกะกะ ต้องมีสองกล่อง

วิธีที่ 2 เพิ่ม wifi เข้าไปที่ Raspberry Pi อีกอัน ไปซื้อ usb wifi มาใส่ ข้อควรระวังคือ ส่วนใหญ่ถ้าใช้ usb wifi สำหรับต่อออก internet ไม่ค่อยมีปัญหากับยี่ห้อ แต่ถ้าจะใช้ แบบในรูป มาต่อแล้วจะให้มันเป็นทำตัวเป็น access point wifi แล้วละก็ ต้องระวังในการเลือกซื้อเป็นอย่างมาก เพราะ software (hostapd) ที่จะใช้มักมีข้อจำกัดเรื่อง driver ที่จะรองรับกับ chip ที่ใช้ใน usb wifi ถ้าเลือกมาไม่ตรงกับที่มันรองรับก็งานงอก ไม่เป็นไร ถ้าหาไม่ได้ เราก็ใช้ wifi on board ที่มีมากับ pi 3 ในการ share เป็น access point แทน

pi_wifi_IMG20170510004001

แล้วก็ออกเน็ตผ่าน usb wifi ที่ใส่เพิ่ม งานนี้ต้องลงโปรแกรมเพิ่มอีกตัวคือ hostapd

# ชื่อของ wifi interface ที่จะใช้ share
interface=wlan0
driver=nl80211

# ชื่อของ wifi access point
ssid=CAT-Register

hw_mode=g

# Use channel 6
channel=6
# Enable 802.11n
ieee80211n=1

# Enable WMM
wmm_enabled=1

# Enable 40MHz channels with 20ns guard interval
ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40]

มีแค่นี้ ถ้าไม่ต้องการให้มีการเข้ารหัสหรือถามรหัสผ่านเวลาที่จะเชื่อมต่อ

แต่ถ้าอยากให้ใส่รหัสเวลาเชื่อมต่อด้วยก็เพิ่ม

# Accept all MAC addresses
#macaddr_acl=0

# Use WPA authentication
#auth_algs=1

# Require clients to know the network name
#ignore_broadcast_ssid=0

# Use WPA2
#wpa=2

# Use a pre-shared key
#wpa_key_mgmt=WPA-PSK

# The network passphrase
#wpa_passphrase=raspberry

# Use AES, instead of TKIP
#rsn_pairwise=CCMP

ตัวอย่างเอามาจากที่นี่ https://frillip.com/using-your-raspberry-pi-3-as-a-wifi-access-point-with-hostapd/

ปัญหาที่เจอคือ ถ้า ใส่ผิดใส่ถูก ใส่ขาด ใส่เกิน แม่…ง ไม่ทำงานครับ ท่านผู้ชม อยากรู้ก็ไปลองเองละกัน เพราะปัญหานี้ทำเอาวนไปวนมาลองผิดลองดูมั่วไปเรื่อยอยู่สองวัน อย่าลืมตั้งค่า ip address ให้ wlan0 ด้วย

allow-hotplug wlan0
iface wlan0 inet static
address 192.168.9.1
netmask 255.255.255.0

เอาบรรทัดนี้ที่อยู่ใต้ wlan0 ออกด้วย  wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf ของ wlan1 ไม่ต้องเอาออก

หน้าตาก็จะประมาณนี้

source-directory /etc/network/interfaces.d

auto lo
iface lo inet loopback

allow-hotplug eth0
iface eth0 inet static
address 172.24.1.1
netmask 255.255.0.0

allow-hotplug wlan0
iface wlan0 inet static
address 192.168.9.1
netmask 255.255.255.0

allow-hotplug wlan1
iface wlan1 inet manual
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf

อีกนิด แก้ไขไฟล์ sudo nano /etc/dhcpcd.conf ด้วยการเพิ่มคำสั่ง

denyinterfaces wlan0 denyinterfaces eth0

เอาไว้ก่อนคำสั่ง interface อื่นใด (ถ้ามี) เพื่อไม่ให้ interface เหล่านี้มันรับค่าจาก dhcp

แล้วก็ sudo service dhcpcd restart จากนั้นก็สั่ง

sudo ifdown wlan0

sudo ifup wlan0

sudo ifdown eth0

sudo ifup eth0

ถ้าไม่ได้ทำจาก console แล้ว remote เชื่อมเข้าไปผ่าน สายหรือ, Wifi ก็ระวังละกันตอน ifdown มันก็จะหลุดแล้วท่านก็จะเข้าไม่ได้ ดังนั้นยังไม่ทำตอนนี้ก็ได้ ให้เสร็จทุกอย่างแล้ว reboot เครื่องทีเดียว

เพิ่ม config ของ dnsmasq ให้จ่าย ip address ไปทาง wlan0 จะได้หน้าตาประมาณนี้

listen-address=172.24.1.1
listen-address=192.168.9.1
bind-interfaces
server=8.8.8.8
domain-needed
bogus-priv
dhcp-range=interface:eth0,172.24.1.10,172.24.1.250,12h
dhcp-range=interface:wlan0,192.168.9.1,192.168.9.250,1h

จัดการรับส่งข้อมูลระหว่าง interface ด้วย

sudo iptables -A FORWARD -i wlan1 -o wlan0 -m state –state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i wlan0 -o wlan1 -j ACCEPT

จากนั้นก็ sudo sh -c "iptables-save > /etc/iptables.ipv4.nat" เพื่อ save ค่า

จากนั้น (ถ้า config ip address eth0, wlan0 แล้ว) ก็ลอง

sudo service hostapd start  
sudo service dnsmasq start 

หรือไม่ก็ sudo reboot รอ boot เสร็จก็ลองดูว่า มีสัญญาณ CAT-Register ปรากฎขึ้นไหมแล้วก็ลองเชื่อมต่อดูว่าเล่นเน็ตได้หรือไม่ สำหรับการเชื่อมต่อให้ wifi (wlan1) ออกเน็ตนั้นจะทำผ่าน GUI ของ rasbian pixel ก็ได้ (ง่ายดี) หรือ

ใส่ ssid ของ wifi เข้าไปในไฟล์   /etc/wpa_supplicant/wpa_supplicant.conf เช่น

network={
ssid=”CAT-Mobile”
key_mgmt=NONE
}

network={
ssid=”seal”
psk=”12345678″
key_mgmt=WPA-PSK
}

แล้วก็ restart networking (sudo service networking restart)

ท่อง web ได้แล้ว แต่กลัวช้า กลัวไม่แรงพอ ยัด proxy ลงไป ด้วย nginx

ใน /etc/nginx/sites-available/default

เพิ่ม

# Proxy server
#
server {
resolver 192.168.9.1;
access_log off;
listen 8080;
listen [::]:8080;

location / {
proxy_pass      $scheme://$host$request_uri;
proxy_set_header Host $http_host;
proxy_buffers   256 4k;
proxy_max_temp_file_size        0k;

}

}

จากนั้นก็ sudo service nginx restart

บังคับให้การเข้า web (ดัก 80 port) ให้วิ่งเข้า proxy ของ nginx ทาง 8080 port

sudo iptables -t nat -A PREROUTING -s 192.168.9.0/24 -p tcp –dport 80 -j DNAT –to 192.168.9.1:8080

อย่าลืม sudo sh -c "iptables-save > /etc/iptables.ipv4.nat" จะได้ไม่มีปัญหาเวลา boot เครื่องใหม่

จบข่าว

งานนี้ก็จะได้ raspberry pi 3 เก๋ หนึ่งเครื่อง บวก usb wifi หิ้วไปไหนมาไหนได้ เอาไว้ share net ได้สบายสบาย ทำอย่างอื่นได้ไหม ทำได้ อยู่แล้ว เพราะที่ทำไว้แบบนี้ ก็เพื่องานบางอย่าง ซึ่งอาศัยพื้นฐานของการตั้งค่าแบบนี้ ส่วนจะเอาไปทำอะไรได้นั้น ก็แล้วแต่ญาติโยมละเน้อ ยกตัวอย่างให้อันหนึ่ง เช่น เรามีสิทธิใช้งาน wifi แบบที่ต้องตรวจสอบ MAC Address และเราก็มีสิทธิให้ใช้ได้แค่ 10 อุปกรณ์ (10 ยังไม่พออีกเหรอ) พอมันครบสิบแล้ว แต่ถ้าเรามีอุปกรณ์ตัวที่ 11 มาจะทำอย่างไร ไอ้กล่องนีี้ช่วยได้ ก็เอาเจ้ากล่องนี้ไปลงทะเบียน 1 ใน 10 ของอุปกรณ์ที่เขาให้สิทธิ แล้วก็ให้มัน share wifi ต่อมาให้อุปกรณ์อื่นของเรา เท่านี้ก็สบายแล้ว ไชโย 😛

ปล. ถ้าออกเน็ตผ่าน 3G ล่ะ ทำไง ไปย้อนอ่านบทความก่อนหน้า แล้วประยุกต์เอาเอง มันทำได้อยู่แล้ว