Help us understand the problem. What is going on with this article?

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

以前、以下の記事で、Mongoose OSとESP32を使ってみました。
 Mongoose OSでESP32を利活用する

利活用するといいながら、具体的な利用例を示せていなかったので、実際にシステムを組んでみました。

今回作るシステムは、人感検知システムです。
とはいっても、画像処理やら警報システムやらがある高度なものではなく、人感センサをESP32につないで、人を検知したらLEDを点灯するだけの単純なものです。
以下の技術を組み合わせていますので、それら使いこなして勉強するのが本当の目的です。

  • Mongoose OSで、ESP32のGPIOやI2Cを制御します。
  • Mongoose OSで、I2Cを使って環境センサ(BME280・DHT12)を制御します。
  • Mongoose OSで、WebサーバからのHTTP Postを受け取り、処理結果を返します。
  • Mongoose OSからMQTTブローカにPublishします。
  • ブラウザからMQTTブローカにSubscribeします。
  • ブラウザからWebサーバにHTTP Postし、さらにMongoose OSにHTTP Postを転送します。

絵にするとこんな感じです。

image.png

MQTTブローカには、Mosquittoを、MQTTクライアントには、PahoのJavascriptライブラリを使いました。
それから、ESP32には、手元にあった「パイ専ボード」を使いました。(なかなか使う機会がなく、ずっと眠っていました。。。)(あっ、もちろんESP32であれば何でもよいです)

パイ専ボード
 https://info.nikkeibp.co.jp/media/RAS/atcl/mag/062200015/

ラズベリパイにつないで使うものなので、それにはラズベリパイ ZERO W を使いました。ちなみに、MongooseOSを書き込むためにラズベリパイを使っているのであって、パイ専用ボードのESP32に書き込んだ後は使っていません(電源供給のみ)。

Mosquittoのブローカを立ち上げる必要があります。ぜひ以下を参照してください。
 AWS IoTにMosquittoをブリッジにしてつなぐ

Eclipse Paho JavaScript Client
 https://www.eclipse.org/paho/clients/js/

Mongoose OS
 https://mongoose-os.com/docs/mongoose-os/quickstart/setup.md

ブラウザからはこんな感じに見えます。

<接続前>
image.png

<接続後>
image.png

ソースコード一式は以下です。
 https://github.com/poruruba/mongoose_test

デモページは以下です。
 https://poruruba.github.io/mongoose_test/web_server/mongoose_test/public/pisen/

ちょっと長くなりそうだったので、2回に分けます。
今回1回目は、ESP32にMongoose OSを書き込んで、外部から呼び出せるようにRPCエンドポイントを作ります。
第二回はこちら。
 Mongoose OSを本当に使いこなす(2/2):ESP32のRPCエンドポイントを呼び出す

システムの仕様

ブラウザを立ち上げて、Webサーバに接続することで、人感検知システムが起動します。
一方、ESP32のパイ専ボードは、事前に立ち上げておき、ネットワーク接続されている状態にしておきます。

ESP32は、常に人感センサを使って、検出を継続します。人を検出すると、MQTTを介してブラウザに通知され、ブラウザのJavascriptからESP32にLED点灯を要求し、LEDが点灯します。
また、ESP32は、常にボタンの押下の検出も継続しますので、青いボタンを押下すると、MQTTを介してブラウザに通知され、ブラウザのJavascriptからESP32にLED消灯を要求し、LEDが消灯します。

人感検知には直接は関係しませんが、ブラウザのJavascriptからの要求で、ESP32に接続された環境センサから環境情報を取得し、ブラウザに表示します。また、赤いボタンを押下するたびに、LEDの点灯・消灯が切り替わります。

ESP32にMongoose OSでRPCエンドポイントを作る

細かな話に入る前に、コンパイル時に固定値となる設定値について説明します。
以下の値は、コンパイル時に決めるため、実行時に変更することはしません。

  • MQTTブローカのURL
  • ESP32のI2CのGPIOポート番号
  • WiFiアクセスポイントのSSID
  • WiFiアクセスポイントのパスワード

環境に合わせて変更してください。

それらは、mos.ymlファイルに指定します。
SPIやPWM、ADC、mDashなどは、お好みに応じてコメントアウトしてください。

mos.yaml
author: mongoose-os
description: A JS-enabled demo Mongoose OS firmware
# arch: PLATFORM
version: 1.0
manifest_version: 2017-05-18
libs_version: ${mos.version}
modules_version: ${mos.version}
mongoose_os_version: ${mos.version}

