コンテンツにスキップ

センサーとテレメトリ

この章では、フィルターが空気の計測を開始してクラウドにデータを送信します。核心テクニック — テレメトリ内の独自フィールド: エコシステム辞書はVOCを知りませんが、コアはテレメトリに任意のフィールドを追加する手段を提供します。

1. センサーライブラリ

platformio.inilib_depsに追加:

    adafruit/Adafruit SGP40 Sensor

2. SGP40読み取り

src/main.cpp内(ピン — 配線図から):

#include <Wire.h>
#include <Adafruit_SGP40.h>

static Adafruit_SGP40 s_sgp;
static int32_t g_vocIndex = -1;   // -1 = データはまだない

static void initVocSensor() {
    Wire.begin(/*SDA=*/8, /*SCL=*/9);
    if (!s_sgp.begin()) {
        Serial.println("[VOC] SGP40 not found, check wiring");
    }
}

static void readVocSensor() {
    // measureVocIndex()は内部センサー補償を自己管理。
    // インデックス: ~100 = 通常の空気、高い = より汚い(最大500)。
    g_vocIndex = s_sgp.measureVocIndex();
}

initVocSensor()の呼び出しはsetup()内のs_link.begin()の後に追加し、readVocSensor()loop()内で毎秒呼び出します(millisタイマー経由、delayは使いません):

static uint32_t s_lastReadMs = 0;

void loop() {
    s_link.loop();

    uint32_t now = millis();
    if (now - s_lastReadMs >= 1000) {
        s_lastReadMs = now;
        readVocSensor();
    }
}

loopの中でdelayを使わないでください

s_link.loop()は常に呼び出し続ける必要があります — Wi-Fi、MQTT、ポータルからのコマンド処理はすべてここで行われます。delay(1000)を入れるとこれらがすべて止まります。必ずmillisタイマーを使ってください。

3. テレメトリ内の独自フィールド

telemetryPeriodMsごとに、コアは自動的にテレメトリJSONメッセージを収集してクラウドに送信します。私たちのデバイス(1ユニット、辞書スキルからはファンのみ)の場合、コアは以下のようなメッセージを収集します:

{
  "units": [ { "unitId": "U1", "fanStatus": false } ],
  "rssi": -52,
  "uptime": 120
}

構造を分解:

  • units — デバイスの**ユニット**(チャンバー)の配列。シリアルiDryerドライヤーは最大4つの独立したチャンバーを持つことができるため、テレメトリはつねに配列で、1つのチャンバーの場合でも;
  • units[0] — 最初の(そして唯一の)ユニット: ConfigunitsCount = 1を指定;
  • fanStatus — 辞書フィールド、hasFan = trueから出現;
  • rssi, uptime — Wi-Fiレベルと稼働時間、コアは常時追加。

このメッセージにはVOCに関する情報がありません — コアは私たちのセンサーを知らないからです。しかし送信直前に、コアは**コールバック**(callback)を呼び出す機会を提供します。コールバックとは「あなたがコアに渡す関数」で、コアは各テレメトリ公開時にそれを呼び出し、組み立て済みのJSON(引数docがそれです)を渡してきます。

setup()内:

s_link.onTelemetryPublish([](JsonObject doc) {
    // doc — それはコアが集めたテレメトリメッセージ(上記のJSON参照)。
    // 最初のユニットに独自フィールドvocIndexを追加。
    if (g_vocIndex >= 0) {
        doc["units"][0]["vocIndex"] = g_vocIndex;
    }
});

doc["units"][0]["vocIndex"] = g_vocIndex;は以下のように読みます: 「メッセージdocで配列unitsを取り、そこにある要素0(最初のユニット)を取り、フィールドvocIndexを書き込む」。フィールド名はあなたが選びます — 次の章でそれを参照して、カード上に値を表示します。

「フック」という言葉について

コアのソースではこのコールバックをPublishHookと呼んでいます — 「フック」(hook)は同じ意味です: ライブラリがあなたの関数を「引っ掛ける」ための接続点。どちらの用語も同義で使われます。このドキュメントでは「コールバック」で統一します。

ラムダと、なぜキャプチャリストが「空」なのか

[](JsonObject doc) { ... }という書き方は**ラムダ**と呼ばれます — 名前のない関数で、使う場所にそのまま書けるため、別途定義して名前を付ける手間が省けます。

先頭の角括弧は「キャプチャリスト」です: 関数が外から借りてくるローカル変数をここに列挙します。コアのルール: 括弧は常に空[])— ラムダは何もキャプチャせず、状態を持ち込みません(stateless、「ステートレス」と呼ばれます)。

理由は技術的なものです: キャプチャありのラムダは動的メモリ確保を必要とし、ESP32で頻繁に確保するとヒープが断片化して最悪の場合Wi-Fi接続が不安定になります。そのためコアはシンプルな関数のみを受け付けます。

実践的な結論: コールバックが必要とするものはすべて**グローバル**変数に保持してください — g_vocIndexがその例です。このルールはすべてのidryer-coreコールバックに適用されます。

ファンの状態は辞書の方法で公開します — オン/オフ切替時にコアのフィールドに書き込むだけです(ロジックは第7章):

s_link.telemetry.fanOn[0] = fanIsOn;

4. 確認

書き込み後、デバイスのMQTTストリーム(またはシリアルログ公開)でテレメトリは以下のように見えます:

{
  "units": [ { "unitId": "U1", "fanStatus": false, "vocIndex": 103 } ],
  "rssi": -52,
  "uptime": 120
}

vocIndex は独自フィールドで、辞書フィールドfanStatusと並んでクラウドに送られています。ポータルはすでに受け取って保存していますが、まだ何を表示すべきか知りません。次の章でそれを教えます。

センサーに息を吹きかけるか、マーカーを近づけてみてください — インデックスが数秒で大きく上昇するはずです。