Mongoose OSを本当に使いこなす(2/2)：ESP32のRPCエンドポイントを呼び出す

以前、以下の記事で、Mongoose OSとESP32を使って、RPCエンドポイントを立ち上げました。

　[Mongoose OSを本当に使いこなす(1/2)：パイ専ボードにRPCエンドポイントを作る](https://qiita.com/poruruba/items/76ac34db408213cdf80d )

今回は、それをブラウザのJavascriptから呼び出します。
下の絵のうちの、WebサーバとWebブラウザの部分です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f54d1163-83ec-039d-ef58-3102e3218c77.png)

ソースコード一式は以下です。
　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を転送するようにします。

```js:index.js
'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
以下を実行します。

```shell
> 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をブリッジにしてつなぐ](https://qiita.com/poruruba/items/084317e01b9fababc02e )

MQTTブローカが待ち受けるポート番号として、tcpは1883、wsは1884を想定しています。

ちなみに、以下のトピック名にPublishするようにしています。

人感センサのイベント：/mongoose/motion
マルチボタンのイベント：/mongoose/mouse

ブラウザのJavascript側は、両方を待ち受けたいため、/mongoose/# でSubscribeします。

# ブラウザのJavascriptからHTTP Postする

ソースコードを見ていただくのがよいと思います。
HTMLではBootstrap3を使っています。

```html:index.html
<!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を使っています。

```js:start.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も作っています。

```js:mongoose.js
'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);
      });
  }
}
```


以上です。