アクセスカウンタ

<<  2016年6月のブログ記事  >>  zoom RSS

トップへ


スマホ から ESP8266リモコン を リモートコントロール

2016/06/18 23:16
前回は、MQTTプロトコルで、ESP8266リモコンを操作しましたので、
今回は、スマホからESP8266リモコンを操作したいです。

普通(?)に考えると、スマホのアプリからAWS IoTへ接続し、
MQTTでPublishするのかもしれませんが、

スマホのアプリを作るのも面倒ですので、
ブラウザ(@スマホ)から、AWS EC2上のWEBサーバーにアクセスして、
そのサーバーからAWS IoTへMQTTでPublishする方法を取ります

ブラウザから操作するようにすれば、スマホからでもPCからでも、
タブレットからでも、操作出来て、一石二鳥以上です

まずは、javascriptでAWS IoTへPublishしてみます。

このサイトに書いてある通りに、、、
1:var awsIot = require('aws-iot-device-sdk');
2:
3:var device = awsIot.device({
4: keyPath: './certs/private.pem.key',
5: certPath: './certs/certificate.pem.crt',
6: caPath: './certs/iot-rootca.pem.crt',
7: clientId: 'test-devdev',
8: region: 'ap-northeast-1'
9:});
10:
11:device
12: .on('connect', function() {
13: console.log('connect');
14: device.subscribe('#');
15: device.publish('topic_2', JSON.stringify({ test_data: 1}));
16: });
17:
18:device
19: .on('message', function(topic, payload) {
20: console.log('message', topic, payload.toString());
21: });
このファイルを app.js として保存して、
証明書は、AWS IoTで作成した証明書を用意します。
ルート証明書は、 VeriSign root CA certificate を用意します。
これで、

npm install aws-iot-device-sdk
node app.js

とすれば、AWS IoTに接続して、publish と subscribe してくれます
簡単すぎますね。

接続先を指定していませんが、なんかうまいことやってくれているのかな

あとは、これをAWS EC2上で動かして、WEB APIのように見せればよいかと思います。

ということで、作成したWEBページがこんな感じ。
画像
初めてのWEBデザインなので素人感満載です
こんなデザインですが、このページ作成には、結構な時間がかかっています

温度、湿度、気圧の最新データはDynamoDBから取得しています。
これも、ブラウザから直接DynamoDBにアクセスせずに、
WEB APIを用意して、AWS EC2がDynamoDBから取得しています。

スピンボタンで設定温度を調整します。
本物のリモコン同様に、下限14℃、上限30℃にしました。
温度設定後に、冷房と暖房、停止ボタンを押すと、
AWS EC2からESP8266リモコンにコマンドが送られます。

コマンドを送信して、ESP8266リモコンからの応答を受信すると、
下からニョキニョキっとメッセージが上がってきて、送信できたことを知らせてくれます
画像
応答が来ない場合は、失敗した旨を知らせてくれます。

さて、これで一応WEBページはできましたが、
AWS EC2上にWEBサーバを立てるとなると、セキュリティの問題が出ちゃいます。
エアコンのON/OFFしかないけどね。

ということで、HTTPS限定にして、さらにクライアント認証必須にすることにしました。
自己ルート認証局、サーバー証明書、クライアント証明書に関しては、
以前お勉強したので、すんなり行けますが、問題は、これをNode.jsで
どのようにHTTPS + クライアント認証にするかです。。。

このページを参考にして、app.jsを以下のようにしました。
1:var
2: routes = require('./lib/routes'),
3: fs = require('fs'),
4: https = require('https'),
5: http = require('http'),
6: express = require('express'),
7: morgan = require('morgan'),
8: app = express(),
9: options = {
10: key : fs.readFileSync('./certs/server/server.key.pem'),
11: cert : fs.readFileSync('./certs/server/server.cert.pem'),
12: passphrase: process.env.CERT_PASS,
13: ca : fs.readFileSync('./certs/server/rootca.cert.pem'),
14:
15: honorCipherOrder: true,
16: requestCert: true,
17: rejectUnauthorized: true
18: };
19:// ------------- END MODULE SCOPE VARIABLES ---------------
20:
21:// ------------- BEGIN SERVER CONFIGURATION ---------------
22:app.use(morgan(':date[iso] : :remote-addr - :method :url :status'));
23:app.use(express.static(__dirname + '/public'));
24:
25:routes.configRoutes(app);
26:// -------------- END SERVER CONFIGURATION ----------------
27:
28:// ----------------- BEGIN START SERVER -------------------
29:var server_port = process.env.WEB_PORT || 3000;
30:if (process.env.OS === 'Windows_NT') {
31: http.createServer(app).listen(server_port, function () {
32: console.log('HTTP server listening on port %d in %s mode',
33: server_port, app.settings.env);
34: });
35:}
36:else {
37: https.createServer(options, app).listen(server_port, function () {
38: console.log('HTTPS server listening on port %d in %s mode',
39: server_port, app.settings.env);
40: });
41:}
42:// ------------------ END START SERVER --------------------
43:

