Getting Started with Kalay Starter Kit

最近在玩 WRTnode (http://wrtnode.cc),為此也做了幾個小應用程式跟這塊可愛的小板子進行互動,不過之間涉及到網路的聯通動作都還只能在內網下操作,雖說在外網的操作可以藉由像是 MQTT 來達成,不過資料總還是得透過一個中繼伺服器來進行傳輸,總覺得不怎麼可靠呢!

不過在市面上找到一個 P2P 的連線解決方案 Kalay,就讓我們來試試骨子裡賣什麼藥吧!

Requirements

  • WRTnode
  • Android 手機 (也可以用模擬器)
  • 一顆熱誠的心

What will we learn

在本次的主題中,將會建立一個 Android App,用以和 WRTnode 互動,在 App 上按下按鈕,將會開啟或關閉 WRTnode 上的 LED 燈。在這個實作上我們將會分為二個部分進行:

  • WRTnode 撰寫 Restful API,讓 Web Client 可以透過 Restful API 控制板子上的 LED。
  • 撰寫 Android App,並使用 WRTnode 的 Restful API。

Let’s start with WRTnode

在拿到 WRTnode 之後發現這塊板子沒有網路孔,看來只能先透過無線網路方式連到它再進行設定了,所幸 WRTnode 官網上的設定方式 還算親民,從設定對外網路到可以透過 SSH 連線進 WRTnode 約半小時內可完成。

WRTnode 也原生支援 python,因此可以透過 python web framework 完成我們的 API。這邊我用的 python web framework 是 Bottle,Bottle 的安裝方式相對容易,可以直接透過 wget 下載。

1
wget --no-check-certificate https://github.com/bottlepy/bottle/raw/master/bottle.py

在此之前,我們先小試一下 Bottle 的功能,下面是 Bottle 官網上的小程式,我們拿來試一下效果。

1
2
3
4
5
6
7
from bottle import route, run, template

@route('/hello/<name>')
def index(name):
return template('<b>Hello {{name}}</b>!', name=name)

run(host='0.0.0.0', port=8080)

執行上面的 python 程式碼後,WRTnode 將會運行一個 Listen 8080 port 的 web server,這時我們可以透過瀏覽器看一下執行結果。
hello world with Bottle

那麼,要怎麼使用 python 控制 LED 燈呢?下面是個簡單的範例,執行這個程式後,WRTnode 上的 LED 燈將會 blink blink 的閃個不停喔!

1
2
3
4
5
6
7
import os, time

while True:
os.system('echo 0 > /sys/class/leds/wrtnode:blue:indicator/brightness')
time.sleep(1)
os.system('echo 1 > /sys/class/leds/wrtnode:blue:indicator/brightness')
time.sleep(1)

太好了!如果把上面二個程式結合在一起,不就可以透過 web browser 控制 LED 了嗎?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from bottle import route, run
import os

@route('/led/<switch>')
def led_switch(switch):
if switch == "on":
os.system('echo 0 > /sys/class/leds/wrtnode:blue:indicator/brightness')
return "LED ON"
elif switch == "off":
os.system('echo 1 > /sys/class/leds/wrtnode:blue:indicator/brightness')
return "LED OFF"
else:
return "Not support"

run(host='0.0.0.0', port=8080)

執行上面的程式後,便可以透過 web browser 操作 WRTnode 上的 LED 燈了。

  • 開啟 LED 燈:http://%IP%:8080/led/on
  • 關閉 LED 燈:http://%IP%:8080/led/off

讓我先告一段落在 WRTnode 上的實作,接下來我們要撰寫 Android App 控制 WRTnode。

當然,在開發 Android 應用程式之前,要先將開發工具打點好,這邊我選用的是 Android Studio 1.3.2 (http://developer.android.com/sdk),Android Studio 的安裝及設定在此就不介紹了,需要這方面資訊的朋友可以前往 Google 大神詢問。

在安裝好 Android Studio 之後,我們先來建立個應用程式吧,然後我們在 Activity 上加入二個按鈕,分別用作開啟及關閉 LED 燈。

以下是我自己應用程式的 layout。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">


<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="LED On"
android:id="@+id/btnOn" />


<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/btnOn"
android:text="LED Off"
android:id="@+id/btnOff"/>


</RelativeLayout>

接下來為了能讓應用程式可以連上網路,因此必須給予 Internet accessible 的 permission。

1
<uses-permission android:name="android.permission.INTERNET" />

在這個應用程式中,我使用了 Volley 來存取 HTTP,所以必須在 Gradle 中加入。

1
compile 'com.mcxiaoke.volley:library:1.0.18'

最後我們在主程式 (MainActivity.java) 中撰寫這二個按鈕的程式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static final String TAG = "LED_SWITCH";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Button btnOn = (Button)findViewById(R.id.btnOn);
Button btnOff= (Button)findViewById(R.id.btnOff);

// We use Volley to send http command to the device.
final RequestQueue queue = Volley.newRequestQueue(this);

btnOn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Send http request to the WRTnode.
StringRequest request = new StringRequest(Request.Method.GET, "http://192.168.2.108:8080/led/on",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d(TAG, response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "That didn't work!");
}
}
);