config_schema:
  - ["wifi.ap.enable", false]
  - ["wifi.sta.ssid", "WiFiアクセスポイントのSSID"]
  - ["wifi.sta.pass", "WiFiアクセスポイントのパスワード"]
  - ["wifi.sta.enable", true]
  - ["mqtt.server", "MQTTブローカのURL"]
  - ["mqtt.enable", true]
  - ["i2c.enable", true]
  - ["spi.enable", true]
  - ["i2c.sda_gpio", ESP32のI2CのGPIOポート番号(SDA)]
  - ["i2c.scl_gpio", ESP32のI2CのGPIOポート番号(SCL)]

tags:
  - js

filesystem:
  - fs

libs:
  - origin: https://github.com/mongoose-os-libs/boards
  - origin: https://github.com/mongoose-os-libs/ca-bundle
  - origin: https://github.com/mongoose-os-libs/core
  - origin: https://github.com/mongoose-os-libs/dash
  - origin: https://github.com/mongoose-os-libs/http-server
  - origin: https://github.com/mongoose-os-libs/rpc-common
  - origin: https://github.com/mongoose-os-libs/rpc-service-config
  - origin: https://github.com/mongoose-os-libs/rpc-service-fs
  - origin: https://github.com/mongoose-os-libs/rpc-service-gpio
  - origin: https://github.com/mongoose-os-libs/rpc-service-i2c
  - origin: https://github.com/mongoose-os-libs/rpc-service-ota
  - origin: https://github.com/mongoose-os-libs/rpc-service-wifi
  - origin: https://github.com/mongoose-os-libs/rpc-loopback
  - origin: https://github.com/mongoose-os-libs/rpc-mqtt
  - origin: https://github.com/mongoose-os-libs/rpc-uart
  - origin: https://github.com/mongoose-os-libs/rpc-ws
  - origin: https://github.com/mongoose-os-libs/sntp
  - origin: https://github.com/mongoose-os-libs/mjs
  - origin: https://github.com/mongoose-os-libs/spi
  - origin: https://github.com/mongoose-os-libs/i2c
  - origin: https://github.com/mongoose-os-libs/mqtt
  - origin: https://github.com/mongoose-os-libs/adc
  - origin: https://github.com/mongoose-os-libs/pwm
#  - origin: https://github.com/mongoose-os-libs/fstab
#  - origin: https://github.com/mongoose-os-libs/provision

さて、ESP32側に以下のRPCエンドポイントを作成します。

・Button.setup:マルチボタンで使うESP32のGPIOポート番号を指定します。
・Button.setEvent:マルチボタンの監視間隔を指定し、監視を開始します。
・Motion.setup:人感センサで使うESP32のGPIOポート番号を指定します。
・Motion.setEvent:人感センサの監視間隔を指定し、監視を開始します。
・GPIO.write:ESP32のGPIOを出力モードにし、出力値を設定します。
・GPIO.Toggle:ESP32のGPIOを出力モードにし、出力値をトグルします。
・I2C.Write:I2C通信でデータ列を出力します。
・I2C.Read:I2C通信でデータ列を取得します。

GPIOおよびI2Cは、Mongoose OSですでに定義されているのでそれを使います。
Button、Motionそれぞれソースファイルを分けて作成しました。

drv_button.js
load('api_gpio.js');
load('api_timer.js');
load('api_mqtt.js');
load('api_rpc.js');

let Button = {
    pin_list: [],
    prev_value: [],
    topic: "/mongoose/button",
    timer_id: -1,

    initialize: function(){
        RPC.addHandler('Button.setup', function(args, err, parent) {
            parent.setup(args.pin_list);
            return {};
        }, this);
    },

    setup: function(input_list){
        for( let i = 0 ; i < input_list.length ; i++ ){
            this.pin_list[i] = input_list[i];
            this.prev_value[i] = -1;
            GPIO.set_mode(input_list[i], GPIO.MODE_INPUT);
        }

        RPC.addHandler('Button.setEvent', function(args, err, parent) {
            if( args.topic )
                parent.topic = args.topic;
            if( args.interval <= 0 ){
                parent.stop_event();
                return { event: false };
            }else{
                parent.set_event(args.interval);
                return { event: true, pin_list: parent.pin_list };
            }
        }, this);
    },

    read: function(no){
        return GPIO.read(this.pin_list[no]);
    },

    set_event: function(interval){
        this.stop_event();

        this.timer_id = Timer.set(interval, Timer.REPEAT, function(parent) {
            let event_list = [];
            for( let i = 0 ; i < parent.pin_list.length ; i++ ){
                let val = parent.read(i);
                if( val !== parent.prev_value[i] ){
                    event_list.push({ pin: parent.pin_list[i], value: val} );
                    parent.prev_value[i] = val;
                }
            }
            if( event_list.length > 0 ){
                let res = MQTT.pub(parent.topic, JSON.stringify(event_list));
                if( res <= 0 )
                    print("MQTT.pub error");
            }
        }, this);
    },

    stop_event: function(){
        if( this.timer_id !== -1 ){
            Timer.del(this.timer_id);
            this.timer_id = -1;
        }
    },
};
drv_motion.js
load('api_gpio.js');
load('api_timer.js');
load('api_mqtt.js');
load('api_rpc.js');

