RaspberryPi
GoogleCloudPlatform
myThings
AndroidThings

Android Thingsで勤怠連絡ボタンで作ってみる

More than 1 year has passed since last update.


概要

下記3つの記事のAndroid Things × Raspberry Pi3(以後ラズパイ)版

3つ目の記事と同様、ラズパイでボタンを押したら、勤怠連絡が飛ぶようにする


前提


検討フロー

下記のどちらかでやってみようと考え、


  1. ラズパイ -> AWS API Gateway -> AWS Lambda -> myThings Developers



  2. ラズパイ -> 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の準備をする


  1. APIマネージャーでPub/Subを有効にする

  2. IAMと管理ページで、新しいサービスアカウントを作成(役割は特に入れなくて問題無し)、その時に新しい秘密鍵をjson形式で作成しておく(完了するとjsonファイルがダウンロードされる)

  3. 新しいトピックを作成して、2で作成したサービスアカウントに権限を付与、その際の役割は「Pub/Subパブリッシャー」にしておく

  4. 作成したトピック配下で、新しいサブスクリプションを作成

  5. 2で作成した際にダウンロードされたjsonファイルを「credentials.json」にリネームし、res配下にrawフォルダを作成してそこに置く

  6. 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のトピックに対するメッセージの着信時とする。


  1. Cloud Functionsで関数を作成をクリック

  2. 各設定は下記のようにする

    スクリーンショット 2017-04-16 0.27.03.png


  3. 作成したコードは下記(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;
}
};
}



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;
}
}
}



実際に動かしてみた

IMG_5207.GIF


感想


  • Android Studioで開発して、そのままビルドできるので、Androidエンジニアの方だと取っ付きやすそう

  • Googleの提供ライブラリで簡単に開発ができる

  • GCP(Pub/Sub、Dataflow、Funtionsなど)との親和性があるのでクラウド連携はとてもしやすい


後々やりたいこと


  • MQTTを使った通信

  • GCPのSpannerにデータを保存

  • Dataflowにデータを流してみる

  • Firebaseとの連携

  • SORACOMを使っての接続


参考