以前、以下の記事で、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を転送します。
絵にするとこんな感じです。
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
ブラウザからはこんな感じに見えます。
ソースコード一式は以下です。
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などは、お好みに応じてコメントアウトしてください。
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それぞれソースファイルを分けて作成しました。
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;
}
},
};
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')
メインのソースは以下です。
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の以下のあたりの変更をお忘れなく。
- ["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で指定します。任意の値に変更可能です。
とりあえず、今回はここまでです。
以上