ESP8266 SDKのJSON APIを使ってみた

前回はオープンソースのJSONライブラリについて紹介しましたが、本家のESP8266 SDKにも、JSONライブラリが入っています。

今回は前回の続きということでこのライブラリを紹介しますが、最初に結論を書いておくと、このライブラリは空のオブジェクト({})をうまく扱えないバグがあります。
また、使いやすさという意味ではArduinoJsonやaJsonにやや劣ります。
その代わり、解析したいJSONデータに合わせてプログラムコードをチューニングすることも可能です。
実際に使用する場合は、SDKのライブラリをリンクするよりも、大元のソースコードをダウンロードしたほうがデバッグしやすいと思います。

前回と同じテストデータを使った場合の星取表は下のようになります。

TEST1 TEST2 TEST3 TEST4
ArduinoJson OK OK NG NG
aJson OK NG OK OK
ESP8266 SDK API OK NG OK OK

このライブラリのAPIの解説はSDKに付属しているプログラミングガイドの中にあります。
現時点でのファイル名は「2C-ESP8266__SDK__Programming Guide__EN_v1.3.0.pdf」となっています。
このガイドの5.2節がJSON関連APIの解説になっています。
ただ、ガイドではAPIそのものが簡単に解説されているだけで、あまり親切ではありません。

このSDKのライブラリとインクルードファイルはArduino IDEの中にも含まれています。
JSON関連のインクルードファイルが入っているフォルダは

%USERPROFILE%\AppData\Roaming\Arduino15\packages\esp8266\hardware\esp8266\1.6.5-947-g39819f0\tools\sdk\include\json

です。
ヘッダファイルのコメントを見ると、最後のほうに

This file is part of the Contiki operating system.

とあります。
つまり、このライブラリは「Contiki」という別のプロジェクトから持ってきたものらしいことが分かります。

探してみると、このライブラリのオリジナルは

contiki/apps/json at master · contiki-os/contiki

であるようです。ソースコードも公開されています。

SDKのプログラミングガイドの解説だけでは、どうも内容が分かりにくかったのですが、このソースコードを眺めるとライブラリの使い方が分かってきました。
APIにはJSONの解析と生成の両方が含まれていますが、関心は解析のほうにありますので、解析側のAPIのみ使ってみます。

まず、先にソースコードを掲載しておきます。

#include <limits.h>
#include <errno.h>
#define JSONPARSE_CONF_MAX_DEPTH 50
#ifdef ESP8266
extern "C" {
#include "json/jsonparse.h"
}
#endif
#define TEST1 "{\"obj1\":\"abc\"}"
#define TEST2 "{\"obj1\":{}, \"obj2\":[{}], \"obj3\":null}"
//TEST3, TEST4 derived from http://json.org/example.html
#define TEST3 "{\"glossary\": {\"title\": \"example glossary\",\"GlossDiv\": {\"title\": \"S\",\"GlossList\": {\"GlossEntry\": {\"ID\": \"SGML\",\"SortAs\": \"SGML\",\"GlossTerm\": \"Standard Generalized Markup Language\",\"Acronym\": \"SGML\",\"Abbrev\": \"ISO 8879:1986\",\"GlossDef\": {\"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\"GlossSeeAlso\": [\"GML\", \"XML\"]},\"GlossSee\": \"markup\"}}}}}"
#define TEST4 "{\"widget\": {\"debug\": \"on\",\"window\": {\"title\": \"Sample Konfabulator Widget\",\"name\": \"main_window\",\"width\": 500,\"height\": 500    },\"image\": {\"src\": \"Images/Sun.png\",\"name\": \"sun1\",\"hOffset\": 250,\"vOffset\": 250,\"alignment\": \"center\"    },\"text\": {\"data\": \"Click Here\",\"size\": 36,\"style\": \"bold\",\"name\": \"text1\",\"hOffset\": 250,\"vOffset\": 100,\"alignment\": \"center\",\"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"    }}} "

#define JSON_TEST_DATA TEST1

