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

以前、以下の記事で、Mongoose OSとESP32を使ってみました。
　[Mongoose OSでESP32を利活用する](https://qiita.com/poruruba/items/5cd1515b30e9ba678885 )

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

今回作るシステムは、人感検知システムです。
とはいっても、画像処理やら警報システムやらがある高度なものではなく、人感センサを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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/9be04374-d24b-facf-4d01-620566ffb867.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をブリッジにしてつなぐ](https://qiita.com/poruruba/items/084317e01b9fababc02e )

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/8e0098ea-6760-fbed-c603-dca61b14d094.png)

＜接続後＞
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/422e9dc3-b67e-219b-08f6-3f4c98b42fd2.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エンドポイントを呼び出す](https://qiita.com/poruruba/items/b4bfa0191d293a3f1aa0)

# システムの仕様

ブラウザを立ち上げて、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それぞれソースファイルを分けて作成しました。

```js: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;
		}
	},
};
```

```js: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')

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

```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での方法も補足)](https://qiita.com/hidenorly/items/f6ffe79fb6f81740eff3)

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

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

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

```sh
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 .
```

```console
> 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環境にコピーしてきました。
あと、実行権を与えておきましょう

```console
> sudo chmod +x ~/.mos/mos
```

後は以前と同じです

```console
> 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)]
```

```console
> 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で指定します。任意の値に変更可能です。

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

以上