วันศุกร์ที่ 4 เมษายน พ.ศ. 2557

ตอนที่ 2 : Ketai กับ Fleet Monitoring

ผมคิดอยู่ว่าจะนำเสนอ Ketai ยังไงให้น่าติดตาม ครั้นจะมาเล่าทีละส่วนของมันก็อาจจะทำให้น่าเบื่อ ก็เลยคิดว่าจะนำเสนอในลักษณะเป็นการนำไปใช้งาน น่าจะดีกว่า ก็เลยทดลองเขียนบทความออกมาในลักษณะนี้ครับ

โจทย์ของผมก็คือ ทำการเก็บข้อมูลของรถบรรทุก แล้วส่งกลับไปที่ Center ข้อมูลที่จะส่งกลับก็เช่น ตำแหน่งรถ ระดับน้ำมันในถัง การเปิดปิดประตูห้องเก็บสินค้า

สิ่งที่ต้องเรียนรู้เพิ่มเติม คือ การสร้างโปรแกรมบน Server เพื่อรองรับค่าที่ส่งมา บันทึกลงใน mySQL

Ketai ทำหน้าที่อ่านตำแหน่งโดยไม่ใช้ GPS แต่จะอ่านตำแหน่งจากข้อมูลของ WIFI Network Location ซึ่งจะมีความแม่นยำอยู่ในระดับประมาณ 40 - 80 เมตร เทียบกับ GPS ที่ประมาณ 5 เมตร

Arduino อ่านการเปิด-ปิดประตู และระดับน้ำมัน มาประกอบกับข้อมูลตำแหน่ง

แพ็กข้อมูลแล้วก็ส่งไปที่ Server ผ่าน Internet

เริ่มที่ Ketai

ทำการ download Ketai libaray มาไว้ใน Processing ก่อนครับ จากนั้นก็เขียนโปรแกรมแบบ Android
ก่อน Run ให้กำหนดใช้ ACCESS_FINE_LOCATION ในเมนู Android -> Sketch Permission เสียก่อน
การทดสอบกับ Android บางตัวจะไม่มีในส่วนของการอ่านค่า location จาก Network ทำให้ไม่สามารถแสดงค่าตำแหนงได้ สำหรับมือถือส่วนใหญ่ใช้งานได้


import ketai.sensors.*;

KetaiLocation location;                     //   เรียกใช้ location library
double longitude, latitude, altitude;  //   เก็บค่า ตำแหน่ง
float accuracy;                                    //    ความแม่นยำ

void setup(){
  orientation(LANDSCAPE);
  textAlign(CENTER,CENTER);
  textSize(36);
  location = new KetaiLocation(this);   // เปิดการใช้งาน
}

void draw() {
  background(128);
  if(location.getProvider() == "none")
    text("Location data is unavailable. \n" +
      "Please check your location settings.", width/2,height/2);
  else
    text("Latitude: " + latitude + "\n" +
      "Longitude: " + longitude + "\n" +
      "Altitude: " + altitude + "\n" +
      "Accuracy: " + accuracy + "\n" +
      "Provider: " + location.getProvider(), width/2, height/2);
}

void onLocationEvent(double _latitude, double _longitude, double _altitude, float _accuracy){
  longitude = _longitude;
  latitude = _latitude;
  altitude = _altitude;
  accuracy = _accuracy;
  println("lat/lon/alt/acc: " + latitude + "/" + longitude + "/" + altitude + "/" + accuracy);
}

ผลการ Run แสดงตำแหน่ง อันนี้ใช้ WIFI Network


เมื่อเปิด GPS

ต่อไปก็จะเป็นการเตรียมการที่ Server

การติดตั้ง XAMPP สำหรับทำ Server บน PC ให้ดาวน์โหลดไฟล์


ให้เลือก XAMPP for Widows แล้วติดตั้ง ขอให้ระบุตำแหน่งที่ติดตั้งเป็น C:\ เพื่อความสะดวก
เมื่อติดตั้งเสร็จให้ทดสอบโดยพิมพ์ http://localhost ที่ Browser จะเห็นหน้าตาแบบนี้ เป็นอันใช้ได้ ตอนนี้เรามี Server ใช้แล้ว


