Milkcocoa ESP8266 SDKの非同期化ハック

問題点

ESP8266でMilkcocoaを使うためのライブラリとしてMilkcocoa ESP8266 SDKがあります。
たいへん分かりやすく使いやすいライブラリだけども、同期処理(ブロッキング処理)なのが困りものです。下のコードを見てください。

loop(){
  // 以下をloopの中で必ず実行します
  milkcocoa.loop(); // ここに1秒間もとどまる
  
  // 以下の処理が回らない
  (略)
}

このようにloop()の中で、必ずMilkcocoa::loop()を実行する必要があります。そして、このMilkcocoa::loop()は、一度入るとタイムアウトまで最大1秒間も戻って来ません。これは本来、組込み系ではやってはいけないことです。loop()が止まってしまうので、その間は他の処理を進めることができません。

問題の原因箇所

それでは、Milkcocoa ESP8266 SDKのコードを追ってみます。
関数の呼び出し階層は以下のようになります。

  1. Milkcocoa::loop()
  2. Adafruit_MQTT::readSubscription()
  3. Adafruit_MQTT_Client::readPacket()

そしてAdafruit_MQTT_Client::readPacket()の中で、delay()を繰り返してタイムアウトまで待っています。ここが原因箇所です。

原因の解消案

何もせずにdelay()で待つから処理が止まるわけです。スレッドが使えれば並行処理できますが、Arduinoはシングルスレッドです。C#のような強力な非同期処理機構もありません。
そこで、苦し紛れのハックですが、MQTT_Client::readPacket()でdelay()を呼ぶ代わりにユーザー定義の関数を呼び返してやることにします。呼び返す関数の名前はsubloop()とでもします。ユーザーはスケッチの中でsubloop()関数を定義します。

解消案の実装

修正するライブラリ側のソースは1つだけです。
Adafruit_MQTT_Client.cpp

(略)
extern void subloop();  // スケッチ側で定義

uint16_t Adafruit_MQTT_Client::readPacket(
(略)
    //delay(MQTT_CLIENT_READINTERVAL_MS); // 待たない
    subloop();  // スケッチを呼び返す
  }
  return len;
}
(略)

スケッチ

loop(){
  // 以下をloopの中で必ず実行します
  milkcocoa.loop();
  
  (略)
}

subloop(){
    // Milkcocoaの待ち中に繰り返したい処理をここに
}