let Motion = {
    pin: -1,
    prev_value: -1,
    topic: "/mongoose/motion",
    timer_id: -1,

    initialize: function(){
        RPC.addHandler('Motion.setup', function(args, err, parent) {
            parent.setup(args.pin);
            return {};
        }, this);
    },

    setup: function(input){
        this.pin = input;
        this.prev_value = -1;
        GPIO.set_mode(input, GPIO.MODE_INPUT);

        RPC.addHandler('Motion.setEvent', function(args, err, parent) {
            if( args.topic )
                parent.topic = args.topic;
            if( args.interval <= 0 ){
                parent.stop_event();
                return { event: false };
            }else{
                parent.set_event(args.interval);
                return { event: true, pin: parent.pin };
            }
        }, this);
    },

    read: function(){
        return GPIO.read(this.pin);
    },

    set_event: function(interval){
        this.stop_event();

        this.timer_id = Timer.set(interval, Timer.REPEAT, function(parent) {
            let event_list = [];
            let val = parent.read();
            if( val !== parent.prev_value ){
                event_list.push({ pin: parent.pin, value: val} );
                parent.prev_value = val;
            }
            if( event_list.length > 0 ){
                let res = MQTT.pub(parent.topic, JSON.stringify(event_list));
                if( res <= 0 )
                    print("MQTT.pub error");
            }
        }, this);
    },

    stop_event: function(){
        if( this.timer_id !== -1 ){
            Timer.del(this.timer_id);
            this.timer_id = -1;
        }
    },
};

あまり複雑なことはしていないので、ソースコードを見れば大体わかるかと思います。

RPC.addHandler で、RPCエンドポイントを作成しています。
少し特殊なことをしていまして、thisを渡しています。そうすることで、RPCのコールバック関数の中で、parentとしてアクセスすることができます。(なぜかアロー関数が使えなかったため)
returnで指定した戻り値がJSONとなってそのままRPCの呼び出しもとに、戻ります。

関数 set_event では、Mongoose OSのTimer.set の機能を使って、定期的に処理を実行します。JavascriptのsetInterval()と同じような機能です。その中で、前回の取得値を覚えておき、変化があったら、MQTTでPublishします。 MQTT.pub の部分です。Mongoose OSにすでにMQTTの機能があるので楽ちんです。

利用する機能に合わせて、JavascriptをLoadする必要がありますので、忘れないようにしてください。
例:load('api_mqtt.js')

メインのソースは以下です。

init.js
load('api_config.js');
load('api_dash.js');
load('api_events.js');
load('api_timer.js');
load('api_sys.js');

load('drv_button.js');
load('drv_motion.js');
//load('drv_angle.js');
//load('drv_pwm.js');

let state = {};  // Device state
let online = false;                               // Connected to the cloud?

Button.initialize();
Motion.initialize();
//Pwm.initialize();
//Angle.initialize();

// Update state every second, and report to cloud if online
Timer.set(10000, Timer.REPEAT, function() {
  state.uptime = Sys.uptime();
  state.ram_free = Sys.free_ram();
  print('online:', online, JSON.stringify(state));
}, null);

Event.on(Event.CLOUD_CONNECTED, function() {
  online = true;
}, null);

Event.on(Event.CLOUD_DISCONNECTED, function() {
  online = false;
}, null);

drv_button.jsとdrv_motion.jsで定義しておいたinitialize()を呼び出して準備しておきます。
あとは、クライアント側からRPCでButton.setupやButton.setEvent、Motion.setupやMotion.setEventを呼び出してもらうのを待つのみです。

Mongoose OSを パイ専ボードに書き込む

Mongoose OSのESP32への書き込み方は、以前の投稿の通りです。
ですが、パイ専ボードは、ラズベリーパイに挿して使うため、少し事情が違いますので、ここで補足しておきます。

私の場合、ラズベリーイパイには通常ディスプレイはつけていません。ですので、CUIで操作する必要があります。
また、mos.exeに相当するARM用の実行ファイルはないため、自身でコンパイルする必要があります。

まず最初にやるのは、ラズベリパイのGPIO端子にあるUARTを有効にすることです。
Windowsの場合は、たいていESP32ボードにUSB仮想COM変換チップがついていて、Windowsからは仮想COMポートに見えていたかと思います。パイ専ボードにはUSB仮想COM変換チップがなく、ESP32のUARTがラズベリパイのGPIOにあるUARTに直接つながります。

