LoginSignup
5
4

More than 3 years have passed since last update.

QuickJSでお手軽ESP32+Javascript実行環境

Last updated at Posted at 2020-12-27

@binzume さんが投稿をされて、ESP32でもJavascriptが動くことを知りました。( ESP32 と QuickJS で小さなJavaScript実行環境を作る )
しかもES2020に対応しているなんて素敵です。

以下のことができることを目指します。

  • console.logがSerial出力されること(これはすでに@binzumeさんが実装されています)
  • LCD、Wire(I2C)、GPIOをJavascriptから触れるようにします。
  • JavascriptのソースコードをWebから取得します。

3番目が今回のモチベーションで、ESP32起動時にネットワークから取得することで、PlatformIOやArduino IDEから毎度コンパイル・書き込みを行う必要がなくなります。動作確認が終わったら、ROMに埋め込みます。

毎度の通りGitHubに上げておきます。

poruruba/Esp32QuickJS_sample
 https://github.com/poruruba/Esp32QuickJS_sample

続編です。とりあえず動かす手順を示しています。
 QuickJSでお手軽ESP32+Javascript実行環境(その2):使ってみよう

QuickJSの用意

@binzumeさんがすでにGitHubにライブラリ化していただいていますので、心配することはありません。

binzume/esp32quickjs
 https://github.com/binzume/esp32quickjs

オリジナルのQuickJSは以下にあります。

bellard/quickjs
 https://github.com/bellard/quickjs
 https://bellard.org/quickjs/

PlatformIOプロジェクトの作成

それではESP32用にJavascript実行環境を作成します。
まずは、PlatformIOのプロジェクトを作成します。

名前はなんでもよいのですが、とりあえず「Esp32QuickJS」とでもしておきます。
Boardには、今回M5Stick-Cを使いました。

image.png

次に、platformio.iniを編集します。
そうです、@binzumeさんが用意していただいているライブラリを指定します。また、同様にtanakamasayukiさんの「ESP32 Lite Pack Library」も使わせていただきました!

platform.ini
[env:m5stick-c]
platform = espressif32
board = m5stick-c
framework = arduino
upload_port = COM6
monitor_port = COM6
lib_deps = 
  tanakamasayuki/ESP32 Lite Pack Library@^1.3.2
  https://github.com/binzume/esp32quickjs.git#v0.0.1
  bblanchon/ArduinoJson@^6.17.2
board_build.partitions = no_ota.csv
board_build.embed_txtfiles =
  src/default.js
;  src/main.js

(2020/12/28 機能追加)
・JSON文字列をパースしたり、HTTP POST JSON できるように機能追加したため、ArduinoJsonも使うように変更しています。

次に、srcフォルダにdefault.jsを置きます。これは、WebからJavascriptのダウンロードに失敗したときに実行するもので、バカ除けです。

src/default.js
console.log("start");
setInterval(() =>{
  console.log('http get failed');
}, 1000);

ESP32用のQuickJSの呼び出し

まずは、mainとなるmain.cppです。
※最新のソースコードは続編投稿もしくはGitHubをご参照ください。

src/main.cpp
#include <WiFi.h>
#include "M5Lite.h"
#include <HTTPClient.h>

//#define LOCAL_JAVASCRIPT // ROMに埋め込む場合にはコメントアウトを外す

const char *wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char *wifi_password = "【WiFiアクセスポイントのパスワード】";
const char *jscode_url = "【Javascriptの取得先URL】";

WiFiClient espClient;
WiFiClientSecure espClientSecure;
#include "quickjs_esp32.h"

// see platformio.ini
#ifdef LOCAL_JAVASCRIPT
extern const char jscode_main[] asm("_binary_src_main_js_start");
#else
extern const char jscode_default[] asm("_binary_src_default_js_start");

#define JSCODE_BUFFER_SIZE  10000
char jscode[JSCODE_BUFFER_SIZE];
unsigned long jscode_len = sizeof(jscode);

void wifi_connect(const char *ssid, const char *password);
long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len);
#endif

ESP32QuickJS qjs;

void setup() {
  M5Lite.begin();
  Serial.begin(9600);

  M5Lite.Lcd.setRotation(3);
  M5Lite.Lcd.fillScreen(BLACK);
  M5Lite.Lcd.setTextColor(WHITE, BLACK);
  M5Lite.Lcd.println("[M5StickC]");

#ifdef LOCAL_JAVASCRIPT
  qjs.begin();
  qjs.exec(jscode_main);
#else
  wifi_connect(wifi_ssid, wifi_password);

  long ret;
  ret = doHttpGet(jscode_url, (uint8_t*)jscode, &jscode_len);
  if( ret == 0 ){
    jscode[jscode_len] = '\0';

    qjs.begin();
    qjs.exec(jscode);
  }else{
    qjs.begin();
    qjs.exec(jscode_default);
  }
#endif
}

