以前、以下の記事で、Mongoose OSとESP32を使って、RPCエンドポイントを立ち上げました。
Mongoose OSを本当に使いこなす(1/2):パイ専ボードにRPCエンドポイントを作る
今回は、それをブラウザのJavascriptから呼び出します。
下の絵のうちの、WebサーバとWebブラウザの部分です。
ソースコード一式は以下です。
https://github.com/poruruba/mongoose_test
デモページは以下です。
https://poruruba.github.io/mongoose_test/web_server/mongoose_test/public/pisen/
ESP32側で制御するデバイス
ここでいったん、ESP32側に接続するデバイスについて整理しておきます。
人感センサ
Digital Output ×1
マルチボタン
Digial Output ×2
LED
Digital Input×1
環境センサ
I2C ×2
BME280:スレーブアドレス:0x76
DHT12:スレーブアドレス:0x5C
上記のInput/Outputは、デバイス側から見た場合の方向です。
人感センサとマルチボタンは、前回の投稿で示した通り、ESP32内で常時GPIOのInputが変化したかどうかを確認しています。
残りの、LEDと環境センサは、WebブラウザのJavascript側から操作します。
LEDは、RPCのうちのGPIOを利用し、環境センサはI2CのRPCを利用します。
パイ専ボードを使っているのですが、ESP32の以下のGPIOに各デバイスを割り当てました。
デバイス名 | デバイス端子 | ESP32のGPIO |
---|---|---|
人感センサ | Digial Output | GPIO16 |
マルチボタン | Digital Output | GPIO4 |
↑ | Digital Output | GPIO21 |
LED | Digital Input | GPIO25 |
環境センサ | I2C(SDA) | GPIO14 |
↑ | I2C(SCL) | GPIO13 |
別のESP32を使っている場合はそれに合わせて変更してください。
GPIOのどのPINを使うかの選択について、補足します。
一応、各PINはマルチプレクサとなっているのですが、PIN番号によっては割り当てできる機能に制限がある場合があります。気になる方は、以下のページを参考にしてください。
ESP32 Pinout Reference: Which GPIO pins should you use?
https://randomnerdtutorials.com/esp32-pinout-reference-gpios/
RPC呼び出しの転送サーバの立ち上げ
Mongoose OSは、HTTP Postを使ってRPC(Remote Procesure Call)ができる機能があるのですが、CORS対応されていませんので、ブラウザのJavascriptからは直接は呼び出せません。
そこで、いったんサーバにRPCと同じ電文でHTTP Postしたのち、そのままESP32にHTTP Postを転送するようにします。
'use strict';
const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const Redirect = require(HELPER_BASE + 'redirect');
const fetch = require('node-fetch');
exports.handler = async (event, context, callback) => {
var body = JSON.parse(event.body);
console.log(body);
const headers = { "Content-Type" : "application/json" };
return fetch(body.target_url, {
method : 'POST',
body : JSON.stringify(body),
headers: headers
})
.then((response) => {
if( !response.ok )
throw 'response is not ok.';
return response.json();
})
.then(json =>{
console.log(json);
return new Response(json);
});
};
ESP32に対するHTTP Post呼び出しには、node-fetchを使いました。
ブラウザのJavascriptからHTTP Post呼び出しを受け取ると、その中のパラメータtarget_urlで示されるURLにそのまま転送しているだけです。target_urlはESP32デバイスのURLです。
GitHubからソースコードをダウンロードしている場合は、以下のフォルダに移動したのち、
mongoose_test/web_server/mongoose_test
以下を実行します。
> node app.js
環境変数を特に設定していなければ、以下のURLにPOST できるようになっているはずです。
http://localhost:10080/mongoose
swagger.yamlに記載の通り、/mongooseというエンドポイントで待ち受けています。
MQTTブローカを立ち上げる
すでにMQTTブローカは立ち上がっていますでしょうか?
あまり細かく説明していませんでしたが、MQTTブローカとして、tcpでの待ち受けとwsでの待ち受けの両方が必要です。
MongooseOSはtcp接続を前提としているようです。一方のブラウザのJavascriptは生のtcpは送受信できず、HTTPベースのWebSocketでの接続が必要となるためです。
ここら辺も以下に記載していますので、参考にしてください。
AWS IoTにMosquittoをブリッジにしてつなぐ
MQTTブローカが待ち受けるポート番号として、tcpは1883、wsは1884を想定しています。
ちなみに、以下のトピック名にPublishするようにしています。
人感センサのイベント:/mongoose/motion
マルチボタンのイベント:/mongoose/mouse
ブラウザのJavascript側は、両方を待ち受けたいため、/mongoose/# でSubscribeします。
ブラウザのJavascriptからHTTP Postする
ソースコードを見ていただくのがよいと思います。
HTMLではBootstrap3を使っています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<title>Mongoose + パイ専ボード</title>
<script src="js/methods_utils.js"></script>
<script src="js/vue_utils.js"></script>
<script src="js/mongoose.js"></script>
<script src="js/bme280.js"></script>
<script src="js/dht12.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="top" class="container">
<h1>Mongoose + パイ専ボード</h1>
<label>connected</label> {{connected}}
<br>
<div v-if="!connected">
<label>target_url</label> <input type="text" class="form-control" v-model="target_url">
<label>forward_url</label> <input type="text" class="form-control" v-model="forward_url">
<label>mqtt_url</label> <input type="text" class="form-control" v-model="mqtt_url">
<button class="btn btn-primary" v-on:click="connect_mongoose()">connect</button>
</div>
<h3>Environment Sensor</h3>
<h4>DHT12</h4>
<div v-if="dht12_data">
<label>humidity</label> {{dht12_data.humidity}}<br>
<label>temperature</label> {{dht12_data.temperature}}<br>
</div>
<h4>BME280</h4>
<div v-if="bme280_data">
<label>temperature_C</label> {{bme280_data.temperature_C}}<br>
<label>pressure_hPa</label> {{bme280_data.pressure_hPa}}<br>
</div>
<button class="btn btn-primary" v-on:click="update_envdata()">update</button>
<br>
<h3>Motion Sensor</h3>
<label>motion_detected</label> {{motion_detected}}<br>
<h3>Button</h3>
<label>button1_detected</label> {{button1_detected}}<br>
<label>button2_detected</label> {{button2_detected}}<br>
<div class="modal fade" id="progress">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{progress_title}}</h4>
</div>
<div class="modal-body">
<center><progress max="100" /></center>
</div>
</div>
</div>
</div>
</div>
<script src="js/start.js"></script>
</body>
Javascriptです。Vue.jsを使っています。
'use strict';
var mongoose = null;
var bme280 = null;
var dht12 = null;
var mqtt_client = null;
const target_url = "http://XXX.XXX.XXX.XXX";
const forward_url = 'RPCを転送するサーバのURL';
const mqtt_url = "MQTTブローカのURL(WebSocket)";
const PIN_BUTTON1 = 4;
const PIN_BUTTON2 = 21;
const PIN_LED = 25;
const PIN_MOTION = 16;
var vue_options = {
el: "#top",
data: {
progress_title: '',
target_url: target_url,
forward_url: forward_url,
mqtt_url: mqtt_url,
connected: false,
dht12_data: null,
bme280_data: null,
motion_detected: false,
button1_detected: false,
button2_detected: false,
},
computed: {
},
methods: {
update_envdata: async function(){
this.dht12_data = await dht12.readSensorData();
// console.log(this.dht12_data);
this.bme280_data = await bme280.readSensorData();
// console.log(this.bme280_data);
},
mqtt_onMessagearrived: async function(message){
var topic = message.destinationName;
console.log(topic);
switch(topic){
case "/mongoose/motion": {
var body = JSON.parse(message.payloadString);
console.log(body[0]);
this.motion_detected = (body[0].value != 0);
if( this.motion_detected)
await mongoose.rpcCall("/GPIO.write", { pin: PIN_LED, value: 1 } );
break;
}
case "/mongoose/button": {
var body = JSON.parse(message.payloadString);
for( var i = 0 ; i < body.length ; i++ ){
switch(body[i].pin){
case PIN_BUTTON1:
this.button1_detected = ( body[i].value == 0 );
if( body[i].value == 0 )
await mongoose.rpcCall("/GPIO.write", { pin: PIN_LED, value: 0 } );
break;
case PIN_BUTTON2:
this.button2_detected = ( body[i].value == 0 );
if( body[i].value == 0 )
await mongoose.rpcCall("/GPIO.Toggle", { pin: PIN_LED } );
break;
default:
console.log("Unknown pin");
break;
}
}
break;
}
default:
console.log('Unknown topic');
break;
}
},
mqtt_onConnectionLost: function(errorCode, errorMessage){
console.log("MQTT.onConnectionLost", errorCode, errorMessage);
},
mqtt_onConnect: async function(){
console.log("MQTT.onConnect");
mqtt_client.subscribe("/mongoose/#");
await mongoose.rpcCall("/Motion.setup", { pin: PIN_MOTION } );
await mongoose.rpcCall("/Button.setup", { pin_list: [PIN_BUTTON1, PIN_BUTTON2] } );
await mongoose.rpcCall("/GPIO.write", { pin: PIN_LED, value: 0 } );
this.connected = true;
await mongoose.rpcCall("/Motion.setEvent", { interval: 500 } );
await mongoose.rpcCall("/Button.setEvent", { interval: 200 } );
/*
var ledbar = new Grove_LED_Bar(mongoose, 21, 4, 0, 10);
ledbar.setLevel(3);
*/
/*
bme680 = new Bme680(mongoose);
await bme680.initialize();
console.log(await bme680.getSensorData());
*/
/*
tsl2561 = new TSL2561_CalculateLux(mongoose);
await tsl2561.init();
console.log(await tsl2561.readVisibleLux());
*/
},
connect_mongoose: async function(){
mongoose = new Mongoose({ target_url: this.target_url, forward_url: this.forward_url } );
await mongoose.rpcCall("/GPIO.write", { pin: PIN_LED, value: 1 } );
bme280 = new BME280(mongoose);
await bme280.initialize();
dht12 = new DHT12(mongoose);
this.update_envdata();
mqtt_client = new Paho.MQTT.Client(this.mqtt_url, "browser");
mqtt_client.onMessageArrived = this.mqtt_onMessagearrived;
mqtt_client.onConnectionLost = this.mqtt_onConnectionLost;
mqtt_client.connect({
onSuccess: this.mqtt_onConnect
});
}
},
created: function(){
},
mounted: function(){
proc_load();
}
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );
以下の部分は環境に合わせて変更してください。
const target_url = "ESP32のURL";
const forward_url = 'RPCを転送するサーバのURL';
const mqtt_url = "MQTTブローカのURL(WebSocket)";
以下設定例です。
const target_url = "http://XXX.XXX.XXX.XXX";
const forward_url = 'http://localhost:10080/mongoose';
const mqtt_url = "ws://test.sample.com:1884/";
以下は、デバイスを接続しているESP32のGPIO番号を指定してください。
const PIN_BUTTON1 = 4;
const PIN_BUTTON2 = 21;
const PIN_LED = 25;
const PIN_MOTION = 16;
簡単に動作の説明をします。
connectボタンを押下すると、connet_mongoose()が呼ばれます。
RPCを呼び出していったんLEDを点灯させます。
そして、RPCのI2Cを呼び出して、BME280とDHT12の初期化をし、update_envdata()で環境情報を取得し、表示します。
その一方で、MQTTブローカに接続を試みます。
MQTTブローカに接続が完了すると、mqtt_onConnect()が呼び出されます。
まずは、MQTTブローカにSubscribeします。
そして、RPC呼び出しで、人感センサとマルチボタンのGPIO番号を設定したのち、LEDを消灯させ、人感センサとマルチボタンの監視の開始をRPCで依頼します。
以上で準備完了です。
ESP32側では、人感センサとマルチボタンの監視が続けていますので、感知すると、MQTTブローカにPublishします。
ブラウザ側ではSubscribeしているので、 メッセージを受け取ると、mqtt_onMessagearrived()が呼び出されます。
その中で、LEDを点灯したり、消灯したりしているわけです。
環境センサのデバイスドライバの作成
人感センサやマルチボタン、LEDは、GPIOの制御で済んでいましたが、環境センサは、I2Cであり、センサ特有のコマンドを送信する必要があります。
そのためのドライバを作っておきました。
1つの環境センサですが、中には2つのI2Cデバイスが存在するため、それぞれごとにドライバを作成しています。
- bme280.js
- dht12.js
前者は、以下を参考にさせていただきました。
もともとNode.js用に書かれていたものを、Javascriptで動くように改造しています。
skylarstein/bme2800-sensor
https://github.com/skylarstein/bme280-sensor
後者は、特に難しくなく、以下のDataSheetに記載されているレスポンスフォーマットを見たら理解できました。
http://www.robototehnika.ru/file/DHT12.pdf
HTTP PostでRPC呼び出しする部分は以下にまとめています。
obnizからの移植だったり、ラズベリパイ向けのnpm i2c-busを使ったデバイスドライバからの移植だったり、Arduino向けのソースコードの移植をすることが多いため、それ用のなんちゃって互換I/Fも作っています。
'use strict';
class Mongoose{
constructor(params){
this.params = params;
this.i2c0 = new I2C(this);
this.io0 = new GPIO(this);
this.i2c = new I2C_Bus(this.i2c0);
this.Wire = new Arduino_Wire(this.i2c0);
this.GPIO = new Arduino_GPIO(this.io0);
}
async rpcCall(url, body){
const headers = new Headers( { "Content-Type" : "application/json" } );
body.target_url = this.params.target_url + "/rpc" + url;
return fetch(this.params.forward_url, {
method : 'POST',
body : JSON.stringify(body),
headers: headers
})
.then((response) => {
if( !response.ok )
throw 'response is not ok.';
return response.json();
});
}
}
class Arduino_GPIO{
/* for arduino I/F */
constructor(gpio){
this.gpio = gpio;
this.INPUT = 0;
this.OUTPUT = 1;
this.LOW = 0;
this.HIGH = 1;
this.DEFAULT = 0;
}
pinMode(pin, mode){
// do nothing
}
digitalWrite(pin, value){
return this.gpio.output(pin, value != this.LOW);
}
async digitalRead(pin){
return this.gpio.inputWait(pin)
.then(value =>{
return (value == 0) ? this.LOW : this.HIGH;
});
}
}
class Arduino_Wire{
/* for arduino I/F */
constructor(i2c){
this.i2c = i2c;
}
beginTransmission(deviceAddress){
this.write_address = deviceAddress;
this.write_buffer = [];
}
write(value){
this.write_buffer.push(value);
}
endTransmission(){
return this.i2c.write(this.write_address, this.write_buffer);
}
requestFrom(deviceAddress, len){
this.read_running = true;
return this.i2c.readWait(deviceAddress, len)
.then(buf =>{
this.read_buffer = buf;
this.read_running = false;
});
}
available(){
if( !this.read_running )
return this.read_buffer.length;
else
return 0;
}
read(){
return this.read_buffer.shift();
}
}
class I2C_Bus{
/* for npm i2c-bus I/F */
constructor(i2c){
this.i2c = i2c;
}
openSync(params){
return this;
}
closeSync(){
// do nothing
}
writeByte(addr, cmd, byte, cb){
this.i2c.write(addr, [cmd, byte])
.then(() =>{
cb(null);
});
}
readByte(addr, cmd, cb){
this.i2c.write(addr, [cmd])
.then(() =>{
return this.i2c.readWait(addr, 1);
})
.then(buf =>{
cb(null, buf[0]);
});
}
async readByteSync(addr, cmd){
return new Promise((resolve, reject) =>{
this.i2c.write(addr, [cmd])
.then(() =>{
return this.i2c.readWait(addr, 1);
})
.then(buf =>{
resolve(buf[0]);
})
.catch(error =>{
reject();
});
});
}
async readWordSync(addr, cmd){
return new Promise((resolve, reject) =>{
this.i2c.write(addr, [cmd])
.then(() =>{
return this.i2c.readWait(addr, 2);
})
.then(buf =>{
resolve(buf[0] << 8 | buf[1]);
})
.catch(error =>{
reject();
});
});
}
readI2cBlock(addr, cmd, len, buffer, cb){
return this.i2c.write(addr, [cmd])
.then(() =>{
return this.i2c.readWait(addr, len);
})
.then(buf =>{
for( var i = 0 ; i < buf.length ; i++ )
buffer[i] = buf[i];
cb(null, len, buffer);
});
}
readI2cBlockSync(addr, cmd, len, buffer){
return this.i2c.write(addr, [cmd])
.then(() =>{
return this.i2c.readWait(addr, len);
})
.then(buf =>{
for( var i = 0 ; i < buf.length ; i++ )
buffer[i] = buf[i];
return buf.length;
});
}
}
class GPIO{
/* for obniz gpio I/F */
constructor(parenet){
this.parenet = parenet;
}
async output(pin, value){
var params = {
pin: pin,
value: value ? 1 : 0
};
return this.parenet.rpcCall('/GPIO.Write', params );
}
async inputWait(pin){
var params = {
pin: pin
};
return this.parenet.rpcCall('/GPIO.Read', params )
.then( json =>{
return json.value;
})
}
}
class I2C{
/* for obniz i2c I/F */
constructor(parent){
this.parent = parent;
}
async start(params){
// do nothing
}
async end(){
// do nothing
}
async write(addr, data){
var params = {
addr: addr,
data_hex: byteAry2hexStr(data)
};
return this.parent.rpcCall('/I2C.Write', params );
}
async readWait(addr, len){
var params = {
addr: addr,
len: len
};
return this.parent.rpcCall('/I2C.Read', params )
.then(json =>{
return hexStr2byteAry(json.data_hex);
});
}
}
function do_post_forward(forward_url, url, body){
const headers = new Headers( { "Content-Type" : "application/json" } );
body.target_url = url;
return fetch(forward_url, {
method : 'POST',
body : JSON.stringify(body),
headers: headers
})
.then((response) => {
if( !response.ok )
throw 'response is not ok.';
return response.json();
});
}
function byteAry2hexStr(bytes, sep = '', pref = '') {
if( bytes instanceof ArrayBuffer )
bytes = new Uint8Array(bytes);
if( bytes instanceof Uint8Array )
bytes = Array.from(bytes);
return bytes.map((b) => {
var s = b.toString(16);
return pref + (b < 0x10 ? '0'+s : s);
})
.join(sep);
}
function hexStr2byteAry(hexs, sep = '') {
hexs = hexs.trim(hexs);
if( sep == '' ){
var array = [];
for( var i = 0 ; i < hexs.length / 2 ; i++)
array[i] = parseInt(hexs.substr(i * 2, 2), 16);
return array;
}else{
return hexs.split(sep).map((h) => {
return parseInt(h, 16);
});
}
}
以上です。