LoginSignup
14
14

More than 3 years have passed since last update.

Mongoose OSでESP32を利活用する

Last updated at Posted at 2019-09-14

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

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

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

(参考)以下の投稿もご参考まで。
 Mongoose OSを本当に使いこなす(1/2):ESP32にRPCエンドポイントを作る

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

まずは、キーボードから、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も不要なコードがたくさんあるので削除します。(今後参考にはなりますが)

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に入れてみた

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

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

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で指定してください。

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を指定してください。

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

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

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で立ち上げた後でも、いろんな用途に使えそうです。

以上

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