2014年10月1日 星期三

Raspberry Pi 樹莓派 無線AP to 有線網路橋接

筆者最近在研究Wi-Fi時發現Pi也可以模擬成AP,因此筆者用閒暇時間整理出如何將Pi變成一台無線基地台

簡介

首先先說明一下筆者所處的網路架構(圖1),圖片從右到左的介紹如下
  i. 首先是modem: 俗稱小烏龜的玩意兒,常見的就是ISP(中華電信, 寬頻業者)給的機器
  ii. 無線基地台(AP): 手機或筆記型電腦能透過Wi-Fi連接上AP後就能上網,很重要的一點是預設要開啟DHCP功能
  iii. 平板、手機、筆電: 透過Wi-Fi上網的機器
  iv. PC: 透過有線網路上網的機器

圖1. 典型網路架構

圖2為筆者目標的網路架構,說明如下
  目標: 
    i. 將有線網路延伸出去,像是AP架在客廳可是房間訊號很差,但卻有有線網路能接的情況
    ii. 另一種玩法就是做封包分析和網路攻擊的時候中間多個橋接器方便很多的
    iii. 由於是橋接的方式,在Pi無線網域下的所有裝置與有線網域下的所有裝置都還是在同一個網段,簡單的說就是網芳還是通的~
  架構:
    將有線網路接到Pi上,再由Pi發出Wi-Fi的訊號給手機、平板等裝置連線


網卡晶片

  筆者使用RTL8188CUS配合Realtek的軟體後可正常使用,但RTL8188SU和RTL8192SU試了好幾次都跑不起來.......

架設前注意事項

  以下的操作建議在Pi上接著螢幕和鍵盤執行,橋接完後AP分給Pi的IP可能會改變,此外有線網卡(eth0)基本上不會再取得IP,即便取得也不穩定,因此完成所有步驟前請直接在Pi上操作

架設步驟

  1. 安裝bridge工具

pi@ ~ $ sudo apt-get install bridge-utils

  2. 檢查網卡晶片組

  若跟筆者一樣是Realtek網卡晶片組請先跳到下方的"修正Realtek網卡在Pi上無法使用hostapd問題"章節安裝Realtek的hostapd,完成後再回到第4步
pi@ ~ $ sudo lsusb

  3. 安裝官方的hostapd

  (Realtek網卡晶片的使用者請勿使用Pi官方版本的hostapd)
pi@ ~ $ sudo apt-get install hostapd

  4. 檢查網卡代號

  此處筆者的有線網卡為eth0,無線網卡為wlan0
pi@ ~ $ ifconfig

  5. 修改無線網卡設定使其eth0和wlan0橋接起來

pi@ ~ $ sudo vi /etc/network/interfaces
auto lo

iface lo inet loopback
#iface eth0 inet dhcp

#allow-hotplug wlan0
#iface wlan0 inet manual
#wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
#iface default inet dhcp

auto br0
iface br0 inet dhcp
bridge_ports eth0 wlan0 

  6. 建立hostapd的設定檔(預設可能不會有此設定檔)

pi@ ~ $ sudo vi /etc/hostapd/hostapd.conf
interface=wlan0
driver=rtl871xdrv
bridge=br0
ssid=RPI
channel=6
hw_mode=g
ieee80211n=1
wmm_enabled=1
wpa=2
wpa_passphrase=Raspberry
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP
auth_algs=1
macaddr_acl=0 

  7. 重開機

pi@ ~ $ sudo reboot

  8. 測試hostapd是否能正常執行

pi@ ~ $ sudo hostapd -dd /etc/hostapd/hostapd.conf

  9. 編輯hostapd的設定檔

  使其在啟動daemon時載入/etc/hostapd/hostapd.conf的設定
pi@ ~ $ sudo vi /etc/default/hostapd
DAEMON_CONF="/etc/hostapd/hostapd.conf"

  10. 啟動hostapd,接著就能RPi當做AP來使用了~

pi@ ~ $ sudo service hostapd restart

修正Realtek網卡在Pi上無法使用hostapd問題

  1. 安裝Pi官方提供的hostapd,目標只是取得hostapd的daemon而已XD
pi@ ~ $ sudo apt-get install hostapd
  2. 刪除hostapd(不要懷疑,上一步就只是為了daemon檔案而已)