queue.add(request);
}
});

btnOff.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Send http request to the WRTnode.
StringRequest request = new StringRequest(Request.Method.GET, "http://192.168.2.108:8080/led/off",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d(TAG, response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "That didn't work!");
}
}
);

queue.add(request);
}
});
}

好了,在程式順利編譯執行後,按下「On」、「Off」按鈕,可以在 logcat 中看到 WRTnode 的回覆。

Use Kalay SDK

截至目前為止,我們都可以在內網 (LAN) 中存取 WRTnode 的 HTTP 服務,但如果我們要在外網的環境下存取 WRTnode 的話,勢必得設定 Router 進行 port-mapping 或是 port-forwarding 了。所幸我們今天要介紹的 Kalay 提供了另一種簡決方式,透過簡易的 API 使用,便可以 P2P tunneling 的方法存取遠端的裝置。

在 Kalay Starter Kit 中提供了給 WRTnode 使用的 C library,我們可以使用 WRTnode 的交叉編譯器 (Cross compiler) 產出 Kalay P2PTunnel 執行檔 (Daemon) 放在 WRTnode 上執行。

在 WRTnode 的 wiki 官網上可以下載預先編譯好的 tool-chain,我這邊使用 Ubuntu 14.04LTS 下載後並解壓縮。

在佈置好 WRTnode 的 tool-chain 後,我們先以 Kit 中的 Sample code 試著編譯看看。

1
2
3
4
$ export CC=%WRTNODE_TOOL_CHAIN_FOLDER%/bin/mipsel-openwrt-linux-uclibc-gcc
$ export PLATFORM=%KALAY_KIT_FOLDER%/Lib/Linux/Mips_MT7620AOpenwrt_4.8.3
$ cd %KALAY_KIT_FOLDER%/Sample/Linux/Sample_P2PTunnel
$ "$CC" P2PTunnelServer.c -I../../../Lib/ -L"$PLATFORM" -lP2PTunnelAPIs -lRDTAPIs -lIOTCAPIs -lpthread -ldl -o P2PTunnelServer

編譯成功後,我們將 P2PTunnelServer 以及 Kalay SDK 的 Library 檔案丟到 WRTnode 上執行。

1
LD_LIBRARY_PATH=%KALAY_KIT_LIB_FOLDER% ./P2PTunnelServer %UID%

這時已經完成 WRTnode 的整合,接下來進行的是 Android App 上的整合。

從 Kalay Starter Kit 中發現提供給 Android 使用的是 Native Library,在 Android Studio 中使用 Native Library 則必須將這些檔案丟到特定的目錄中。

app/src/main/jniLibs/armeabi/xxx.so

app/src/main/jniLibs/x86/xxx.so

在完成 Library 及 wrapper class 的佈署後,我們在畫面上新增一個文件方塊及按鈕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">


<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="Please input UID"
android:maxLength="20"
android:inputType="textCapCharacters"
android:id="@+id/edtUID" />


<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/edtUID"
android:text="CONNECT"
android:id="@+id/btnConnect"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/btnConnect"
android:text="LED On"
android:id="@+id/btnOn" />


<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/btnOn"
android:layout_alignTop="@id/btnOn"
android:text="LED Off"
android:id="@+id/btnOff"/>


</RelativeLayout>