void loop() {
  M5Lite.update();
  qjs.loop(); // For timer, async, etc.

  if (M5Lite.BtnB.wasPressed()) {
    Serial.println("BtnB pressed");
    esp_restart();
  }
}

#ifndef LOCAL_JAVASCRIPT
void wifi_connect(const char *ssid, const char *password){
  Serial.println("");
  Serial.print("WiFi Connenting");
  M5Lite.Lcd.print("Connecting");

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    M5Lite.Lcd.print(".");
    delay(1000);
  }
  Serial.println("");
  Serial.print("Connected : ");
  Serial.println(WiFi.localIP());

  M5Lite.Lcd.fillScreen(BLACK);
  M5Lite.Lcd.setCursor(0, 0);
  M5Lite.Lcd.println(WiFi.localIP());
}

long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len){
  HTTPClient http;

  Serial.print("[HTTP] GET begin...\n");
  // configure traged server and url
  if( url.startsWith("https") )
    http.begin(espClientSecure, url); //HTTPS
  else
    http.begin(espClient, url); //HTTP

  Serial.print("[HTTP] GET...\n");
  // start connection and send HTTP header
  int httpCode = http.GET();
  unsigned long index = 0;

  // httpCode will be negative on error
  if(httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      Serial.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if(httpCode == HTTP_CODE_OK) {
        // get tcp stream
        WiFiClient * stream = http.getStreamPtr();

        // get lenght of document (is -1 when Server sends no Content-Length header)
        int len = http.getSize();
        Serial.printf("[HTTP] Content-Length=%d\n", len);
        if( len != -1 && len > *p_len ){
          Serial.printf("[HTTP] buffer size over\n");
          http.end();
          return -1;
        }

        // read all data from server
        while(http.connected() && (len > 0 || len == -1)) {
            // get available data size
            size_t size = stream->available();

            if(size > 0) {
                // read up to 128 byte
                if( (index + size ) > *p_len){
                  Serial.printf("[HTTP] buffer size over\n");
                  http.end();
                  return -1;
                }
                int c = stream->readBytes(&p_buffer[index], size);

                index += c;
                if(len > 0) {
                    len -= c;
                }
            }
            delay(1);
        }
      }
  } else {
    http.end();
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    return -1;
  }

  http.end();
  *p_len = index;

  return 0;
}
#endif

LOCAL_JAVASCRIPT がDefineされているかどうかで変わります。
定義されていない場合、Wifiに接続し、指定された【Javascriptの取得先URL】からHTTP GetでJavascriptをダウンロードしそれを実行しています。
WiFiアクセスポイントは以下で指定します。
【WiFiアクセスポイントのSSID】
【WiFiアクセスポイントのパスワード】

Javascriptの実行は以下の部分です。

src/main.cpp
    qjs.begin();
    qjs.exec(jscode);

実行エンジンは以下で指定しています。実行エンジンの実装は後ほど説明します。

src/main.cpp
#include "quickjs_esp32.h"
ESP32QuickJS qjs;

Javascriptの動作の確認ができたら、WiFiから取得するのではなくROMに埋め込みます。
その場合は、LOCAL_JAVASCRIPTをDefine定義して、以下にファイルを配置します。

src/main.js

platform.iniで上記ファイルを定義し、以下のようにしてCソースから取り出せます。

extern const char jscode_main[] asm("_binary_src_main_js_start");

ちなみに、すぐにWeb上のJavascriptを再ロードできるように、M5StickCのBボタン(右側面にあるボタン)を押せば、リブートがかかるようにしています。

ESP32用のQuickJSの実装

Javascript実行エンジンは、ESP32に特化してカスタマイズしており、ヘッダファイルinclude/quickjs_esp32.hに記述しています。
※というより、ほぼ @binzume さんのオリジナルです。
ちなみに、QuickJS本体は、@binzumeさんのライブラリを参照しておりquickjs.hにあります。

ソースコードが長いので、詳細はGitHubをご参照ください。

なんかだらだらと長いですが、JavascriptからLCDやI2CやGPIOをたたくためのラッパーを実装しています。
モジュール名は、デバイスタイプごとにサポートする名前が異なります。
整理する意味でざっと上げてみました。

モジュール名 関数名
gpio pinMode
^ digitalWrite
^ digitalRead
^ analogRead
wire begin
^ requestFrom
^ beginTransmission
^ endTransmission
^ write
^ available
^ read
wire1 begin
^ requestFrom
^ beginTransmission
^ endTransmission
^ write
^ available
^ read
lcd setRotation
^ setTextColor
^ setTextSize
^ setCursor
^ setBrightness
^ drawPixel
^ drawLine
^ print
^ println
^ fillScreen
^ getWidth
^ getHeight
^ getDepth
esp32 millis
^ deepSleep
^ restart
^ delay
^ setLoop
^ jsonParse
^ jsonPost
^ jsonGet

Javascriptを実行