การบันทึกข้อมูลต้องใช้ PHP ก็เลยต้องทดสอบการทำงานของ PHP กันก่อน ให้กดที่ phpinfo() จะได้หน้าตานี้


ในส่วนของฐานข้อมูล จะยังไม่ใช้ mySQL เพราะจะทำให้เนื้อหายืดยาวเกินไป ก็เลยจะเก็บข้อมูลไว้ใน Text file แทน ซึ่งต้องเขียนไฟล์ PHP ขึ้น ตามนี้ แล้วนำไปวางไว้ที่ C:\xampp\htdocs\xampp 

<?php
// Geolocation Device Locator PHP Script
// Writing to a text file on a web server

if(isset($_GET['get']))
{
 $filename = $_GET['get'].".txt";
 if(file_exists($filename))
{
 $file = file_get_contents($filename);
 echo $file;
} else
echo "ERROR! No location found for " . $_GET['get'];
}
//if the request is an update,we dump the location into a file
// named after the device making the request
 else if(isset($_GET['update']) && isset($_GET['location']))
{
 $fh =fopen($_GET['update'].".txt", "w");
if($fh == FALSE)
{
echo "ERROR. Cannot open file on server.";
return;
}
 if(fwrite($fh, $_GET['location']."\n") == FALSE)
echo "ERROR. Writing to file.";
 if(fclose($fh) == FALSE)
echo "ERROR. Closing file,";
}
?>

ทดลองให้ PHP ทำงาน ด้วยการพิมพ์ http://localhost/xampp/location.php?get=Test ใน Browser จะแสดงข้อความว่า 

ERROR! No location found for Test

ถ้าได้ตามนี้ ก็โอเคครับ ต่อไปจะเพิ่มการบันทึกตำแหน่งไปที่ Server

ให้เพิ่มโค๊ดต่อจากโปรแกรมเดิม จุดที่สำคัญคือ 

serverIP เป็น IP ของเครื่อง PC ที่ติดตั้ง XAMPP นะครับ ถ้าไม่รู้ใหัเข้าที่ Command Prompt ใช้คำสั่ง ipconfig ดูที่ IPv4 Address

myName เป็นการแยกไฟล์เก็บข้อมูลของเครื่อง Android แต่ละเครื่อง ถ้ามี 2 เครื่องขึ้นไปให้ตั้ง myName ที่ไม่ซ้ำกัน 

String myName = "Supot"; // ชื่อของเครื่อง Android ตั้งไม่ให้ซ้ำกัน
String serverMessage = "";
String serverIP = "192.168.1.14";  // IP Address ของ Server อันนี้เป็นของเครื่องผม

void mousePressed(){
  String url = "http://" + serverIP + "/xampp/location.php?update=" + myName +
    "&location="+latitude+","+longitude+","+altitude;
    
  // Update ข้อมูลตำแหน่งของเครื่องไปที่ Server ชื่อเครื่องระบุที่ myName
  
  loadStrings(url);  

  url = "http://" + serverIP + "/xampp/location.php?get=" + myName;
  
  // get ข้อมูลตำแหน่ง ระบุชื่อเครื่องที่ myName
  
  String results[] = loadStrings(url);
  if(results.length > 0)
    serverMessage = results[0];
    
}



และแก้ไขในส่วนขอ void draw() ตามนี้

void draw() {
  background(128);
  if(location.getProvider() == "none")
    text("Location data is unavailable. \n" +
      "Please check your location settings.", width/2,height/2);
  else
    text("Latitude: " + latitude + "\n" +
      "Longitude: " + longitude + "\n" +
      "Altitude: " + altitude + "\n" +
      "Accuracy: " + accuracy + "\n" +
      "Provider: " + location.getProvider() + "\n" +
      "Last Servver Message: " + serverMessage, width/2, height/2);
}


