Mongoose OSでDS18B20のmJSライブラリを作ったが…

# はじめに

Mongoose OSは組み込み向けOSのひとつで、mJSというライブラリによってマイコン上でJavascriptのようなコードを動かすことが可能です。
参考: [ESP32をWiFi経由でコントロールするアーキテクチャとライブラリをまとめた](https://qiita.com/uhey22e/items/c285edf79893db14bfa6)

ただ、センサー類のドライバをすべてJSで書くとなると大変なので、
C言語でドライバを書いて、それをJSから利用することが可能な仕組みが用意されています。

今回はC言語でDS18B20の制御コードを書き、それをJSから利用しようとした記録です。

# 環境

* Windows 10
* Mongoose OS v2.1.5
* ESP32-DEVKITC

# 結論

思いの外面倒だったので、これなら全部Cで書いたほうが良いと思いました。

# Cで書いたドライバ

以下のリポジトリにアップしています。
https://github.com/uhey22e/mongoose-os-ds18b20

以下リポジトリをフォークさせていただきました。
https://github.com/maclema/mongoose-os-ds18b20

mJS用のコードを追加している他に、Initialize/Deinitializeの関数を分離したりなどしています。
mJSに関係ない部分は割愛します。

このライブラリは、Cであれば以下のように使うことが可能です。

```c:main.c
enum mgos_app_init_result mgos_app_init(void) {
    ds18b20_init(25, 10);
    mgos_set_timer(1000 /* ms */, MGOS_TIMER_REPEAT, timer_cb, NULL);
    return MGOS_APP_INIT_SUCCESS;
}

// 1秒おきにOSから呼ばれる
static void timer_cb(void *arg)
{
  // 計測開始
  ds18b20_read_all(temperatures_cb);
  // 諸々の処理...
}


// 計測完了コールバック
static void temperatures_cb(ds18b20_result *results)
{
    if ( results == NULL ) {
        LOG(LL_INFO, ("Results is null."));
    }
    
    // 各センサの計測結果を出力
    while ( results != NULL ) {
        LOG(LL_INFO,
            ("ROM: %s, Temp: %f", results->mac, results->temp));
        results = results->next;
    }
}
```

結果が入る構造体は以下のようになっています。

```c:ds18b20.h
typedef struct ds18b20_result {
    uint8_t rom[8];                     // センサのアドレス
    char* mac;                          // センサのアドレスを文字列化したもの
    float temp;                         // 計測結果
    struct ds18b20_result* next;        // 次の結果
} ds18b20_result;
```

DS18B20はひとつのバスに複数のセンサを接続可能です。
そのため、センサがいくつ繋がっていても対応出来るように、Linked Listの形を取っています。

# mJSから使う

このドライバを、以下のようなコードで使えるといいなと思っていましたが、そんな簡単にはいきませんでした。

```javascript
// 動かないコード
load('api_ds18b20.js');

DS18B20.init(25, 10);

DS18B20.readAll(function (results) {
    results.forEach(function (result) {
        print("Addr:", result.mac);
        print("Temp:", result.temp);
    });
}, null);
```

mJSではJavascriptと比べて色々と制約があります。
https://github.com/cesanta/mjs#Restrictions

## 最終的なコード

```javascript:init.js
load('api_ds18b20.js');

DS18B20.init(25, 10);

// 2秒おきに計測
Timer.set(2000, Timer.REPEAT, function () {
  Log.info("Timer Callback");
  DS18B20.read_all();
  // 計測完了まで100ms待つ
  Timer.set(100, false, function () {
    let index = 0;
    // 各センサの計測結果を取得する
    while (true) {
      let result = DS18B20.getResultByIdx(index);
      if (!result) {
        break;
      }
      print(result.mac);
      print(result.temp);
      index++;
    }
  }, null);
}, null);
```

本当は、`read_all`のコールバック内で計測結果を取得したかったのですが、
CのコードからmJSのfunctionを呼び出すと、`Guru Mediation Error`が発生してしまい、これが解決できませんでした。
結果として、Timerで固定時間のwaitをいれてから、ひとつずつ取得する形式にしました。

mJSとCをつなげるコードは以下のように書きました。

```javascript:api_ds18b20.js
let DS18B20 = {
  init: ffi('void ds18b20_init(int, int)'),
  deinit: ffi('void ds18b20_deinit(void)'),
  read_all: ffi('void ds18b20_read_all_wrap(void)'),
  _getResultByIdx: ffi('void * get_result_by_idx(int)'),

  // Cの構造体をmJSのオブジェクトに変換
  getResultByIdx: function (idx) {
    let sd = ffi('void * get_ds18b20_result_descr()')();
    let s = this._getResultByIdx(idx);
    if (s !== null) {
      return s2o(s, sd);
    }
    return null;
  },
};
```

`read_all_wrap`というmJS用に`read_all`をラップした関数を作成しました。
他にも、Cの構造体とmJSのオブジェクトを結びつけるための`ds18b20_result_descr`という定数も作っています。
LinkedListに使う`next`のフィールドは、mJSでうまく扱えないため落としています。
アドレスを示す`rom`という配列も、mJSで配列がうまく扱えないため落としています。

```c:ds18b20.c
// For mJS API
static const struct mjs_c_struct_member ds18b20_result_descr[] = {
    {"mac", offsetof(ds18b20_result, mac), MJS_STRUCT_FIELD_TYPE_CHAR_PTR, NULL},
    {"temp", offsetof(ds18b20_result, temp), MJS_STRUCT_FIELD_TYPE_FLOAT, NULL},
    {NULL, 0, MJS_STRUCT_FIELD_TYPE_INVALID, NULL},
};

const struct mjs_c_struct_member* get_ds18b20_result_descr(void)
{
    return ds18b20_result_descr;
}

void ds18b20_read_all_wrap(void)
{
    ds18b20_read_all(NULL);
}

void * get_result_by_idx(int idx)
{
    ds18b20_result* temp;

    // Get result ptr by idx
    temp = _list;
    for (int i = 0; i < idx; i++) {
        if (temp == NULL) {
            return NULL;
        }
        temp = temp->next;
    }
    return temp;
}
```

# まとめ

なんとか、mJSからDS18B20を使えるようにはなりましたが、あまり使い勝手の良いAPIとは言えない状態です。
とはいえ、`Guru Mediation Error (LoadProhibited)`のデバッグがしんどく、これ以上手を加える気にもなれません。
Cのデータ構造をそのまま持って来られるわけでは無いのも、なかなか面倒でした。

各クラウドへの接続が簡単に出来て、RPCの仕組みも強力なMongoose OS、
メインの制御ロジックにmJSを使えばかなりの省力化が可能でとても良いのですが、
私は「やっぱりCでいいか…」となってしまいました。