それでは、Javascriptを実行してみます。
1秒ごとにLEDをついたり消したりするLチカアプリです。
GPIOの操作には以下のgpioを使います。

import * as gpio from "gpio";

例えばこんな感じです。

main.js
import * as esp32 from "esp32";
import * as gpio from "gpio";
import * as lcd from "lcd";

console.log("start");
gpio.pinMode(10, gpio.OUTPUT);

var led = false;
gpio.digitalWrite(10, led ? gpio.LOW : gpio.HIGH);
lcd.println("Hello World");
lcd.setTextSize(2);

setInterval(async () =>{
try{
  console.log('interval');
led = !led;
  gpio.digitalWrite(10, led ? gpio.LOW : gpio.HIGH);
  }catch(error){
      console.log(error);
  }
}, 1000);

これを、src/main.cpp で指定した【Javascriptの取得先URL】に配備します。
それでは、PlatformIOからコンパイル・書き込みを実施し、M5StickCを起動させてみましょう。
WiFiアクセスポイントに接続後、LCDにHello Worldと表示され、1秒ごとにLEDが付いたり消えたりしたのではないでしょうか。

(応用)ENV Sensorで温度を計測

Groveで接続可能なセンサーユニットを使います。

M5Stack用環境センサユニット
 https://www.switch-science.com/catalog/5690/
(おうっ、販売終了しているではないか。。。)

I2Cで接続します。
I2Cの操作には、lcdを使います。

import * as lcd from “lcd”;

こんな感じです。

main.js
import * as esp32 from "esp32";
import * as gpio from "gpio";
import * as wire from "wire";
import * as lcd from "lcd";

console.log("start");
gpio.pinMode(10, gpio.OUTPUT);

var led = false;
gpio.digitalWrite(10, led ? gpio.LOW : gpio.HIGH);
lcd.println("Hello World");
lcd.setTextSize(2);

class DHT12{
  constructor(wire, scale = 1, id = 0x5c){
    this.CELSIUS = 1;
    this.KELVIN = 2;
    this.FAHRENHEIT = 3;

    this.wire = wire;
    this.scale = scale;
    this.address = id;
  }

  async read(){
    this.wire.beginTransmission(this.address);
    var ret = this.wire.write(0);
    if( ret != 1 )
      throw 'failed';
    var ret = this.wire.endTransmission();
    if( ret != 0 )
      throw 'failed';

    var ret = this.wire.requestFrom(this.address, 5);
    if( ret != 5 )
      throw 'failed';

    var datos = this.wire.read(5);
    await this.sleep_async(50);

    var ret = this.wire.available();
    if( ret != 0 )
      throw 'failed';

    if (datos[4] != (datos[0] + datos[1] + datos[2] + datos[3]) )
      throw 'datos error';

    this.datos = datos;
  }

  async readTemperature(scale){
    await this.read();
    if( scale == undefined )
      scale = this.scale;

    var resultado = 0.0;
    switch(scale) {
      case this.CELSIUS:
        resultado = this.datos[2] + this.datos[3] / 10.0;
        break;
      case this.FAHRENHEIT:
        resultado= (this.datos[2] + this.datos[3] / 10.0) * 1.8 + 32.0;
        break;
      case this.KELVIN:
        resultado= (this.datos[2] + this.datos[3] / 10.0) + 273.15;
        break;
    };
    return resultado;
  }

  async readHumidity(){
    await this.read();

    var resultado = (this.datos[0] + this.datos[1] / 10.0);
    return resultado;
  }

  async sleep_async(msec){
    return new Promise(resolve =>{
      setTimeout(resolve, msec);
    });
  }
}

wire.begin();
var dht12 = new DHT12(wire);
lcd.println("DHT12 start");
lcd.setTextSize(3);

setInterval(async () =>{
  try{
      console.log('interval');
      led = !led;
      gpio.digitalWrite(10, led ? gpio.LOW : gpio.HIGH);

      var temp = await dht12.readTemperature();
      console.log(temp);
      lcd.setCursor(0, 40);
      lcd.print("     ");
      lcd.setCursor(0, 40);
      lcd.print(temp);
  }catch(error){
    console.log(error);
  }

}, 1000);

うまくいけば、SerialコンソールとLCDに温度が表示されているかと思います。

終わりに

QuickJSを作ってくれたbellardさんに感謝ですし、見つけてくれた@binzumeさんにも感謝です!

HTTP Getによるファイルダウンロードは以下を参考にしています。
 ESP32でバイナリファイルのダウンロード・アップロード

もし、Javascript(Node.js)上で、ブレークポイントを入れたりとデバッグしたい場合は以下もご検討ください。DHT12のライブラリはこちらで作成しました。
 M5StickCの書き換えが面倒だったので、Node.jsでArduinoっぽくしてみた

ほかにも、こんな応用もあります。
 MakeCode for micro:bitで作ったプロジェクトをM5Atomで動かす

以上

5
4
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
5
4