การเก็บข้อมูลจะทำการเขียนเพิ่มลงไปในไฟล์ สมมุติว่ามีการเคลื่อนที่ของเครื่อง เมื่อกดหน้าจอข้อมูลก็จะเอาไปบันทึกต่อท้ายในไฟล์ที่ Server ถ้าสามารถโหลดไฟล์มาได้ ก็จะดูได้ว่าตัวเครื่องเคลื่อนที่ไปที่ไหนบ้าง ถ้าจะให้ดีก็ทำการกำหนดเวลาให้บันทึกตำแหน่ง อาจจะทุกๆ 5 นาที ส่วนนี้ทิ้งไว้ให้คิดต่อกันนะครับ

ไฟล์ Location.php ต้องแก้ไขเล็กน้อย เพื่อให้สามารถเขียนเพิ่มได้ บรรทัดที่ 19

จาก
 $fh =fopen($_GET['update'].".txt", "w");

เป็น
 $fh =fopen($_GET['update'].".txt", "a");

(w = write, a = append)

ส่วนการอ่านข้อมูลจากไฟลที่บันทึกให้พิมพ์ที่ Browser 


เปลี่ยน Supot เป็นชื่อที่ระบุใน myName จากนั้น Server จะตอบกลับมาเป็นตำแหน่งที่บันทึกไว้ โดยใช้ช่องว่างแยกข้อมูลออกจากกัน

13.8921433,100.7591537,0.0 13.8921433,100.7591537,0.0 13.8921433,100.7591537,0.0 13.8921433,100.7591537,0.0

ในส่วนของการนำเอาค่าต่างๆ ที่รับมาจาก Arduino มาเพิ่มเติมเพื่อบันทึกนั้น สามารถประยุกต์จากตัวอย่างที่ผ่านมา ซึ่งผมจะนำมาเพิ่มเติมให้ภายหลัง แต่ผมคิดว่าผู้อ่านสามารถเพิ่มเติมได้เอง

ในส่วนของ Server เอง เราจะเข้าถึงจากภายนอกไม่ได้ ถ้ามีความรู้เรื่อง DDNS (Dynamic Domain Name Server) ก็สามารถทำการปรับตั้งเพื่อให้สามารถบันทึกข้อมูลจากภายนอกได้ ของผมใช้ของ no-ip ถ้าจะลองก็ให้ตั้ง serverIP เป็น supotsaeea.no-ip.info:81 ก็จะมาบันทึกข้อมูลไว้ที่เครื่องผมได้ ลองดูนะครับ ได้ไม่ได้ยังไงก็ส่งความคิดเห็นเข้ามาได้ครับ

------------------------------------------
สำหรับใครที่ทดลอง XAMPP แล้วไม่ผ่าน ให้เปิด Control Panel ของ XAMPP อยู่ใน c:\xampp ชื่อไฟล์ xampp-control.exe เปิดขึ้นมาจะได้หน้าต่างนี้


กดปุ่ม Start ที่แถว Apache


ตัวหนังสือ Apache จะคลุมเป็นสีเขียว ถ้าเป็นสีอื่นต้องแก้ไขหมายเลขพอร์ท สำหรับบางเครื่องที่ลง IIS ของ windows ซึ่งใช้พอร์ท 80 แล้ว ให้หลีกเลี่ยงไปใช้พอร์ท 81 แทน ให้กดที่ Config เลือกเมนูแรก Apache (httpd-conf)  

ไฟล์จะเปิดขึ้นด้วย Notepad ให้ไปที่บรรทัดที่ 47 หรือบรรทัดที่เขียนว่า Listen 80 ให้เปลี่ยนเป็น Listen 81 แล้วบันทึก ถ้าไม่สามารถบันทึกได้ให้ดูว่าไฟล์อยู่ที่โฟลเดอร์ไหน ปิดไฟล์จากนั้นเปิด Notepad แบบ Administrator เรียกไฟล์ทำการแก้ไขแล้วบันทึก

ทดลองกด Start อีกครังที่ Apache อาจต้องลองเปลี่ยนพอร์ทเป็น 8080 หรือ 8081 ซึ่งก็สามารถใช้งานได้เช่นกัน ถ้าไม่ได้จริงๆ ลองยังไงก็ไม่ได้ ก็หลังไมค์ได้ครับ