在這個新的應用程式,我們必須先輸入 Kalay UID,然後按下 CONNECT 按鈕,待 App 與 WRTnode 成功後,才得使用 LED ONLED OFF 這二個按鈕進行控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
public class MainActivity extends ActionBarActivity implements P2PTunnelAPIs.IP2PTunnelCallback {

static final String TAG = "LED_SWITCH";
static final String USER = "Tutk.com"; // refer to P2PTunnelServer.c
static final String PWD = "P2P Platform"; // refer to P2PTunnelServer.c

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

final EditText edtUID = (EditText)findViewById(R.id.edtUID);
final Button btnOn = (Button)findViewById(R.id.btnOn);
final Button btnOff= (Button)findViewById(R.id.btnOff);
final Button btnConnect = (Button)findViewById(R.id.btnConnect);

// Disable these LED switch buttons
btnOn.setEnabled(false);
btnOff.setEnabled(false);

// Initiate the P2PTunnelAPI
final P2PTunnelAPIs api = new P2PTunnelAPIs(this);
api.P2PTunnelAgentInitialize(4);

btnConnect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (edtUID.length() == 20) {

// Disable the connect button
refreshConnectButtonStatus(false);

// Start a new thread to connect to device
new Thread(new Runnable() {
@Override
public void run() {
String UID = edtUID.getText().toString();
int ret = 0;
int[] errFromDevice = new int[1];

ret = api.P2PTunnelAgent_Connect(UID, getAuthData(USER, PWD), getAuthDataLength(), errFromDevice);
if (ret >= 0) {
int idx = api.P2PTunnelAgent_PortMapping(ret, 8080, 8080);
if (idx >= 0) {
Log.d(TAG, "Session established");
// Enable LED buttons
refreshLEDButtonStatus(true);
} else {
Log.d(TAG, "Failed to mapping ports");
// Enable the connection button
refreshConnectButtonStatus(true);
}
} else if (ret == P2PTunnelAPIs.TUNNEL_ER_AUTH_FAILED) {
if (errFromDevice[0] == -888) {
// The error code -888 is from P2PTunnelServer.c
Log.d(TAG, "The auth data is wrong.");
}
// Enable the connection button
refreshConnectButtonStatus(true);
} else {
// Enable the connection button
refreshConnectButtonStatus(true);
}
}
}).start();
}
}
});

// We use Volley to send http command to the device.
final RequestQueue queue = Volley.newRequestQueue(this);

btnOn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Send http request to the WRTnode.
StringRequest request = new StringRequest(Request.Method.GET, "http://127.0.0.1:8080/led/on",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d(TAG, response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "That didn't work!");
}
}
);

queue.add(request);
}
});

btnOff.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Send http request to the WRTnode.
StringRequest request = new StringRequest(Request.Method.GET, "http://127.0.0.1:8080/led/off",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d(TAG, response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "That didn't work!");
}
}
);

queue.add(request);
}
});
}

private int getAuthDataLength() {
return 128;
}

private byte[] getAuthData(String username, String password) {
/* The authdata structure between device and client:
typedef struct st_AuthData
{
char szUsername[64];
char szPassword[64];
} sAuthData;
*/


byte[] result = new byte[128];
byte[] acc = username.getBytes(StandardCharsets.US_ASCII);
byte[] pwd = password.getBytes(StandardCharsets.US_ASCII);

// copy acc and pwd to result
System.arraycopy(acc, 0, result, 0, acc.length);
System.arraycopy(pwd, 0, result, 64, pwd.length);

return result;
}

private void refreshConnectButtonStatus(final boolean status) {
this.runOnUiThread(new Runnable() {
@Override
public void run() {
final Button btnConnect = (Button)findViewById(R.id.btnConnect);
btnConnect.setEnabled(status);
}
});
}

private void refreshLEDButtonStatus(final boolean status) {
this.runOnUiThread(new Runnable() {
@Override
public void run() {
final Button btnOn = (Button)findViewById(R.id.btnOn);
final Button btnOff= (Button)findViewById(R.id.btnOff);
btnOn.setEnabled(status);
btnOff.setEnabled(status);
}
});
}

@Override
public void onTunnelStatusChanged(int nErrCode, int nSID) {
if (nErrCode == P2PTunnelAPIs.TUNNEL_ER_DISCONNECTED) {
refreshConnectButtonStatus(true);
refreshLEDButtonStatus(false);
}
}

@Override
public void onTunnelSessionInfoChanged(sP2PTunnelSessionInfo object) {

}
}

讓我們編譯進執行,然後輸入 WRTnode 的 UID,按下 CONNECT 按鈕,成功建立連線後,就可以操作 LED 燈號了!

App Layout

透過上述的實作,我們明白即便在外網,透過 Kalay 也能夠簡易地連線到遠端的裝置。

上述 Android 程式碼可於 Github 上下載 (https://github.com/cloudhsiao/led_switch)。


本文章以 CC 方式授權。