ここら辺が参考になりました。
 Raspberry Pi3 や Zero Wで オンボードのシリアル (UART) を使う (DietPiでの方法も補足)

次に、mosをコンパイルします。
ソースコード一式は以下にあります。

mongoose-os/mos
 https://github.com/mongoose-os/mos

先に手動でGo言語をインストールする必要があります。
OS標準では、v1.0.7ぐらいの古いバージョンがインストールされるので、v1.10以上をダウンロードしてインストールします。

wget https://dl.google.com/go/go1.10.3.linux-armv6l.tar.gz
sudo tar -C /usr/local -xvf go1.10.3.linux-armv6l.tar.gz
cd /usr/bin
sudo ln -s /usr/local/go/bin/go .
> sudo apt-get install build-essential git python3 libftdi-dev libusb-1.0-0-dev
> pkg-config 
> git clone https://github.com/mongoose-os/mos
> cd mos
> make deps
> make

これで、mos/mos という実行ファイルが出来上がります。
一点、注意事項があります。
RAMサイズが少ない環境ではメモリ不足でコンパイルに失敗します。
たとえば、ラズベリパイ ZEROは512MBなのですが、失敗します。なので、例えば、同じARM CPUであるラズベリパイ 3 Model B でコンパイルし、mos だけZERO環境にコピーしてきました。
あと、実行権を与えておきましょう

> sudo chmod +x ~/.mos/mos

後は以前と同じです

> cd
> mkdir mos
> cd mos
> mos clone https://github.com/mongoose-os-apps/demo-js app1
> cd app1

さきほど作った、mos.ymlやらinit.jsやらdrv_button.jsやdrv_motion.jsなどなどを上書きコピーします。
mos.ymlの以下のあたりの変更をお忘れなく。

init.yml
  - ["wifi.sta.ssid", "WiFiアクセスポイントのSSID"]
  - ["wifi.sta.pass", "WiFiアクセスポイントのパスワード"]
  - ["mqtt.server", "MQTTブローカのURL"]
  - ["i2c.sda_gpio", SP32のI2CのGPIOポート番号(SDA)]
  - ["i2c.scl_gpio", SP32のI2CのGPIOポート番号(SCL)]
> mos build --platform esp32
> mos flash --port /dev/serial0
> mos console --port /dev/serial0

/dev/serial0 というのが、ラズベリパイのGPIOにあるUARTです。

RPCを試してみる

mos consoleで、いろいろ実行の様子が流れていると思います。
その中に、WiFiのアクセスポイントに接続してIPアドレスが割り当たっているのがわかるかと思います。(Linux上だと、文字に色がついていて見つけやすい)
ESP32のIPアドレスなので、それを覚えておきます。以降、XXX.XXX.XXX.XXX としています。

JSONをHTTP Postしたいので、例えば、Postmanを使って実行してみましょう。

<I2Cデバイスの一覧取得>
POST http://XXX.XXX.XXX.XXX/rpc/I2C.Scan
Content-Type: application/json
body: {}
→ これで、ESP32に接続されたI2Cデバイスのスレーブアドレスの配列が取得されます。

<LEDの点灯>
POST http://XXX.XXX.XXX.XXX/rpc/GPIO.write
Content-Type: application/json
body: { “pin”: YY, value: 1 }

※YYは、LEDが接続されたGPIOポート番号です。
※1が点灯、0が消灯です。

<LEDのトグル>
POST http://XXX.XXX.XXX.XXX/rpc/GPIO.Toggle
Content-Type: application/json
body: { “pin”: YY }

※YYは、LEDが接続されたGPIOポート番号です。

<人感センサの初期化>
POST http://XXX.XXX.XXX.XXX/rpc/Motion.setup
Content-Type: application/json
body: { “pin”: YY }

※YYは、人感センサの信号線に接続されたGPIOポート番号です。

<マルチボタンの初期化>
POST http://XXX.XXX.XXX.XXX/rpc/Button.setup
Content-Type: application/json
body: { “pin_list”: [YY, ZZ] }

※YY、ZZは、マルチボタンの信号線に接続されたGPIOポート番号です。複数のボタンを接続している場合があると思いますので、それを配列で指定します。

<人感センサの監視開始>
POST http://XXX.XXX.XXX.XXX/rpc/Motion.setEvent
Content-Type: application/json
body: { “interval”: 500 }

※数字は、監視間隔をmsecで指定します。任意の値に変更可能です。

<マルチボタンの監視開始>
POST http://XXX.XXX.XXX.XXX/rpc/Button.setEvent
Content-Type: application/json
body: { “interval”: 200 }

※数字は、監視間隔をmsecで指定します。任意の値に変更可能です。

とりあえず、今回はここまでです。

以上

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away