pi@ ~ $ sudo apt-get remove hostapd
  3. 到Realtek的下載頁面下載RTL8188CUS的工具包(在下載頁面搜尋RTL8188CUS即可找到)
    PS: 由於Realtek的下載頁是用js觸發下載事件,為節省麻煩建議直接用其他電腦的瀏覽器下載再使用scp指令(Windows請用WinSCP or FileZilla的sftp)複製到pi

  4. 將RTL8188CUS的工具包從Mac, Linux or Windows傳送到pi的家目錄中(Windows請使用WinSCP或FileZilla)
Shiun@ShiundeMacBook-Pro ~ scp RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911.zip pi@192.168.0.148:/home/pi
  5. 將RTL8188工具包解壓縮
pi@ ~ $ unzip RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911.zip
  6. 移動到hostapd的目錄
pi@ ~ $ cd RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911/wpa_supplicant_hostapd/
  7. 解壓縮wpa_supplicant_hostapd-0.8_rtw_r7475.20130812.tar.gz
pi@ ~/RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911/wpa_supplicant_hostapd $ tar -zxvf wpa_supplicant_hostapd-0.8_rtw_r7475.20130812.tar.gz
  8. 進入hostpad的目錄
pi@ ~/RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911/wpa_supplicant_hostapd $ cd wpa_supplicant_hostapd-0.8_rtw_r7475.20130812/hostapd/
  9. 開始建置hostapd
pi@ ~/RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911/wpa_supplicant_hostapd/wpa_supplicant_hostapd-0.8_rtw_r7475.20130812/hostapd $ make clean; make
  10. 安裝hostapd到正確的位置
pi@ ~/RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911/wpa_supplicant_hostapd/wpa_supplicant_hostapd-0.8_rtw_r7475.20130812/hostapd $ sudo make install;
  11. 再複製hostapd的執行檔以方便daemon使用,for Realtek的hostapd到此安裝完成
pi@ ~/RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911/wpa_supplicant_hostapd/wpa_supplicant_hostapd-0.8_rtw_r7475.20130812/hostapd $ sudo cp /usr/local/bin/hostapd* /usr/sbin


這篇寫的好累Orz.......,讀者若有發現任何錯誤請盡量向筆者糾正,謝謝^^
參考資料
  1. Raspberry Pi 的應用 - Wifi 無線基地台

2014年8月17日 星期日

使用Eclipse Luna + Tomcat 7 + Jersey 2架設RESTful service

前言

此篇文章介紹如何使用Jersey架設RESTful web service,使用到的工具如下
  1. Eclipse Luna
  2. Tomcat 7
  3. Jersey 2.11

為避免對Maven或Gradle不熟悉的人感到疑惑,因此此篇文章只針對如何使用Jersey 2架設RESTful service,不使用Maven或Gradle進行管理

架設步驟

1. 開啟Eclipse

2. 建立新的專案File -> New -> Dynamic Web Project


3. 輸入專案名稱JerseySample並選擇Apache Tomcat v7.0作為Target runtime,記得要把Generate web.xml deployment descriptor打勾




4. 將jersey官網下載的jaxrs-ri-2.11.zip解壓縮,接著把所有的jar檔複製到jerseySample/WebContent/WEB-INF/lib下


5. 重新整理JerseySample專案確保lib都有正常載入



