obnizOSをOLED付ESP32に入れてみた

先日、obnizOSがリリースされたので、さっそく手元にあったESP32に入れてみました。

obnizOS
　https://obniz.io/ja/doc/obnizos/os_overview

ちょうどそのESP32には、OLEDディスプレイがついているので、それを制御してみようと思います。
(本当はM5Stick-Cでやろうとしたのですが、CoreDumpを吐いて動きませんでした。。。)

私が買っていたのはこれです。
　[ESP32 OLED開発ボードWiFi Bluetoothデュアルコアディスプレイモジュール、ケーブルESP WROOM 32 Wemos Lolin for Arduino](https://www.amazon.co.jp/ESP32-OLED%E9%96%8B%E7%99%BA%E3%83%9C%E3%83%BC%E3%83%89WiFi-Bluetooth%E3%83%87%E3%83%A5%E3%82%A2%E3%83%AB%E3%82%B3%E3%82%A2%E3%83%87%E3%82%A3%E3%82%B9%E3%83%97%E3%83%AC%E3%82%A4%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%80%81%E3%82%B1%E3%83%BC%E3%83%96%E3%83%ABESP-WROOM-Arduino/dp/B07GGS9RH2/ref=pd_lpo_sbs_328_img_1?_encoding=UTF8&psc=1&refRID=VGV966VBH9VTXRQG0PRJ)

obnizOSのインストールは、他の方の記事を参考にさせていただきました。例えば、
　[obnizOSをM5Stack(M5StickC)にインストールする手順](https://qiita.com/iizuka2019/items/4736796f4d07b82c677e)
　[obniz OSをESP32(NefryBT)に入れて動かそう！](https://qiita.com/wamisnet/items/f0c939ef9d7ad40d85d7)

# ESP32 OLED開発ボードとOLEDディスプレイについて

ESP32 OLED開発ボードには、WeMosという刻印があり、それをもとに調べてみると、以下の参考ページがありました。
　[Wemos Lolin board (ESP32 with 128x64 SSD1306 I2C OLED display)](https://github.com/FablabTorino/AUG-Torino/wiki/Wemos-Lolin-board-(ESP32-with-128x64-SSD1306-I2C-OLED-display))

ESP32 OLED開発ボードについているディスプレイは以下だそうです。
　[SSD1306](https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf)

128x64ドットのモノクロです。
接続I/Fはいろいろあるのですが、その中のI2Cで接続されているようです。
スレーブアドレスは0x3Cでした。

PINの関係がいまいちわからないのですが、以下にしたら動きました。
・SDA：5
・SCL：4

# SSD1306ドライバの作成

以下を参考にさせていただき、obnizに移植しました。

oled-js-pi
　https://github.com/juddflamm/oled-js-pi/blob/master/oled.js

以下、ソースコードです。

```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 = 16;

		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;
    
    /* initialze */
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.DISPLAY_OFF]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_DISPLAY_CLOCK_DIV, 0x80]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_MULTIPLEX, 0x3F]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_DISPLAY_OFFSET, 0x00]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_START_LINE]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.CHARGE_PUMP, 0x14]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.MEMORY_MODE, 0x00]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SEG_REMAP]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.COM_SCAN_DEC]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_COM_PINS, 0x12]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_CONTRAST, 0x8F]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_PRECHARGE, 0xF1]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.SET_VCOM_DETECT, 0x40]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.DISPLAY_ALL_ON_RESUME]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.NORMAL_DISPLAY]);
    this.obniz.i2c0.write(this.ADDRESS, [0x00, this.DISPLAY_ON]);
  
    this.mode = true;
  }
  
  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);
        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 > 127 || grey > 127.5)
      return 1;
    else
      return 0;
  }
}
```

obniz.displayの仕様に似せて作ってみました。
線の描画や文字列の描画機能はなく、canvasを使うことを前提にしています。
使うときには、以下のような流れになります。

```js
var wemos = new WeMos(obniz); /* これで初期化 */
wemos.display.clear(); /* 必要に応じて画面クリア */
const ctx = obniz.util.createCanvasContext(wemos.display.width, wemos.display.height); /* canvasを作成 */
/* ここでいろいろcanvasの描画 */
wemos.display.draw(ctx); /* OLEDに描画 */
```

# HTMLサンプル

ついでに、ブラウザから操作するHTMLページを載せておきます。

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

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

  <script src="https://unpkg.com/obniz@2.2.0/obniz.js" crossorigin="anonymous"></script>
  <script src="js/wemos.js"></script>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>obniz + WeMos</h1>
        <label>connected</label> {{connected}}<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>
```

```js:js/start.js
'use strict';

var obniz = new Obniz("【obnizのid】");
var wemos = null;

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

        connected: false,
        string: ''
    },
    computed: {
    },
    methods: {
        initialize: function(){
            obniz.onconnect = async () => {
                console.log('obniz.onconnect');

                this.connected = true;
                wemos = new WeMos(obniz);
                wemos.display.clear();
            };
            obniz.onclose = () =>{
                console.log('obniz.onclose');
                wemos = null;
                this.connected = false;
            };
        },
        print_string: function(){
            const ctx = obniz.util.createCanvasContext(wemos.display.width, wemos.display.height);
            
            ctx.fillStyle = "white";
            ctx.font = "20px Avenir";
            ctx.fillText(this.string, 0, 40);

            wemos.display.draw(ctx);
        },
        clear_screen: function(){
            wemos.display.clear();
        }
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

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

【obnizのid】の部分は、お手持ちのobnizデバイスに合わせてください。
その他、足りないソースは [こちら](https://github.com/poruruba/swagger_template/tree/master/public/js) です。

ブラウザから参照すると、obnizデバイスと接続されて、connectedがtrueになります。
stringのテキストボックスに適当な文字を入れて、「print_string」ボタンを押下すると、obnizのディスプレイにその文字が表示されます。
「clear_screen」ボタンを押下すると、ディスプレイがクリアされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2e4359d7-ce38-5d8d-62ef-177b5494102d.png)

以上