概要
下記3つの記事のAndroid Things × Raspberry Pi3(以後ラズパイ)版
- myThingsで勤怠連絡ボタンを作ってみた
- myThingsとAmazon Dash ButtonをAWS経由で繋げてみる
- Raspberry Pi3×SORACOM×AWS IoT×myThingsで勤怠連絡ボタンを作ってみる
3つ目の記事と同様、ラズパイでボタンを押したら、勤怠連絡が飛ぶようにする
前提
- Android ThingsをRaspberry Pi3で使ってみたでAndroid Thingsとラズパイの準備が終わっている
- ボタンでLチカサンプルを実際にやってみている
- Google Cloud Platform(以後GCP)の登録が完了している
検討フロー
下記のどちらかでやってみようと考え、
- ラズパイ -> AWS API Gateway -> AWS Lambda -> myThings Developers
- myThingsとAmazon Dash ButtonをAWS経由で繋げてみる」のAWS API Gatewayを使用
- ボタンでLチカサンプルボタンを押したタイミングでHTTPリクエスト
- ただし、keydownの間に通信としてしまうと、ずっと通信しちゃうので要注意
- ラズパイ -> Google Cloud Pub/Sub -> Google Cloud Funtions -> myThings Developers
- 天気サンプルのPub/Subを参考にする
- ボタンを押したらPublishし、subscribeして受けた方はFunctionsを呼び出す
- そのFuntionsの中でmyThings Developersを呼び出す
今回は2のGCPを使うパターンでやってみることにします。
ベースのソースコードはボタンでLチカサンプルを使い、必要に応じて、天気サンプルのPub/Sub部分を使う
Google Cloud Pub/Sub
github上の説明を参考にして、GCPの準備をする
- APIマネージャーでPub/Subを有効にする
- IAMと管理ページで、新しいサービスアカウントを作成(役割は特に入れなくて問題無し)、その時に新しい秘密鍵をjson形式で作成しておく(完了するとjsonファイルがダウンロードされる)
- 新しいトピックを作成して、2で作成したサービスアカウントに権限を付与、その際の役割は「Pub/Subパブリッシャー」にしておく
- 作成したトピック配下で、新しいサブスクリプションを作成
- 2で作成した際にダウンロードされたjsonファイルを「credentials.json」にリネームし、res配下にrawフォルダを作成してそこに置く
- app/build.gradleを下記のように修正
app/build.gradle
・・・・・
buildTypes {
debug {
buildConfigField "String", "PROJECT_ID", '"<<プロジェクトID>>"'
buildConfigField "String", "PUBSUB_TOPIC", '"<<topic>>"'
}
release {
initWith(buildTypes.debug)
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
・・・・・
dependencies {
compile 'com.google.android.things.contrib:driver-button:0.2'
provided 'com.google.android.things:androidthings:0.3-devpreview'
compile('com.google.api-client:google-api-client-android:1.22.0') {
exclude group: 'org.apache.httpcomponents'
}
compile('com.google.apis:google-api-services-pubsub:v1-rev12-1.22.0') {
exclude group: 'org.apache.httpcomponents'
}
}
Google Cloud Functions
発火タイミングは、上記で準備したCloud Pub/Subのトピックに対するメッセージの着信時とする。
- Cloud Functionsで関数を作成をクリック
- 各設定は下記のようにする
- 作成したコードは下記(myThingsとAmazon Dash ButtonをAWS経由で繋げてみるで使用したjsとほぼ一緒)
index.js
'use strict';
const qs = require("querystring");
var https = require("https");
var date = new Date();
var dateString = createPostTimeString();
// myThings Developersに必要なリクエスト項目
var appid = "<<appid>>";
var secret = "<<secret>>";
var accessToken = "<<accessToken>>";
var refreshToken = "<<refreshToken>>";
/**
* Triggered from a message on a Cloud Pub/Sub topic.
*
* @param {!Object} event The Cloud Functions event.
* @param {!Function} The callback function.
*/
exports.subscribe = function subscribe(event, callback) {
// myThings Developersへリクエスト
requestDevelopers(callback);
};
/**
* "2016/12/21(水)"の形式の文字列を返す
* @return string 日付文字列
*/
function createPostTimeString() {
// 年
var year = date.getFullYear();
// 月
var month = date.getMonth() + 1;
if (month < 10) {
month = '0' + month;
}
// 日
var day = date.getDate();
if (day < 10) {
day = '0' + day;
}
// 曜日
var weekDayList = [ "日", "月", "火", "水", "木", "金", "土" ];
var weekDay = weekDayList[ date.getDay() ];
return year+"/"+month+"/"+day+"("+weekDay+")";
}
/**
* myThings Developersへのリクエスト
* @return void
*/
function requestDevelopers(callback) {
// リクエストパラメータの生成
var postArgs = {
date: dateString
}
var postData = qs.stringify({
"entry": JSON.stringify(postArgs),
});
// リクエスト設定
var options = {
hostname: "mythings-developers.yahooapis.jp",
path: "/v2/services/<<hogehoge>>/mythings/<<hugahuga>>/run",
port: 443,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Authorization": "Bearer " + accessToken,
},
};
// リクエスト
var req = https.request(options, function(res){
// 401のとき
if (res.statusCode == 401) {
// コールバック付きのrefreshAccessTokenを呼ぶ
refreshAccessToken(callback);
return;
}
// レスポンス処理
res.on("data", function(body){
var parseData = JSON.parse(body);
if(typeof( parseData["flag"] ) != "undefined") {
callback();
} else {
console.log("カスタムトリガーの実行リクエストの受付に失敗しました。:"+body);
}
});
})
.on("error", function(res){
callback();
});
req.end(postData)
}
/**
* アクセストークンのリフレッシュ
*/
function refreshAccessToken(callback) {
console.log("refreshAccessTokenにきたよ");
// リフレッシュ用データのセット
var reqData = qs.stringify({
"grant_type": "refresh_token",
"refresh_token": refreshToken
});
// リクエスト設定
var buffer = new Buffer(appid + ":" + secret, "ascii");
var options = {
hostname: "auth.login.yahoo.co.jp",
path: "/yconnect/v1/token",
port: 443,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Authorization": "Basic " + buffer.toString("base64"),
}
};
// リクエスト実行
var req = https.request(options, function(res) {
console.log("refreshAccessTokenのrequestのなかにきたよ");
// 401の場合
if(res.statusCode == 401) {
callback();
} else if(res.statusCode != 200) {
callback();
}
// レスポンス処理
res.on('data', function(body){
var parseData = JSON.parse(body);
accessToken = parseData['access_token'];
requestDevelopers(callback);
});
});
// POSTデータのリクエスト
req.end(reqData);
}
Android Things側の修正
- AndroidManifest.xmlは、下記の部分を追記
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- Pub/Sub部分は天気サンプルの[PubsubPublisher.java]をベースにして、下記のように実装。
PubsubPublisher.java
class PubsubPublisher {
private static final String TAG = PubsubPublisher.class.getSimpleName();
private final Context mContext;
private final String mAppname;
private final String mTopic;
private Pubsub mPubsub;
private HttpTransport mHttpTransport;
private Handler mHandler;
private HandlerThread mHandlerThread;
PubsubPublisher(Context context, String appname, String project, String topic,
int credentialResourceId) throws IOException {
mContext = context;
mAppname = appname;
mTopic = "projects/" + project + "/topics/" + topic;
mHandlerThread = new HandlerThread("pubsubPublisherThread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
InputStream jsonCredentials = mContext.getResources().openRawResource(credentialResourceId);
final GoogleCredential credentials;
try {
credentials = GoogleCredential.fromStream(jsonCredentials).createScoped(
Collections.singleton(PubsubScopes.PUBSUB));
} finally {
try {
jsonCredentials.close();
} catch (IOException e) {
Log.e(TAG, "Error closing input stream", e);
}
}
mHandler.post(new Runnable() {
@Override
public void run() {
mHttpTransport = AndroidHttp.newCompatibleTransport();
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
mPubsub = new Pubsub.Builder(mHttpTransport, jsonFactory, credentials)
.setApplicationName(mAppname).build();
}
});
}
public void start() {
mHandler.post(mPublishRunnable);
}
public void close() {
mHandler.removeCallbacks(mPublishRunnable);
mHandler.post(new Runnable() {
@Override
public void run() {
try {
mHttpTransport.shutdown();
} catch (IOException e) {
Log.d(TAG, "error destroying http transport");
} finally {
mHttpTransport = null;
mPubsub = null;
}
}
});
mHandlerThread.quitSafely();
}
private Runnable mPublishRunnable = new Runnable() {
@Override
public void run() {
ConnectivityManager connectivityManager =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
if (activeNetwork == null || !activeNetwork.isConnectedOrConnecting()) {
Log.e(TAG, "no active network");
return;
}
try {
JSONObject messagePayload = createMessagePayload();
Log.d(TAG, "publishing message: " + messagePayload);
PubsubMessage m = new PubsubMessage();
m.setData(Base64.encodeToString(messagePayload.toString().getBytes(),
Base64.NO_WRAP));
PublishRequest request = new PublishRequest();
request.setMessages(Collections.singletonList(m));
mPubsub.projects().topics().publish(mTopic, request).execute();
} catch (JSONException | IOException e) {
Log.e(TAG, "Error publishing message", e);
} finally {
// 天気情報のように定期的に情報をpublishしたいときは、下記のように1分おきにやるなどをする
//mHandler.postDelayed(mPublishRunnable, PUBLISH_INTERVAL_MS);
}
}
private JSONObject createMessagePayload()
throws JSONException {
JSONObject messagePayload = new JSONObject();
messagePayload.put("deviceId", Build.DEVICE);
messagePayload.put("channel", "pubsub");
messagePayload.put("timestamp", System.currentTimeMillis());
return messagePayload;
}
};
}
- ActivityはボタンでLチカサンプルのButtonActivityに対して下記のように追記
ButtonActivity.java
public class ButtonActivity extends Activity {
private static final String TAG = ButtonActivity.class.getSimpleName();
private Gpio mLedGpio;
private ButtonInputDriver mButtonInputDriver;
private PubsubPublisher mPubsubPublisher;
private boolean mLock = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "Starting ButtonActivity");
PeripheralManagerService pioService = new PeripheralManagerService();
try {
Log.i(TAG, "Configuring GPIO pins");
mLedGpio = pioService.openGpio(BoardDefaults.getGPIOForLED());
mLedGpio.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW);
Log.i(TAG, "Registering button driver");
// Initialize and register the InputDriver that will emit SPACE key events
// on GPIO state changes.
mButtonInputDriver = new ButtonInputDriver(
BoardDefaults.getGPIOForButton(),
Button.LogicState.PRESSED_WHEN_LOW,
KeyEvent.KEYCODE_SPACE);
mButtonInputDriver.register();
} catch (IOException e) {
Log.e(TAG, "Error configuring GPIO pins", e);
}
// start Cloud PubSub Publisher if cloud credentials are present.
int credentialId = getResources().getIdentifier("credentials", "raw", getPackageName());
if (credentialId != 0) {
try {
mPubsubPublisher = new PubsubPublisher(this, "sample-button",
BuildConfig.PROJECT_ID, BuildConfig.PUBSUB_TOPIC, credentialId);
} catch (IOException e) {
Log.e(TAG, "error creating pubsub publisher", e);
}
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SPACE) {
// Turn on the LED
setLedValue(true);
sendrequest();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SPACE) {
// Turn off the LED
setLedValue(false);
mLock = false;
return true;
}
return super.onKeyUp(keyCode, event);
}
/**
* Update the value of the LED output.
*/
private void setLedValue(boolean value) {
try {
mLedGpio.setValue(value);
} catch (IOException e) {
Log.e(TAG, "Error updating GPIO value", e);
}
}
/**
* リクエスト送信しちゃうぞ
*/
private void sendrequest() {
// onKeyDownの間、ずっと通信されない用の対応
if (!mLock) {
mLock = true;
Toast.makeText(this, "Press attendance contact button!!", Toast.LENGTH_LONG).show();![IMG_5206.GIF](https://qiita-image-store.s3.amazonaws.com/0/71637/74344646-e10d-8170-22a0-9adfeb48f586.gif)
if (mPubsubPublisher != null) {
mPubsubPublisher.start();
}
}
}
@Override
protected void onDestroy(){
super.onDestroy();
if (mButtonInputDriver != null) {
mButtonInputDriver.unregister();
try {
mButtonInputDriver.close();
} catch (IOException e) {
Log.e(TAG, "Error closing Button driver", e);
} finally{
mButtonInputDriver = null;
}
}
if (mLedGpio != null) {
try {
mLedGpio.close();
} catch (IOException e) {
Log.e(TAG, "Error closing LED GPIO", e);
} finally{
mLedGpio = null;
}
mLedGpio = null;
}
if (mPubsubPublisher != null) {
mPubsubPublisher.close();
mPubsubPublisher = null;
}
}
}
実際に動かしてみた
感想
- Android Studioで開発して、そのままビルドできるので、Androidエンジニアの方だと取っ付きやすそう
- Googleの提供ライブラリで簡単に開発ができる
- GCP(Pub/Sub、Dataflow、Funtionsなど)との親和性があるのでクラウド連携はとてもしやすい
後々やりたいこと
- MQTTを使った通信
- GCPのSpannerにデータを保存
- Dataflowにデータを流してみる
- Firebaseとの連携
- SORACOMを使っての接続
参考
- Android Things
- GCP