2014年7月30日 星期三

AMIGO Camera(三)

《AMIGO Camera》是一台由紅米 + Arduino + 伺服馬達組成,比 IP 攝像頭多一點功能的裝置。希望讓使用者在遠端經互聯網控制《AMIGO Camera》,進行移動、拍攝、縮時錄影等功能。早兩天大約設計過《AMIGO Camera》的外觀,今日花點時間編寫實驗程式,看看能否做到遠端控制。

我選擇在 Android 上加入 HTTP 服務器,這樣只要把域名或地址接通,便能在瀏覽器上輸入地址,透過互聯網直接連線。到時在家中的路由器按埠號把數據分發到紅米就行了。利用瀏覽器的好處是不同平台都有,不用安裝特別程式。

我不懂 Android 編程,所以這次試驗遇到不同問題。首先是 NanoHTTPD 有很多版本,網上的教學也有很多版本,通通都不適用最新的版本。public Response serve(IHTTPSession session) 就是新版本的格式。既然不能照抄,那只好參考舊版的編寫方式,加上自己的思考去做。好不容易改好了,畫面也出現了,瀏覽器卻連不上。找了很久,原來是舊版不用 server.start(),新版要。好了,成功連上了,但想連線是在畫面顯示當刻時間,發現那時要更新介面,不如 iOS 般簡單、直接、容易。又找了一會,成功以 Handler 來達成。今回學到不少。下一步就是加入 OTG + Arduino 連線。這樣基本的硬件功能便已齊全。
//========================================================================================
//  AMIGO Camera
//----------------------------------------------------------------------------------------
//  Created by Pacess on 2014-07-30
//  Copyright (c) 2014 Sita Technology Company.  All rights reserved.
//========================================================================================

//----------------------------------------------------------------------------------------
//  0    1         2         3         4         5         6         7         8         9
//  56789012345678901234567890123456789012345678901234567890123456789012345678901234567890

package com.pacess.amigocamera;

import android.widget.TextView;
import android.app.Activity;
import android.app.Fragment;
import android.os.Message;
import android.os.Handler;
import android.os.Bundle;
import android.net.wifi.WifiManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.view.MenuItem;
import android.view.Menu;
import android.view.View;

import java.text.SimpleDateFormat;
import java.io.IOException;
import java.lang.Runnable;
import java.util.Date;

//========================================================================================
public class MainActivity extends Activity  {

 //----------------------------------------------------------------------------------------
 //  Defines
 private static final int PORT = 8080;

 //----------------------------------------------------------------------------------------
 //  Class variables
 private TextView statusTextView;
 private HTTPServer httpServer;

 //----------------------------------------------------------------------------------------
 Handler handler = new Handler()  {
  @Override public void handleMessage(Message message)  {
   if (statusTextView == null)  {return;}

   String timeStamp = (String)message.obj;

   StringBuilder stringBuilder = new StringBuilder();
   stringBuilder.append("Last request: ");
   stringBuilder.append(timeStamp);

   statusTextView.setText(stringBuilder.toString());
  }
 };

 //----------------------------------------------------------------------------------------
 @Override protected void onCreate(Bundle savedInstanceState)  {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  Log.d("MainActivity", "onCreate");

  if (savedInstanceState != null)  {return;}
  getFragmentManager().beginTransaction().add(R.id.container, new PlaceholderFragment()).commit();
 }

 //----------------------------------------------------------------------------------------
 @Override protected void onResume()  {
  super.onResume();
  Log.d("MainActivity", "onResume");

  SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  String timeString = dateFormat.format(new Date());
  statusTextView = (TextView)findViewById(R.id.status);
  if (statusTextView == null)  {
   Log.d("MainActivity", "### statusTextView is null...");
  }  else  {
   statusTextView.setText("Time stamp: "+timeString);
  }

  WifiManager wifiManager = (WifiManager)getSystemService(WIFI_SERVICE);
  int ipAddress = wifiManager.getConnectionInfo().getIpAddress();
  final String ipString = String.format("%d.%d.%d.%d", (ipAddress & 0xff), (ipAddress >> 8 & 0xff), (ipAddress >> 16 & 0xff), (ipAddress >> 24 & 0xff));

  TextView textView = (TextView)findViewById(R.id.ipAddress);
  textView.setText("http://"+ipString+":"+PORT);

  try  {
   httpServer = new HTTPServer();
   httpServer.start();
   Log.d("MainActivity", "HTTP server started...");

  }  catch (IOException e)  {
   e.printStackTrace();
  }
 }

 //----------------------------------------------------------------------------------------
 @Override protected void onPause()  {
  super.onPause();
  Log.d("MainActivity", "onPause");

  if (httpServer == null)  {return;}

  httpServer.stop();
  Log.d("MainActivity", "HTTP server stopped");
 }