ถ้าใครทำอาชีพขนส่งแล้วอยากรู้ว่ารถของเราอยู่ที่ไหนก็ติดตั้งระบบนี้เข้าไป แต่อยากให้ใช้ GPS จะแม่นยำกว่าครับ เปิดเครื่องที่บริษัทเป็น Server ไม่ต้องไปเช่าใคร ปัจจุบันค่าเนตรายเดือนก็ไม่แพงแล้ว ส่งข้อมูลทั้งวันทั้งคืนก็จ่ายเท่าเดิม





ตอนที่ 10 : แสดงค่าโวลต์แบบหน้าปัทม์และตัวเลขกราฟฟิก

ดูคลิปนี้

http://www.youtube.com/watch?v=NbM7Ni2_eEQ&list=UUyt09UeQcMrj8QUSr5BEmaQ

จะทำการอ่านค่าจากพอร์ท Analog ของ Arduino มาแสดงบนหน้าจอ Android


ตอนนี้จะเรียนรู้การนำภาพมาแสดงบนจอ การวาดเส้นขยับตามค่าโวลต์ที่อ่านจากพอร์ท แสดงตัวเลขแบบกราฟฟิก

ไฟลต่างๆที่ต้องใช้ โหลดมาครับ
1. ไฟล์หน้าปัทม์ https://www.dropbox.com/s/q5ouvwpmr7188sr/voltmeter.JPG


2. ไฟล์ตัวเลขกราฟฟิก https://www.dropbox.com/s/0er5h40u5zoiwo3/flipnumber.png


การนำตัวเลขมาใช้งาน
ทำได้ด้วยการเจาะเอาเฉพาะภาพในส่วนที่ตัวเลขนั้นอยู่ ที่เราคุ้นเคยก็คือการ Crop นั่นเอง วิธีการนี้ใช้ทั่วไปกับการทำเกมส์ โดยการวาดท่าทางต่างๆของตัวแสดง เช่นรูปนกบิน โดยวาดการขยับปีกขึ้นลงในแต่ละช่อง จากนั้นก็ให้โปรแกรมนำช่องที่ต้องการมาแสดง แต่ของเราก็จะเอาค่าที่อ่านได้ของหลักใดหลักหนึ่งมาบอกว่าจะเจาะเอาช่องไหนมาแสดง

ในรูปตัวเลขที่ให้มาจะมีระยะห่างระหว่างตัวเลขเท่ากับ 128 ดังนั้นถ้าต้องการตัวเลข 4 ก็จะไป crop ที่ X = (128 * (4 - 1)) = 384, Y = 0; ความกว้าง = 128, ความสูง = 174 ส่วนของการนำภาพที่ crop ไปใช้ก็ให้คำนวณค่าก่อน หาตัวเลขที่อยู่ในแต่ละหลัก แล้วก็มาหาตำแหน่งที่ต้องการดึงมาใช้ 

สร้าง Processing Sketch ใหม่ ตั้งขื่อตามใจชอบ บันทึก จากนั้นลากไฟล์รูปภาพทั้งสองวางลงในพื้นที่ของ Editor ตรงไหนก็ได้ครับ โปรแกรมจะทำการสร้างโฟลเดอร์ชื่อ data เพื่อเก็บไฟล์ทั้งสอง ลองเปิดดูในโฟลเดอร์ของ sketch  ดูครับ ทีนี้ถ้าต้องการใช้ไฟล์อะไร ก็ลากไปวางตรงพื้นที่ที่ใช้พิมพ์โค๊ดนั่นแหละ มันก็จะเอาไปเก็บให้เราเอง

ทีนี้เอาไฟล์ AndroidManifest.xml และโฟลเดอร์ res (ย้อนกลับไปดูตอนที่ 7 AndroidSerial Library) ยกมาวางในโฟลเดอร์ของ Sketch อันนี้ลากมาวางในพื้นที่โค๊ดไม่ได้นะครับ เพราะมันจะเอาไปใส่ในโฟลเดอร์ data โปรแกรมจะมองไม่เห็น ให้วางอยู่ในระนายเดียวกับตัว sketch

