0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-09-25

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

 Mongoose OSを本当に使いこなす(1/2):パイ専ボードにRPCエンドポイントを作る

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

image.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を転送するようにします。

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
以下を実行します。

> 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を使っています。

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を使っています。

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も作っています。

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

以上です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?