 //----------------------------------------------------------------------------------------
 @Override public boolean onCreateOptionsMenu(Menu menu)  {
  //  Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }

 //----------------------------------------------------------------------------------------
 @Override public boolean onOptionsItemSelected(MenuItem item)  {
  //  Handle action bar item clicks here. The action bar will automatically handle clicks
  //  on the Home/Up button, so long as you specify a parent activity in AndroidManifest.xml.
  int id = item.getItemId();
  if (id == R.id.action_settings)  {return true;}
  return super.onOptionsItemSelected(item);
 }

 //========================================================================================
 //  A placeholder fragment containing a simple view.
 public static class PlaceholderFragment extends Fragment  {

  //----------------------------------------------------------------------------------------
  public PlaceholderFragment()  {
  }

  //----------------------------------------------------------------------------------------
  @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)  {
   View rootView = inflater.inflate(R.layout.fragment_main, container, false);
   return rootView;
  }
 }

 //========================================================================================
 public class HTTPServer extends NanoHTTPD  {

  //----------------------------------------------------------------------------------------
  public HTTPServer() throws IOException  {
   super(PORT);
   Log.d("HTTPServer", "Server start "+PORT);
  }

  //----------------------------------------------------------------------------------------
  @Override public Response serve(IHTTPSession session)  {
   Log.d("HTTPServer", "Server response");

   SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
   final String timeString = dateFormat.format(new Date());

   //----------------------------------------------------------------------------------------
   //  Use handler to update user interface
   Thread thread = new Thread(new Runnable()  {
    @Override public void run()  {
     Message message = handler.obtainMessage();
     message.obj = timeString;
     message.sendToTarget();
    }
   });
   thread.start();

   //----------------------------------------------------------------------------------------
   //  Create response HTML
   StringBuilder stringBuilder = new StringBuilder();
   stringBuilder.append("<html>");
   stringBuilder.append("<body>");

   stringBuilder.append("<b>AMIGO Camera - ").append(timeString).append("</b><hr>");
   stringBuilder.append("<b>Headers</b> = ").append(String.valueOf(session.getHeaders())).append("<br>");
   stringBuilder.append("<b>Method</b> = ").append(String.valueOf(session.getMethod())).append("<br>");
   stringBuilder.append("<b>URI</b> = ").append(String.valueOf(session.getUri())).append("<br>");
   stringBuilder.append("<b>Parameters</b> = ").append(String.valueOf(session.getParms())).append("<br>");

   stringBuilder.append("</body>");
   stringBuilder.append("</html>");
   return new NanoHTTPD.Response(stringBuilder.toString());
  }
 }
}

2014年7月29日 星期二

AMIGO Camera(二)


第一個版本的《AMIGO Camera》的設計概念是「簡約」,似乎做得不夠到題,看起來變成「簡陃」;而且感覺很粗大,欠一點美感。只好再畫一個。第二版比之前更「簡陃」,但體積縮減了不少。槓桿情況不算嚴重,S3003 應該夠力運行。不過,這兩個版本都未合心意,得再想想有沒有更好的設計。

2014年7月28日 星期一

AMIGO Camera


上星期被 Jibo 的影片吸引住。它的設計十分優美可愛,很想訂購一台回來。但剛剛向香港 3D Printer 教父 Chris 購入一台 ArrayZ,要拿出 US$499 還是有點壓力。而且最快要等到 2015 年 12 月付運,實在很遙遠。加上 MakiBox 很差的經驗,還是不要重蹈覆轍。

想購買 Jibo 是因為它能拍照,而且能多角度移動。當然,一台 IP Cam 也能做到,但我希望能有擴展能力,甚至編程操作。既然沒錢買,那就造一台。手上的紅米基本上是廢物一部,要是加上伺服馬達及 Arduino mini,以 3D Printer 打印外殻,應該能造出我想要的產品。我希望最終成品能在他方用瀏覽器經互聯網控制及進行拍攝。硬照及 Timelapse 是必須的選項。使用紅米是因為 HK$999,比起 iPhone 或 iPod 便宜。最重要一點是 Android 在點對點連線方面來得簡單。大概內容都似乎能達成得到,那麼下一步就得想想機械人的外觀。利用 Inventor 初步畫了外型出來,內部還是實心,部件的高度是隨意,日後需要跟據伺服馬達的尺寸作出調整。家裡還有很多 Futaba S3003,它們將會是伺服馬達的選擇。現在部件的角度是 10 度,最大的斜度似乎過過,加上紅米的重量,相信機械人會倒下來,設計要再作修改。

2014年7月24日 星期四

創客空間 @ RTHK31


之前到馬頭圍做的錄音訪問《創客空間》第二集在今晚九時播出。有 Keith、Peter 及我講關於《Tri-Robot》的心得。有興趣的朋友可到 http://programme.rthk.hk/channel/radio/programme.php?name=dab31/g0177_maker_club&p=6343 重溫節目廣播。

2014年7月22日 星期二

漫遊台北 2014