Arduino โค๊ด

void setup(){
  pinMode(11,OUTPUT);
  digitalWrite(11,LOW);
  Serial.begin(9600);
}
char c;
String str = "";

boolean active = false;

void loop(){
  //delay(200);
  if(Serial.available()){
    while(Serial.available() > 0){
      c = Serial.read();
      if(c == '\n'){
        if(str[0] == 'B')
        {
          digitalWrite(11, !digitalRead(11));
          active = true;
        }
        if(str[0] == 'S')
          active = false;
       
        str = "";
      }else{
        str += c;
      }
    }
  }
  else
  {
    if(active)
    {
      Serial.println(map(analogRead(A0),0,1023,0,255));
      active = false;
    }
  }
}

ตัวโค๊ดจะอ่านตัวอักษรมาตรวจสอบว่ามีตัว newline (\n) หรือไม่ ถ้ามีก็ให้ดูว่าตัวคำสั่งคืออะไร ถ้าเป็นตัวอักษร B ก็จะสั่งให้ LED ติด-ดับ และเปิดให้ส่งข้อมูลของ A0 กลับไป สังเกตุว่าเมื่อส่งค่าคืนไปแล้วก็จะปิดการส่ง ด้วยคำสั่ง active = false; การทำแบบนี้ก็เพื่อไม่ให้ส่งข้อมูลกลับมากเกินไป ซึ่งจะทำให้ตัวรับต้องมาตอบสนองข้อมูลจนไม่สามารถไปทำงานอื่นได้ เมื่อส่งข้อมูลกลับแล้วก็จะมารอรับคำสั่งต่อไป วิธีการนี้สามารถประยุกต์ไปใช้ได้กับงานอื่นๆได้นะครับ

โค๊ดสำหรับ Processing

import com.yourinventit.processing.android.serial.*;
import java.net.*;

PImage pimage;
PImage nimage;

Serial port;
String voltRead="0"; // เก็บค่าโวลต์

void setup(){
  size(400,342);
  originX = 205;
  originY = 245;
  frameRate(10); // กำหนดความเร็วในการวาดภาพ ตอนนี้เป็น 10 ครั้งต่อวินาที
  pimage = loadImage("voltmeter.JPG"); // โหลดรูปหน้าปัทม์
  nimage = loadImage("flipnumber.png"); // โหลดรูปตัวเลข

  println(Serial.list(this));
  port = new Serial(this,Serial.list(this)[0],9600);
  port.clear();
  port.bufferUntil('\n');

  port.write("B\n"); // ส่งคำสั่งไปที่ Arduino สังเกตุว่าต้องมี \n ตามไปด้วย
}

int originX, originY;
float measureVolt;
long lastMillis = millis();
int digit = 0;
int count = 0;
boolean portReady = true;