6. 修改web.xml讓Tomcat可以正確載入Jersey相關資源
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>JerseySample</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
  
  <servlet>
    <servlet-name>Jersey Web Application</servlet-name>
    <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>jersey.config.server.provider.packages</param-name>
      <param-value>org.sample.test</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Jersey Web Application</servlet-name>
    <url-pattern>/api/*</url-pattern>
  </servlet-mapping> 
  
</web-app>

  參數說明
    i. servlet-class
      Jersey 1的servlet-class為
        com.sun.jersey.spi.container.servlet.ServletContainer

      而Jersey 2的servlet-class必須改成(畢竟Sun已經R.I.P.了QAQ........)
        org.glassfish.jersey.servlet.ServletContainer

    ii. jersey.config.server.provider.packages
      Jersey掃描的packet路徑,resource的java檔要放在此package下

    iii. url-pattern
      Resource對應的URL位置,client端必須要照此參數設定才能呼叫到對應的resource

7. 建立package: org.sample.test



8. 建立resource的java檔案: Hello.java



9. 加入以下的程式碼至Hello.java
package test;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello")
public class Hello {
    // This method is called if TEXT_PLAIN is request
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String sayPlainTextHello() {
        return "Hello Jersey";
    }

    // This method is called if XML is request
    @GET
    @Produces(MediaType.TEXT_XML)
    public String sayXMLHello() {
        return "" + " Hello Jersey" + "";
    }

    // This method is called if HTML is request
    @GET
    @Produces(MediaType.TEXT_HTML)
    public String sayHtmlHello() {
        return "<html> " + "<title>" + "Hello Jersey" + "</title>"
              + "<body><h1>" 
              + "Hello Jersey"
              + "</h1></body></html></pre>" 
              + " ";
 }
}

10. 將專案在Tomcat上跑起來,並使用瀏覽器檢查http://localhost:8080/JerseySample/api/hello是否正確




11. 使用RESTful client工具檢查xml的request是否能正常使用。只要送出的request包含Accept: text/xml檔頭,Jersey就會自動切換到回傳sayXMLHello函式結果


參考資料
1. REST with Java (JAX-RS) using Jersey - Tutorial (適用Jersey 1)
2. Jersey 2.5 + Maven = easy REST web services

[手工藝玩具] 1入2出 音源切換器

完成以下兩篇的玩具後,筆者的音樂播放器就從PC換成Pi
  Raspberry Pi 樹莓派 啟動I2S DAC or USB DAB 音效卡
  Raspberry Pi 樹莓派 架設ShairPort使用AirPlay播放音樂

但由於最近有不少狀況需要開PC,又PC要輸出聲音的話需要把喇叭從Pi換到PC的耳機座上,
根據以前悲劇的經驗,這樣不是喇叭的耳機頭先壞掉就是PC的耳機座先被插壞.........
因此筆者決定弄一個音源切換器,參考DIY音源切換器[DIY]兩種音源切換器(二入一出)後筆者自製了一個音源切換器

原料如下
  1. 6pin 2段式開關x1
  2. 3.5mm 耳機座x3 (二入一出)
  3. 塑膠盒子,大小為60x40x30mm
  4. 比6pin 2段式開關的pin粗一點點就熱縮套

以上原料大概只有80元可以搞定,接著就是開始對把線焊上2段式開關 -> 塑膠盒鑽4個孔 -> 鎖上耳機座元件 -> 鎖上2段式開關 -> 將耳機座和2段式開關焊上,接著就完成了(詳細內容請直接參考最底下的參考資料)

最後成品如下圖~


補上小小心得
  整天過著寫程式的生活難免會有枯燥乏味的時候,偶而來DIY些小東西來玩也挺有趣的,尤其是對筆者這種拿焊槍次數10根手指頭就能算完的新手,以玩家的角度來做這東西其實挺有趣的

  在焊接的部份以往都有強者學長支援,但這次在沒人支援的情況下在60x40x30mm的盒子裡焊接東西讓筆者苦戰整個下午才搞定,做完其實挺有成就感的

  而實際上這東西在網路上買的到,價格大概3xx到4xx(2014年8月份),不過根據筆者DIY整個東西的時間,其實3xx元已經非常划算了,如果不是以玩家的心情做的話建議還是花錢直接買現成的吧


參考資料
  1. DIY音源切換器
  2. [DIY]兩種音源切換器(二入一出)

2014年7月20日 星期日

Mac OSX 10.9 查詢listening ports

在Linux中可以簡單透過netstat -tulnp列出目前系統listening ports,但OSX 10.9上這招行不通,要改用以下指令才能查詢到目前系統正在listening的ports

Shiun@ShiundeMacBook-Pro ~ sudo lsof -i -P | grep -i "listen"

2014年7月17日 星期四

Mac OSX Apache 啟用/停止 開機啟動

由於筆者最近跑去玩RoR使用Pow作為web server,之前在MAC上開啟的Apache每次都會開機自動啟動,老實說要手動關後才能開Pow真的很煩,因此筆者上網找要怎麼關閉開機啟動Apache

停止開機啟動Apache

  1. 打開瀏覽器並輸入http://127.0.0.1先確認Apache是開啟狀態,若是關閉狀態請先執行以下指令開啟Apache
Shiun@ShiunMac ~ sudo apachectl start
  2. 打開終端機

  3. 輸入以下指令
Shiun@ShiunMac ~ sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist 此時Apache也會被一起關閉


啟用開機啟動Apache

  1. 確認Apache是關閉狀態,若是開啟狀態請先執行以下指令關閉Apache
Shiun@ShiunMac ~ sudo apachectl stop
  2. 打開終端機

  3. 輸入以下指令
Shiun@ShiunMac ~ sudo launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist 此時Apache會跟著一起被啟動

2014年7月14日 星期一

PHP 比對非0字串與整數0時卻回傳true問題

今天強者我朋友遇到PHP比對字串和0時卻發生回傳結果不如預期的問題,因為還挺有趣的所以就筆記下來

以下用簡單的switch case程式碼說明
<?php
$foo = 0;
switch ($foo)
{
    case "str1":
        echo "str1";
        break;
    case "str2":
        echo "str2";
        break;
    default:
        echo "default";
}
?>
Result
str1
由於直覺上0不可能等於"str1"應該要印出"default"才合理,為此筆者翻了一下PHP的文件Comparison Operators,其中有一句話這麼說

If you compare a number with a string or the comparison involves numerical strings, then each string is converted to a number and the comparison performed numerically. These rules also apply to the switch statement.

簡單翻譯:
   若用"=="比對的是數字和字串時,字串會被轉換為數字再進行比對,此規則也適用於switch-case

因此上述的switch-case程式碼實際比對的流程如下
  1. 由於比對的是整數和字串,因此準備開始轉換字串"str1"為整數
  2. 無法將字串由"str1"轉換為整數,因此PHP將字串"str1"轉換為0
  3. 由於"str1"轉換為0且$foo也為0,因此PHP判斷條件式case "str1"成立印出"str1"到網頁上

同樣的問題可以簡化為
<?php
echo (0 == "str1" ? "true" : "false") . "<br />";
var_dump((int)"str1");
?>
Result
true // 0等於"str1" int(0) // 整數型態的0
顯然地,要比對字串和數字時千萬要小心,不然就是用"==="來進行比對避免出問題
那若我們比對的是0和"1str"呢?
<?php
echo (0 == "1str" ? "true" : "false") . "<br />";
echo (1 == "1str" ? "true" : "false") . "<br />";
var_dump((int)"1str");
?>
Result
false // 0不等於"1str" true // 1等於"1str" int(1) // 整數型態的1 至於為啥會有這樣的結果呢?不要懷疑,就只是PHP的規則如此而已XD

參考資料
1. Comparison Operators

2014年6月19日 星期四

Android 使用Volley發送RESTful request

Android Volley建置一文中筆者建立了Volley的jar檔但卻沒有提說怎麼使用,接下來說明如何使用Volley發送RESTful request吧

流程說明

1. 建立Volley的RequestQueue: 使用者將request放到RequestQueue後Volley會自動將request以非同步方式發送出去

2. 建立JsonObjectRequest, JsonArrayRequest或StringRequest: 依照target的RESTful service規格決定要用JSONObject, JSONArray或String其中之一吧~
註: 筆者採用 使用CodeIgniter架設RESTful Web Service 一文中架設的RESTful web service當做練習對象,其index.php同時提供GET, POST, PUT和DELETE的範例

3. 將第2步中產生的request放到RequestQueue中

4. 等待server的回應並進行處理


實作

1. 建立Android application project



2. 加入volley.jar到專案中

  建置volley.jar可參考Android Volley建置

3. 建立RequestQueue

  此處採用Singleton pattern建立Singleton class,其名稱為ApplicationController,詳細程式碼可參考"參考資料1",以下的程式碼筆者針對Singlegon做簡單的說明
public class ApplicationController extends Application {

    /**
     * Log or request TAG
     */
    public static final String TAG = "VolleyPatterns";

    /**
     * Global request queue for Volley
     */
    private RequestQueue mRequestQueue;

    /**
     * A singleton instance of the application class for easy access in other places
     * Singleton物件,簡單的說就是整個app只會有這一個ApplicationController物件,理想上不會產生第二份ApplicationController物件
     */
    private static ApplicationController sInstance;

    @Override
    public void onCreate() {
        super.onCreate();

        // initialize the singleton
        sInstance = this;
    }

    /**
     * @return ApplicationController singleton instance
     * 要使用ApplicationController一定要先透過此method取得ApplicationController物件
     */
    public static synchronized ApplicationController getInstance() {
        return sInstance;
    }

    /**
     * @return The Volley Request queue, the queue will be created if it is null
     */
    public RequestQueue getRequestQueue() {
        // lazy initialize the request queue, the queue instance will be
        // created when it is accessed for the first time
        if (mRequestQueue == null) {
            mRequestQueue = Volley.newRequestQueue(getApplicationContext());
        }

        return mRequestQueue;
    }

    /**
     * Adds the specified request to the global queue, if tag is specified
     * then it is used else Default TAG is used.
     * 
     * @param req
     * @param tag
     */
    public  void addToRequestQueue(Request req, String tag) {
        // set the default tag if tag is empty
        req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);

        VolleyLog.d("Adding request to queue: %s", req.getUrl());

        getRequestQueue().add(req);
    }

    /**
     * Adds the specified request to the global queue using the Default TAG.
     * 
     * @param req
     * @param tag
     */
    public  void addToRequestQueue(Request req) {
        // set the default tag if tag is empty
        req.setTag(TAG);

        getRequestQueue().add(req);
    }

    /**
     * Cancels all pending requests by the specified TAG, it is important
     * to specify a TAG so that the pending/ongoing requests can be cancelled.
     * 
     * @param tag
     */
    public void cancelPendingRequests(Object tag) {
        if (mRequestQueue != null) {
            mRequestQueue.cancelAll(tag);
        }
    }
}