void test_sdk_json() {
 char json[] = JSON_TEST_DATA;
 jsonparse_state state;
 int c;
 int valueType;

 jsonparse_setup(&state, json, strlen(json));
 while((c = jsonparse_next(&state)) != JSON_TYPE_ERROR) {
   char token[256];
   long value;
   
   switch(c) {
     case JSON_TYPE_ARRAY:
     case JSON_TYPE_OBJECT:
     case JSON_TYPE_PAIR:
       Serial.print(char(c));
       break;
     case JSON_TYPE_PAIR_NAME:
     case JSON_TYPE_STRING:
       valueType = jsonparse_copy_value(&state, token, 255);
       Serial.print(String('"') + token + '"');
       break;
     case JSON_TYPE_INT:
     case JSON_TYPE_NUMBER:
       value = jsonparse_get_value_as_long(&state);
       Serial.print(value, DEC);
       break;
     case JSON_TYPE_NULL:
       Serial.print("null");
       break;
     case JSON_TYPE_TRUE:
       Serial.print("true");
       break;
     case JSON_TYPE_FALSE:
       Serial.print("false");
       break;
     default:
       Serial.print(char(c));
   }
 }
 if (c == JSON_TYPE_ERROR) {
   String errStr;

   Serial.println();
   switch(state.error) {
     case JSON_ERROR_OK: errStr="OK"; break;
     case JSON_ERROR_SYNTAX: errStr="Syntax Error"; break;
     case JSON_ERROR_UNEXPECTED_ARRAY: errStr="Unexpected array"; break;
     case JSON_ERROR_UNEXPECTED_END_OF_ARRAY: errStr="Unexpected end of array"; break;
     case JSON_ERROR_UNEXPECTED_OBJECT: errStr="Unexpected object"; break;
     case JSON_ERROR_UNEXPECTED_STRING: errStr="Unexpected string"; break;
   }
   Serial.println(errStr);

   if (state.depth < JSONPARSE_MAX_DEPTH) {
     // dump json stack
     int i;
     for(i = 0; i < state.depth; i++) {
       Serial.print(state.stack[i]);
     }
     Serial.println();
  } else {
     Serial.println("JSON too deep");
   }
 }
}

void setup() {
 Serial.begin(115200);
 Serial.println("");
 Serial.println(String("Test data: ")+JSON_TEST_DATA);

 Serial.println("\n\ntesting SdK json API\n");
 test_sdk_json();
}

void loop() {
 // put your main code here, to run repeatedly:
 delay(100);
}

aJsonやArduinoJsonでは、最初にJSON文字列を渡して一気に全体をパースしていました。
一方、このSDKのライブラリでは、文字列を少しずつ読み進めていくループを自分で書く必要があります。

最初の

jsonparse_setup(&state, json, strlen(json));

は変数の初期値を設定するだけで、そのあと

c = jsonparse_next(&state)

を呼ぶたびに、新しいデータを読み込みます。

このとき、空白はまとめて読み飛ばされ、文字列や数値など文字に分解できない「アトム」はまとめて読み込まれます。
自分のプログラムで行いたい処理は、このwhileループの中で行うことになります。

“{“と”}”のような対応する入れ子構造はパーサがチェックし、文法ミスがあればエラーを返します。

また、このライブラリで扱えるデータの入れ子構造の最大値は、デフォルトでは10段です。
TEST3で使うデータはもう少し階層が深いので、

#define JSONPARSE_CONF_MAX_DEPTH 50

で最大を50にしています。

なお、これはESP8266用Arduinoの不備だと思われますが、上記のスケッチをコンパイルすると「strtoulが無い」というエラーが出ます。
これについては、以下のやり取りが参考になります。

cstdlib function strtoul not supported · Issue #608 · esp8266/Arduino

要は、ESP8266用Arduinoのlibcにstrtoulが入ってなかったということのようです。
今回は、とりあえず以下からstrtoul()関数を丸ごとコピーしてスケッチの末尾へ貼り付けました。

Arduino/libc_replacements.c at f73457de0d6b703b96a809e48b2163f16aa367e8 · kzyapkov/Arduino

また、strtoulを正しくコンパイルするために、

#include <limits.h>
#include <errno.h>

の2つのファイルをインクルードしています。

さらに、ESP8266用Arduinoの標準ではSDKのJSONライブラリをリンクしてくれませんので、これについても修正が必要です。

修正は、

%USERPROFILE%\AppData\Roaming\Arduino15\packages\esp8266\hardware\esp8266\1.6.5-947-g39819f0

のフォルダの中の

platform.txt

の中の下記の部分を追記しました。(上に掲載したスケッチからは省略しています。)

compiler.c.elf.libs=-lm -lgcc -lhal -lphy -lnet80211 -llwip -lwpa -lmain -lpp -lsmartconfig -lwps -lcrypto -ljson

(これが正しいやり方なのかどうかは分かりません。)
この修正を有効にするにはIDEの再起動が必要です。

コメント