Mongoose OSでESP32を利活用する


ESP32にMongoose OSをいれて、Javascriptでいじったり、リモートからWebAPIでたたいたりします。
あまり需要はないのかもしれませんが、今後何かと使えそうなので、この機会に備忘録として残しておきます。

[Mongoose OS]
　https://mongoose-os.com/mos.html

（ちなみに、本当はM5Stick-Cに入れるつもりで調べたのですが、入れてみたら動作が不安定でしたので、またの機会にしたいと思います。）

(参考)以下の投稿もご参考まで。
　[Mongoose OSを本当に使いこなす(1/2)：ESP32にRPCエンドポイントを作る](https://qiita.com/poruruba/items/76ac34db408213cdf80d)

# Mongoose OSとは

・Javascriptで動かせます。
　Webサーバ上にJavascriptを置くObnizと違って、ESP32にJavascriptソースコードを書き込んで単独で動作します。C言語も書けます。
・代表的なIoTにすでに対応しています。
　AWS IoT、Microsoft Azure IoT、Google IoT Core など、すでに対応しています。
・RPCで呼び出されます。
　WebSocket、RESTful、MQTTなどで呼び出すことができるので、Obnizと同じように、サーバ側からJavascriptなどで制御できます。

# Mongoose OSを書き込む

基本的に以下に書いてある通りに進めれば、何も難しいことはなかったです。
　https://mongoose-os.com/docs/mongoose-os/quickstart/setup.md

以下から、mosツールをダウンロードします。以下はWindowsの場合です。
　https://mongoose-os.com/downloads/mos-release/win/mos.exe

例えば、以下に保存します。
　c:\mos\mos.exe
そして実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/7b11c885-1be1-3e15-0864-6744b440950b.png)

まずは、キーボードから、Ctrl-n を押します。そうすると、下の方に、

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

と入力されます。
app1は任意のアプリ名なので、好きな名前に変えます。
そうすると、c:\mos\app1というフォルダが作られて、ファイルがダウンロードされて、app1フォルダに移動してくれます。

左下のフォルダボタンを押すと、エクスプローラで開いてくれます。

次に、書き込むMongoose OSの内容をカスタマイズするために、mos.ymlを編集します。

```mos.yml
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:
  - ["i2c.enable", true]
  - ["i2c.sda_gpio", 5]
  - ["i2c.scl_gpio", 4]

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/fstab
  - origin: https://github.com/mongoose-os-libs/http-server
  - origin: https://github.com/mongoose-os-libs/provision
  - 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-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/rpc-loopback
  - origin: https://github.com/mongoose-os-libs/rpc-mqtt
  - 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/spi
  - origin: https://github.com/mongoose-os-libs/i2c
```