我在 U1 工作時曾跟老闆出差到過台北。當時的落差很大,感覺比中國大陸落後及沉悶。妻兒們沒到過台灣,藉著暑假陪她們走一趟台北。是次之行,一改以往的印象。


今次的行程從宜蘭開始,到菁桐、十分、九分、鼻頭,再回到台北的貓空、中正紀念堂、西門、Baby Boss、101、信義誠品、Hello Kitty Cafe。前半部份的地方是第一次去,滿足到我拍照的興趣;後半部份的地方也只去過西門及 101,行程也蠻新鮮。在五天的行程中,我覺得台灣人十分熱情好客,也感受到一份人情味;在飯店報到時,台灣人會提出行程建議;在街上找路時,台灣人會主動地幫忙;在付費時,台灣人會完成工作後才收錢。最有趣的是在信義誠品找書遇到的一位女仕。她問我為何細女能乖乖地坐著娃娃車,並讓我能慢慢地找書;到問我為何選大前研一的作品;甚至問我大前研一是如何練成的。她問問題的角度我沒有想過,也令我有一點啟發。為迷茫於事業的我帶來一點鼓勵。看來她是上天派給我的天使。本來想問問她的名字,最後還沒有問,留待日後有緣相聚。

台灣的消費不像其他地方高,某些景點的費用更覺得便宜。原本不打算再到台灣的我,變成繼日本之後希望再去的地方。

2014年7月11日 星期五

Sita @ App Stars World


網友跟我說 iPad 版的《App Stars World》曾有過 Sita 陳僖儀的訪問,但他註冊不了,問我有沒有辦法。於是我也下載這個 App 了解了解。原來它的服務器出了問題。碰巧早幾天面試時,一位應徵者在哪家公司工作過,並告訴我公司已賣盤,而 App 業務也關閉了。可能是這個原因,服務器出事也沒有人修理。


研究了一下,找到了有關的報導檔案。內容是圖跟字分開,正合 Sita Fans 相去字化保留圖片的意願。有興趣的朋友可到 http://www.appstarsworld.com/appstarsworld/split/music/music2012050201.zip 下載。

2014年7月7日 星期一

澳門 RBL 2014


昨天是《RBL Macau》舉行的日子。《Tri-Robot》小組一行三人走到澳門觀摩一下。我們乘坐早上 8:00 的船到澳門。到達會場時,已經有很多學生認真地為機體作最後修改及設定。機械人格鬥比賽影碟已經看過幾屆。現場的則是第一次。起初的比賽節奏較慢,裁判的指令也不夠熟練;不過到比賽八強淘汰賽時,已有明顯的進步。而我有份開發的《Tri-Robot》亦出現了一點狀況。太多同類型機體在啟動時,導致藍牙名稱重複,需要跟大家合作才能接對機體;動作也得要更方便編輯才行;速度亦有待改善。汲收了今次的經驗,希望香港 RBL 能做得更好。


RBL Macau 第一場比賽


RBL Macau 2014: Tri-Robot vs Tri-Robot


RBL Macau 2014: 總決賽上半場


RBL Macau 2014: 總決賽下半場

更多的相片可到 https://www.facebook.com/media/set/?set=a.278346562348718.1073741847.145737068943002&type=1 觀看。香港 RBL 將於 10 月 19 日假香港理工大學內舉行,免費入場參觀。

2014年7月5日 星期六

Hong Kong Mini Maker Faire 2014 & RBL Hong Kong 2014


一年一度的《Hong Kong Mini Maker Faire》已經是第三屆舉辦。將於 10 月 18 及 19 日在理工大學舉行。現正接受 Maker 報名展出作品。今年是首屆為期兩天展出,亦是首屆加入《Robot Boxing League Hong Kong》機械人格鬥大賽。《Tri Robot》小組將會參加比賽,希望有一番激烈的戰鬥。

2014年7月1日 星期二

繞過 HTTPS 防禦機制


我替客人開發的應用程式,如有重要資料向服務器存取時,會利用 SSL 來進行加密。這樣數據即使被攔截,內容也因為被加密而無法看到。不過,最近發現到一個方法能突破這個防禦機制。

在研究別人的應用時,Charles 抓下來的數據因為利用 HTTPS 而只能看到一大堆亂碼。由於看不到內容,就無法進行修改。但無意中發現,有些服務器的 HTTP 及 HTTPS 會設定到相同地址。舉個例子。https://www.pacess.com/api/getContentList.php 及 http://www.pacess.com/api/getContentList.php 都指著同一個目錄同一個檔案。既然是相同的地方,亦即是說兩者的分別只在於前者有 SSL 加密,後者則沒有。那麼要觀看內容,甚至是作出修改的話,只要把 HTTPS 改為 HTTP 就行。

功能強大的 Charles 有一個「Map Remote Settings」功能可讓我們達到以上目的。實在利害!所以,要避免以上情況發生,就要把 HTTPS 及 HTTP 設定在不同的目錄。