void draw(){
  try{ // ใส่คำสั่ง try ไว้เผื่อว่าเกิดข้อผิดพลาดใดๆ ตอนทำงาน โปรแกรมจะได้ไม่หยุดทำงาน
    background(0);
    image(pimage,0,0); // วางรูปหน้าปัทม์
    needle(originX, originY, measureVolt); // วาดรูปเข็มตามค่าที่รับมาจาก  Arduno
 
   //
   // ส่วนนี้เป็นการ Crop ตัวเลขมาจากรูปตัวเลข
   //
    int c = int(measureVolt) % 1000;  // ดึงค่าตัวเลขที่น้อยกว่า 1000 มาใช้งาน

   // ส่วนนี้เป็นหลักร้อย โดยที่ c/100 จะดึงตัวเลขที่หลักร้อยมาใช้งาน เพื่อหาว่าจะนำช่องไหนของรูปตัวเลขมาแสดง

  // นี้คือตำแหน่งที่ของตัวเลข
    int iStart = ((c / 100) * 128);
 
  // คำสั่ง copy จะ crop รูปตัวเลข nimage ที่ตำแหน่ง x = iStart, y = 0, กว้าง 128 สูง 174 จากนั้นเอาไป
  //วาดไว้ที่ตำแหน่ง x = 152, y = 250 ย่อขนาดกว้างลงมาเป็น 32 และสูง 45

    copy(nimage,iStart,0,128,174,152,250,32,45);

   // หลักสิบ
    c = c % 100;
    int iStart1 = (( c / 10) * 128);
    copy(nimage,iStart1,0,128,174,184,250,32,45);
 
   // หลักหน่วย
    c = c % 10;
    int iStart2 = (c * 128);
    copy(nimage,iStart2,0,128,174,216,250,32,45);
 
   // ส่วนนี้ให้รอ 300 ms แล้วค่อยส่งคำสั่งไปอีกครั้ง
    if((millis() - lastMillis) > 300){
      lastMillis = millis();
      port.write("B\n");
    }
  }
  catch(Exception e){
   // ส่วนนี้เพื่อให้รู้ว่าตอนทำงานพบปัญหา เพื่อให้สามารถ debug เพื่อแก้ไข
    println("Something happen: " + (++count));
    lastMillis = millis();
  }
}

 //
 // ส่วนนี้จะทำการวาดเข็มให้เคลื่อนไปในตำแหน่งที่ได้รับมาจาก Arduino
 //
void needle(int originX, int originY, float measureVolt){
  int minToMaxAngle = 90;  // มุมที่วัดจากตำแหน่งค่าเท่ากับ 0 ไปถึงค่าสูงสุดในที่นี้คอที่ตำแหน่ง 50
  int maxVolt = 50;  // ค่าโวลต์สูงสุด
  int startAngle = 45;   //มุมที่วัดจากตำแหน่ง 0 องศา (ซ้ายมือ) มาถึงตำแหน่ง 50 โวลต์

  // มุมที่เข็มเคลื่อนที่
  float angle = (PI/180) * ((minToMaxAngle * (maxVolt - measureVolt) / maxVolt) + startAngle);

  // ความยาวของเข็ม
  float needleLength = 200;

  strokeWeight(2); // ขนาดความหนาของเข็ม
  stroke(255,0,0); // สีของเข็ม (สีแดง)

  // วาดเส้น
  line(originX, originY, originX+cos(angle)*needleLength, originY-sin(angle)*needleLength);

  fill(0);
  noStroke();

  // วาดรูปวงกลมปิดเข็มในส่วนที่ไม่ต้องการ
  ellipse(originX,originY,100,100);
}

// ส่วนการรับข้อมูลและแปลงค่าที่อ่านได้เป็นขนาดโวลต์

void serialEvent(Serial port){
  // อ่านค่าที่ส่งมาจาก Arduino เมื่อพบตัว \n ให้เก็บค่าไว้ที่ voltRead
  voltRead = port.readStringUntil('\n');
  port.clear();
  voltRead = trim(voltRead);

  // นำ voltRead มาแปลงค่าเป็น measureVolt เพื่อใช้ในการวาดเข็มและแสดงตัวเลข
  measureVolt = float(voltRead) * 50 / 255;
}

บทความตอนนี้ จะเน้นในการนำค่าที่ได้มาแสดงให้เกิดความน่าสนใจ โดยที่อาจทำการสร้างหน้าปัทม์ขึ้นเองด้วยโปรแกรม Graphic Editor ต่างๆ เช่น Photoshop, Paint แล้วก็บันทึกเป็นไฟล์ jpg, gif หรือ png นำมาวางเป็นพิ้นหลัง จากนั้นก็สร้างการเคลื่อนที่ของเข็ม การแสดงตัวเลข การติดดับของหลอดไฟ แบบที่แสดงในห้องควบคุมในเครื่องบิน

หวังว่าจะทำให้เกิดไอเดีย ไปทำงานอื่นๆนะครับ

ส่วนของการเขียนบทความ อาจไม่สม่ำเสมอ ยังไงก็ follow ได้นะครับ