libsのところは、お好みで追加削除してください。
init.jsも不要なコードがたくさんあるので削除します。(今後参考にはなりますが）

```js:fs/init.js
load('api_config.js');
load('api_dash.js');
load('api_events.js');
load('api_gpio.js');
load('api_timer.js');
load('api_sys.js');

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

// 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);
```

あとは、以下を実行するだけです。
左上のポート番号とデバイスの種類を設定するのを忘れずに。

　mos build
　mos flash

これで、OSが書き込まれて、再起動します。右側のConsoleにその様子が出力されます。

WiFiのアクセスポイントは以下で設定できます。

　mos wifi [AP名] [パスワード]

mDashというサービス（ https://mdash.net ）を使う場合は以下を設定します。

　mos mdash-setup [Device ID] [Device token]

以下のところは、必要に応じて設定してください。SDAを5、SCLを4に設定しています。何も設定しないと、SDAが32、SCLが33になります。

　  - ["i2c.sda_gpio", 5]
　  - ["i2c.scl_gpio", 4]

以下のように、実行すると、

　mos call I2C.Scan

以下のように表示されて、I2Cデバイスが検出されているのがわかります。

　[
　　60
　]
　Command completed.

スレーブアドレス=60(0x3C)です。
これで、起動が完了し、Wifiにも接続されていると思いますので、準備完了です。

# I2C接続のOLEDディスプレイにつなげる

それじゃあ、fs/init.js にI2Cを操作するJavascriptを書きましょう、という方法もあるのですが、Javascriptを変更するたびに、mos build、mos flashするのはめんどうなので、Mongoose OSのRPCを使って、クライアントPCからリモートで操作してみます。

(参考)
　[obnizOSをOLED付ESP32に入れてみた](https://qiita.com/poruruba/items/a2f2fe115ff1d8b255c3)

I2C操作は、mongoose.jsにおいて、以下の関数にまとめました。

class Mongoose
　this.i2c0 = class I2C
　　this.write(addr, data)
　　this.readWait(addr, len)

上記のI2C操作関数を、OLEDディスプレイドライバwemos.jsの中で呼び出しています。

実際のI2C関数の中身はというと、ここで、Mongoose OSのRPCがCROSS Domain対応していないことがわかりました。。。
なので、いったんRESTful APIサーバに渡して、そこからMongoose OSのRPCを呼び出すようにしました。

サーバからESP32のRPCへの呼び出し方は単純です。

I2C Writeの場合
　{
　　	"addr": 60,
　　	"data_hex": "000102030405"
　}
を
　http://XXXXXXX/rpc/I2C.Write
にPOSTするだけです。
I2C Readの場合は、
　{
　　	"addr": 60,
　　	"len": 10
　}
を
　http://XXXXXXX/rpc/I2C.Read
にPOSTするだけです。

(参考情報)
https://mongoose-os.com/docs/mongoose-os/api/rpc/rpc-service-i2c.md

# ソース一式

クライアント側のWebページはこんな感じに見えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a42c8b22-1cb0-5de5-0891-110e2e3e1c73.png)

クライアント側ソースコードです。

```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 + WeMos</title>

  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="js/mongoose.js"></script>
  <script src="js/wemos.js"></script>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>Mongoose + WeMos</h1>
        <br>
        <label>string</label> <input type="text" class="form-control" v-model="string">
        <button class="btn btn-primary" v-on:click="print_string()">print_string</button>
        <button class="btn btn-primary" v-on:click="clear_screen()">clear_screen</button>


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

```start.js
'use strict';

var mongoose = null;
var wemos = null;

const device_url = "http://192.168.1.219";

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        string: '',
    },
    computed: {
    },
    methods: {
        connect_mongoose: async function(){
            mongoose = new Mongoose({ url: device_url } );
            wemos = new WeMos(mongoose);
            await wemos.display.clear();
        },
        print_string: function(){
            if( wemos == null ){
                alert('wemosと接続していません。');
                return;
            }

            try{
                var canvas = document.createElement('canvas');
                canvas.setAttribute("width", wemos.display.width.toString());
                canvas.setAttribute("height", wemos.display.height.toString());
                var ctx = canvas.getContext('2d');
                ctx.clearRect(0, 0, wemos.display.width, wemos.display.height);

                ctx.fillStyle = "white";
                ctx.font = "20px Avenir";
                ctx.fillText(this.string, 0, 40);

                wemos.display.draw(ctx);
            }catch( error ){
                alert(error);
            }
        },
        clear_screen: function(){
            if( wemos == null ){
                alert('wemosと接続していません。');
                return;
            }

            try{
                wemos.display.clear();
            }catch( error ){
                alert(error);
            }
        }
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        this.connect_mongoose();
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );
```

device_url には、ESP32のIPアドレスをURLで指定してください。

```js:mongoose.js
'use strict';

const forward_url = 'http://localhost:10080/mongoose';

class Mongoose{
	constructor(params){
		this.i2c0 = new I2C(params);
	}
}

class I2C{
	constructor(params){
    this.url = params.url;
  }
  