optionsにhttpsの設定が入っています。証明書は、自分で作成した証明書になります。
passphraseは、サーバーの秘密鍵のパスワードです。
ブログに間違ってアップロードしないように、環境変数に入っています
honorCipherOrderは、よくわからない攻撃用に推奨しているので入れました。
requestCertが、クライアント認証要求です。
rejectUnauthorizedは、認証されていないクライアントを拒否るようです。

httpも入っているのは、localhostでhttpsをやろうとしたら怒られたからです。
判定はいい加減で、OSがWindows_NTかどうかで見ています。

あと、ルーティング部分は、
1:configRoutes = function (app) {
2: if (process.env.OS !=='Windows_NT') {
3: app.all('*', function (req, res, next) {
4: // 証明書の検証
5: var cert = res.connection.getPeerCertificate();
6:
7: if (cert.subject.CN !== 'home-controller' ||
8: new Date(cert.valid_to) < Date.now()) {
9: console.log("CN :" + cert.subject.CN); // for debug
10: console.log("Valid:" + cert.valid_to);
11: res.sendStatus(406); //Not Acceptable
12: return;
13: }
14: next();
15: });
16: }
17:
として、クライアント証明書の検証を入れています。
Node.js側でどのくらいやってくれているかよく分からないので、
とりあえず、クライアント証明書のCNと有効期限をチェックしています。
CNは、home-controller用に証明書を発行しましたので、これ以外の目的は
拒否するようにしています。まだ、これ以外は無いんですけどね

これで、クライアント側にルート認証局証明書、クライアント証明書を入れて、
AWS EC2上で、Node.js WEBサーバーを立てれば完了です。

PCのChromeからアクセスすると、証明書の選択画面が出てきました。
画像
OKをおすと、無事にhttpsでアクセスできました
画像

同様にスマホからも、証明書をインストールして、
画像
となりました

ここで注意
スマホからテストするとき、WiFiを切ってアクセスしましょう。
AWS EC2側でIPアドレス制限している場合、外に行ったらスマホから
アクセスできないってことになります

もう一点
EC2上にバックグラウンドでNode.js WEBサーバーを立てる場合、
単に、

node app.js &

としただけでは、PUTTYを切った後、しばらくすると、
nodeプロセスは死んでしまいます

いろいろ調べたところ、どうやらSSHで接続すると、
SSHのプロセスが親プロセスになって、nodeプロセスは子プロセス
として動作するため、親がいなくなると、子は消されるようです

これを回避するためには、

nohup node app.js > test.log 2>&1 &

のように、nohupコマンドを使用します。
ログはエラー含めて、test.logに出力されます

なんとか、真夏になる前に、外から冷房を入れられるようになりました
真夏が楽しみでなりません

最後に、証明書は入ってないですが、WEBサーバーのソースコードを参考まで ⇒ ここ

さて、つぎは、何つくろうかな
記事へブログ気持玉 / トラックバック / コメント


ESP8266 を AWSクラウド エアコン用リモコンに

2016/06/12 09:48
前回は、ESP8266をエアコンの赤外線リモコンにましたので、
AWS IoTを経由して、このリモコンを操作します。

AWS IoTへMQTTプロトコルでPublishしたコマンドを
ESP8266でSubscribeしてあげれば、外部操作できそうです。

まずは、コマンドを定義します。コマンドは、流行りのjson形式で、

{
"operation":"stop",
"temp": 26
}

としました。
operationは、動作停止(stop)、冷房(cool)、暖房(heat)、除湿(dry)をサポートし、
tempは、設定温度を数値で入れます。

ESP8266 SDKに付属のcJSONライブラリで簡単にjsonを読めます。

コマンド解析と、赤外線送信は次のようにしました。
1:char name[12];
2:cJSON *msg;
3:cJSON *cjson_ope;
4:cJSON *cjson_temp;
5:int8 response = -1;
6:uint16 cmd = 0;
7:uint16 temp = 0;
8:
9:msg = cJSON_Parse(cmd_json);
10:if (msg == NULL) {
11: IPRINTF("cJSON error\n");
12: return;
13:}
14:
15:irom_strcpy(name, "operation");
16:cjson_ope = cJSON_GetObjectItem(msg, name);
17:irom_strcpy(name, "temp");
18:cjson_temp = cJSON_GetObjectItem(msg, name);
19:
20:if ((cjson_ope != NULL) && (cjson_temp != NULL)
21: && (cjson_ope->type == cJSON_String)
22: && (cjson_temp->type == cJSON_Number)) {
23:
24: temp = (cjson_temp->valueint * 2) & 0xff;
25: if (irom_strcmp(cjson_ope->valuestring, "stop") == 0) {
26: cmd = AIRCON_OPERATE_STOP;
27: }
28: else if (irom_strcmp(cjson_ope->valuestring, "cool") == 0) {
29: cmd = AIRCON_OPERATE_COOL;
30: }
31: else if (irom_strcmp(cjson_ope->valuestring, "heat") == 0) {
32: cmd = AIRCON_OPERATE_HEAT;
33: }
34: else if (irom_strcmp(cjson_ope->valuestring, "dry") == 0) {
35: cmd = AIRCON_OPERATE_DRY;
36: }
37: if ((cmd != 0) && (temp >= 16*2) && (temp <= 30*2)) {
38: ir_data[AIRCON_OFST_OPERATE] = cmd;
39: ir_data[AIRCON_OFST_TEMP] = temp;
40: ir_data[AIRCON_OFST_SUM] = (AIRCON_SUM + cmd + temp) & 0x00ff;
41:
42: IPRINTF("AIRCON:%s:%d\n",
43: cjson_ope->valuestring, cjson_temp->valueint);
44: IR_Tx(ir_data, sizeof(ir_data) / sizeof(uint16));
45: response = 0;
46: }
47: else {
48: ERROR("Aircon cmd error");
49: response = 1;
50: }
51:}
52:else {
53: ERROR("Aircon cmd format error");
54: response = 2;
55:}
56:cJSON_Delete(msg);
最初にcJSON_Parseで解析して、cJSON_GetObjectItemでアイテムを取り出します。
最後にcJSON_Deleteで後始末します
15行目と17行目でitem名をname変数にコピーしていますが、
これは文字列をすべてirom領域に配置しているため、
そのままcJSON_GetObjectItemに渡すとアクセスエラーとなるためです。
ESP8266 SDKを変な風に使っているための仕様です

AWS IoTを使用するなら、Shadow機能を使ったほうが良いかもしれませんが、
単なるリモコンであり、エアコン側の状態が取得できないため、
Shadow機能を利用してもあまりメリットがないと考えました

しかし、本当にコマンドが遅れたかどうかは、知りたいので、
ESP8266はコマンドに対して応答を返すようにしました。
1:if (response >= 0) {
2: MQTTPublishParams *pubResp;
3: char *pTopicMsg;
4:
5: if ((pubResp = calloc(sizeof(MQTTPublishParams), 1)) == NULL) {
6: WARN("AIRCON result alloc err");
7: }
8: else if ((pTopicMsg = calloc(20+50, 1)) == NULL) {
9: free(pubResp);
10: WARN("AIRCON result alloc err");
11: }
12: else {
13: irom_strcpy(&pTopicMsg[ 0], AIRCON_RESP_TOPIC);
14: irom_strcpy(&pTopicMsg[20], response_text[response]);
15: pubResp->pTopic = &pTopicMsg[0];
16: pubResp->MessageParams.qos = QOS_0;
17: pubResp->MessageParams.isRetained = false;
18: pubResp->MessageParams.pPayload = &pTopicMsg[20];
19: pubResp->MessageParams.PayloadLen = strlen(&pTopicMsg[20]);
20: aws_iot_mqtt_publish(pubResp);
21: INFO("AIRCON:%s", response_text[response]);
22:
23: free(pTopicMsg);
24: free(pubResp);
25: }
26:}
このコードが前記のコードの後に続いています。
応答用のトピックに応答結果をjson形式で入れて、Publishしています
応答文字列(response_text)は
1:static const char* response_text[] = {
2: "{\"result\":\"success\"}",
3: "{\"result\":\"command error\"}",
4: "{\"result\":\"format error\"}"
5:};
の3種類だけです

実験として、対抗機はいつものMQTT.fxを使用しました。
次のようにPublishして、
画像

Subscribeで結果を見てみると、
画像

ESP8266が応答をPublishしていることが確認できました

なんか、応答までの時間がやたら短いですが、
コマンドの時間は、こちらからの送信時間ではなく、AWSがSubscribe先に
送信したものを受信した時間だからだと思います。

ESP8266のログは、
画像

とあり、コマンドの受信と応答しています
エアコンもちゃんと、「ピッ」と言ってくれてます

ちなみに、前回の自作したIRのライブラリは、メモリリークしていましたので、
こっそり修正しておきました。

これで、ESP8266側のソフトは完了かと思います
参考までに、こちらに置きました。 ⇒ ここ

次は、スマホからエアコンを操作ですね
記事へなるほど(納得、参考になった、ヘー) ブログ気持玉 2 / トラックバック 0 / コメント 0


<<  2016年6月のブログ記事  >> 

トップへ

迷える子羊の苦悩 2016年6月のブログ記事/BIGLOBEウェブリブログ
文字サイズ:       閉じる