4. 修改AndroidManifest.xml

  加入網路存取權限以及開啟app自動啟動ApplicationController
    <!-- 加入網路存取權限 -->
    <uses-permission android:name="android.permission.INTERNET" />
       
    <!-- 加入android:name這項屬性讓ApplicationController能在開啟app後自動建立ApplicationController-->
    <application
        android:name=".ApplicationController"
        android:allowBackup="true"
        .......
    />

5. 建立request

i. GET
private void doGet(){
    // Service的URL
    String url = "http://192.168.11.13/~shiun/ws/service/index";
    // 建立期望收到JsonObject的request
    // 若new JsonObjectRequest的第二個參數為null則代表是GET,反之若為JSONObject則為POST
    JsonObjectRequest request = new JsonObjectRequest(url, null, 
        new Listener<JSONObject>() {
            // 若server回傳HTTP status 200或204則會進入onResponse method 
            // 註:根據官網Issue 57527的討論,最新版應該是2xx都算request success
            @Override
            public void onResponse(JSONObject response) {
                Log.i(tag, response.toString());
            }
        }, new ErrorListener() { // Request失敗處理
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(tag, error.getMessage());
            }
    });
    // 將request放到RequestQueue,Volley會以非同步方式送出request
    ApplicationController.getInstance().addToRequestQueue(request);
}