  async start(params){
  }

  async write(addr, data){
    var params = {
      addr: addr,
      data_hex: byteAry2hexStr(data)
    };
    return do_post_forward(this.url + '/rpc/I2C.Write', params );
  }

  async readWait(addr, len){
    var params = {
      addr: addr,
      len: len
    };
    return do_post_forward(this.url + '/rpc/I2C.Read', params )
    .then(ret =>{
      return hexStr2byteAry(ret.data_hex);
    });
  }

  async end(){
  }
}

function do_post_forward(url, body){
  const headers = new Headers( { "Content-Type" : "application/json" } );
  
  body.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);
      });
  }
}
```

forward_url には、RESTfulサーバを立ち上げたURLを指定してください。

```js:wemos.js
'use strict';

class WeMos{
	constructor(obniz){
		this.display = new WeMos_display(obniz);
	}
}

class WeMos_display{
	constructor(obniz, addr, sda, scl){
    this.obniz = obniz;

    this.ADDRESS = addr || 0x3C;
    this.SDA = sda || 5;
    this.SCL = scl || 4;
    
    this.obniz.i2c0.start({mode: "master", sda: this.SDA, scl: this.SCL, clock: 400000}); 

    this.WIDTH = 128;
    this.HEIGHT = 64;
    
    this.width = this.WIDTH;
    this.height = this.HEIGHT;
		
		this.mode = false;
		this.buffer = [];
		this.clear();
		
		this.TRANSFER_SIZE = 64;

		this.DISPLAY_OFF = 0xAE;
		this.DISPLAY_ON = 0xAF;
		this.SET_DISPLAY_CLOCK_DIV = 0xD5;
		this.SET_MULTIPLEX = 0xA8;
		this.SET_DISPLAY_OFFSET = 0xD3;
		this.SET_START_LINE = 0x00;
		this.CHARGE_PUMP = 0x8D;
		this.EXTERNAL_VCC = false;
		this.MEMORY_MODE = 0x20;
		this.SEG_REMAP = 0xA1; // using 0xA0 will flip screen
		this.COM_SCAN_DEC = 0xC8;
		this.COM_SCAN_INC = 0xC0;
		this.SET_COM_PINS = 0xDA;
		this.SET_CONTRAST = 0x81;
		this.SET_PRECHARGE = 0xd9;
		this.SET_VCOM_DETECT = 0xDB;
		this.DISPLAY_ALL_ON_RESUME = 0xA4;
		this.NORMAL_DISPLAY = 0xA6;
		this.COLUMN_ADDR = 0x21;
		this.PAGE_ADDR = 0x22;
		this.INVERT_DISPLAY = 0xA7;
		this.ACTIVATE_SCROLL = 0x2F;
		this.DEACTIVATE_SCROLL = 0x2E;
		this.SET_VERTICAL_SCROLL_AREA = 0xA3;
		this.RIGHT_HORIZONTAL_SCROLL = 0x26;
		this.LEFT_HORIZONTAL_SCROLL = 0x27;
		this.VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL = 0x29;
		this.VERTICAL_AND_LEFT_HORIZONTAL_SCROLL = 0x2A;

    new Promise(async (resolve, reject) =>{
      /* initialze */
      try{
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.DISPLAY_OFF]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_DISPLAY_CLOCK_DIV, 0x80]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_MULTIPLEX, 0x3F]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_DISPLAY_OFFSET, 0x00]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_START_LINE]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.CHARGE_PUMP, 0x14]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.MEMORY_MODE, 0x00]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SEG_REMAP]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.COM_SCAN_DEC]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_COM_PINS, 0x12]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_CONTRAST, 0x8F]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_PRECHARGE, 0xF1]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_VCOM_DETECT, 0x40]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.DISPLAY_ALL_ON_RESUME]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.NORMAL_DISPLAY]);
        await this.obniz.i2c0.write(this.ADDRESS, [0x00, this.DISPLAY_ON]);

        this.mode = true;

        resolve();
      }catch(error){
        reject(error);
      }
    });
  }
  
  async clear(){
    for( var y = 0 ; y < this.fl(this.HEIGHT / 8) ; y++ ){
      for( var x = 0 ; x < this.WIDTH ; x++ ){
        this.buffer[ y * this.WIDTH + x ] = 0x00;
      }
    }
  
    if( this.mode ){
      return this.update();
    }else{
      return Promise.resolve();
    }
  }
  
  async drawing(mode){
    this.mode = mode;
    if( this.mode ){
      return this.update();
    }else{
      return Promise.resolve();
    }
  }
  
  async raw(ary){
    for( var y = 0 ; y < this.HEIGHT ; y++ ){
      for( var x = 0 ; x < this.WIDTH ; x += 8 ){
        var val = ary[y * this.fl(this.WIDTH / 8) + this.fl(x / 8)];
        for( var i = 0 ; i < 8 ; i++ )
          this.put_pixel(x + i, y, (val & (0x01 << i)) != 0x00);
      }
    }

    if( this.mode ){
      return this.update();
    }else{
      return Promise.resolve();
    }
  }
  
  async draw(ctx){
      var img = ctx.getImageData(0, 0, this.WIDTH, this.HEIGHT);

    for (var y = 0; y < this.HEIGHT; y++ ) {
      for (var x = 0; x < this.WIDTH; x++) {
        var val = this.to_mono(img.data[(x + y * this.WIDTH) * 4], img.data[(x + y * this.WIDTH) * 4 + 1], img.data[(x + y * this.WIDTH) * 4 + 2], img.data[(x + y * this.WIDTH) * 4 + 3]);
        this.put_pixel(x, y, val);
      }
    }

    if( this.mode ){
      return this.update();
    }else{
      return Promise.resolve();
    }
  }
  
  put_pixel(x, y, val){
    var temp = this.buffer[this.fl(y / 8) * this.WIDTH + x];
    var index = y % 8;
    if(val)
      temp |= 0x01 << index;
    else
      temp &= (~(0x01 << index)) & 0xff;
    this.buffer[this.fl(y / 8) * this.WIDTH + x] = temp;
  }
  
  async update(){
    var ret = await this.obniz.i2c0.readWait(this.ADDRESS, 1);
    if( (ret[0] >> 7) & 0x01 != 0x00 ){
      console.log('busy');
      return;
    }
    
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.COLUMN_ADDR, 0, this.WIDTH - 1]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.PAGE_ADDR, 0, this.fl(this.HEIGHT / 8) - 1]);
    for( var y = 0 ; y < this.fl(this.HEIGHT / 8) ; y++ ){
      for( var x = 0 ; x < this.WIDTH ; x += this.TRANSFER_SIZE ){
        var data = this.buffer.slice( y * this.WIDTH + x, y * this.WIDTH + x + this.TRANSFER_SIZE);
        data.unshift(0x40);
        await this.obniz.i2c0.write(this.ADDRESS, data);
      }
    }
  }

  fl(f){
    return Math.floor(f);
  }
  
  to_mono(r, g, b, a){
    var grey = r * 0.299 + g * 0.587 + b * 0.114;
    if( a > 128 || grey > 128.5)
      return 1;
    else
      return 0;
	}
}
```

サーバ側ソースコードも載せておきます。

```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.url, {
      method : 'POST',
      body : JSON.stringify(body),
      headers: headers
  })
  .then((response) => {
      if( !response.ok )
          throw 'response is not ok.';
      return response.json();
  })
  .then(json =>{
    return new Response(json);
  });
};
```

# 最後に

Mongoose OSは、いろんな機能が実装されていて、それらをRPCでremote呼び出しできるようなので、いったんESP32で立ち上げた後でも、いろんな用途に使えそうです。

以上