GET成功~

ii. POST: POST的關鍵在於new JsonObjectRequest時要將POST的資料放到第二個參數中
private void doPost() throws JSONException {
    // Service的URL
    String url = "http://192.168.11.13/~shiun/ws/service/index";
    JSONObject param = new JSONObject();
    param.put("key1", "POST_value1");
    param.put("key2", "POST_value2");
    // 建立期望收到JsonObject的request
    // 因為第二個參數不是null,所以Volley把request當做POST傳送
    JsonObjectRequest request = new JsonObjectRequest(url, param, 
        new Listener<JSONObject>()
        // Request成功
            @Override
            public void onResponse(JSONObject response) {
                Log.i(tag, response.toString());
            }
        }, new ErrorListener() { // Request失敗處理
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(tag, error.getMessage());
            }
    });
    // 將request放到RequestQueue,Volley會以非同步方式送出request
    ApplicationController.getInstance().addToRequestQueue(request);
}

POST成功~

iii. PUT: PUT的關鍵點在於要new有method版本的JsonObjectRequest
private void doPut() throws JSONException
{
    String url = "http://192.168.11.13/~shiun/ws/service/index";
    JSONObject param = new JSONObject();
    param.put("key1", "PUT_value1");
    param.put("key2", "PUT_value2");
    // 這次的關鍵點在於使用Request.Method.PUT
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.PUT, url, param,
        new Listener() {
            @Override
            public void onResponse(JSONObject response) {
                Log.i(tag, response.toString());
            }
        }, new ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(tag, error.getMessage());
            }
        });
    ApplicationController.getInstance().addToRequestQueue(request);
}

PUT成功~

iv. DELETE: DELETE時只需要把Method換成Request.Method.DELETE並配合service提供的URL即可
private void doDelete() throws JSONException
{
    String url = "http://192.168.11.13/~shiun/ws/service/index/1";
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.DELETE, url, null,
        new Listener() {
            @Override
            public void onResponse(JSONObject response) {
                Log.i(tag, response.toString());
            }
        }, new ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(tag, error.getMessage());
            }
        });
    ApplicationController.getInstance().addToRequestQueue(request);
}

DELETE成功~


參考資料

1. Asynchronous HTTP Requests in Android Using Volley

2. Android Volley Tutorial - Making HTTP GET, POST, PUT, DELETE Requests

3. Application-element: "Android:name"