🐘

arduino-esp32のMatterライブラリを使う

に公開

夏が近いのでM5Atomと赤外線LEDを使い、arduino-esp32のMatterライブラリでエアコンをコントロールするシステムを作成しました。

すでにNature Remo nanoを使っていて概ね満足でしたが

特に内部洗浄について家族から苦情があったので自作することにしました。

Wi-Fi経由でMatterネットワークに接続し、冷房モードのオンオフや温度設定を行うことができます。
エアコン制御自体は、赤外線リモコンのRAWデータを温度別にプリセットして、M5Atomから直接送信する形になっています。
初めはIRremoteESP8266を使っていましたが、Sharp製エアコンと相性が悪かったのでArduino-IRremoteでrawをキャプチャして送信することにしました。IRを送信する際には温度別に格納された赤外線RAW配列(例:rawCool24On[])を選択するだけです。以下は使わなかった解析です。
https://gist.github.com/kyjibato/be12ef9c608e35860f52977c1d839789

M5のENV IV Unitを購入し、実測温度センサーの値(measuredTemperature)をMatterの温度表示に使用するつもりでした。しかし「実測温度を使うと、設定温度に達した瞬間に自動でエアコンがオフになる」という現象に見舞われました。
これは、Matter側で「現在温度 < 設定温度」となると冷房モードをオフにする仕様になっているようです。
この問題に対処するため、最終的には実測温度の使用をやめ、代わりに目標温度をそのままローカル温度としてMatterに反映する形に変更しました。
つまり、設定された目標温度=現在の温度としてMatterに報告する仕様にしています。どうも一致しているな限りオフにならないようです。この理解が正しいかはわからないですが、別に現在温度なんてなくてもいいやと思ったので。実測センサーの値は捨てて、あくまで設定値基準で運用する、という妥協策です。

measuredTemperature = target;
ThermostatEndpoint.setLocalTemperature(measuredTemperature);

他にも冷房の温度の加減上限が固定されているので近い値を使うようにしています。

MatterThermostat.h
// clang-format off
// Default Thermostat values - can't be changed - defined in the Thermostat Cluster Server code
static const int16_t kDefaultAbsMinCoolSetpointLimit = 1600; // 16C (61 F)
static const int16_t kDefaultMinCoolSetpointLimit    = 1600; // 16C (61 F)
static const int16_t kDefaultAbsMaxCoolSetpointLimit = 3200; // 32C (90 F)
static const int16_t kDefaultMaxCoolSetpointLimit    = 3200; // 32C (90 F)

実測値でハマったことにより、Matterの設計思想や仕様を理解する良い機会になりました。
プロトコル標準に従った実装は簡単ではないですが自作なので適当に書けるのはメリットですね。
実際に作成した感想ですが、arduino-esp32は自由度が低いので次はESP-IDFで開発しようと思います。

以下全コードですが、機種ごとにかなり異なるので参考にはならないと思います。実際には数回IRの送信をした方がいいです。

main.ino
#include <Matter.h>
#include <WiFi.h>
#include <Wire.h>
#include <M5Atom.h>      // LED & ボタン(G39)
#include <cmath>
#include <IRremote.h> 

#include <esp_task_wdt.h>

#define WDT_TIMEOUT 10


// ────────────────────────  CONSTANTS  ─────────────────────────────
constexpr char kSsid[]                       = "";
constexpr char kPassword[]                   = "";
constexpr uint32_t kFactoryResetTimeoutMs    = 5000;
constexpr float kMinTargetTemp               = 18.0f;
constexpr float kMaxTargetTemp               = 28.0f;
constexpr uint8_t kLedBrightness             = 0x20;
constexpr uint8_t IR_SEND_PIN = 23;

// ────────── 温度別 RAW 配列 ────────────

extern const uint16_t rawCool18On[] = {3900,  1850,  550,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  500,  1400,  500,  450,  500,  1400,  500,  400,  550,  1350,  500,  1400,  500,  1400,  500,  1400,  500,  400,  500,  450,  550,  1350,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  550,  1350,  550,  400,  550,  1350,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  550,  1350,  500,  1400,  500,  1350,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  550,  1350,  500,  450,  500,  0};
extern const uint16_t rawCool19On[] = {3900,  1850,  550,  400,  550,  1350,  500,  450,  550,  1350,  500,  400,  550,  1400,  500,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  1350,  500,  1400,  500,  1400,  500,  400,  500,  450,  550,  1350,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  550,  1350,  500,  450,  550,  1350,  550,  350,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  550,  1350,  500,  1400,  500,  1350,  500,  1400,  500,  450,  450,  500,  450,  450,  500,  450,  550,  1350,  500,  1400,  500,  450,  500,  0};
extern const uint16_t rawCool20On[] = {3900,  1800,  550,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  450,  550,  1350,  500,  1400,  500,  400,  550,  1350,  500,  450,  550,  1350,  500,  1400,  450,  1400,  550,  1350,  500,  450,  500,  450,  500,  1400,  500,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  550,  1350,  500,  1400,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  500,  450,  500,  450,  550,  1350,  500,  400,  500,  450,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  400,  550,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  1350,  500,  1400,  500,  1400,  500,  1400,  500,  400,  550,  400,  500,  450,  550,  1350,  500,  1400,  500,  1400,  500,  400,  550,  0};
extern const uint16_t rawCool21On[] = {3900,  1850,  500,  450,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  500,  450,  550,  1350,  500,  400,  550,  1400,  500,  400,  550,  1350,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  1400,  500,  1400,  500,  1350,  550,  400,  500,  450,  550,  1350,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  500,  450,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  450,  500,  500,  1400,  500,  400,  550,  400,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  400,  550,  1350,  550,  1350,  550,  1350,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  0};
extern const uint16_t rawCool22On[] = {3900,  1850,  500,  450,  550,  1350,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  1400,  450,  1450,  500,  1350,  550,  400,  500,  450,  550,  1350,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  400,  550,  1350,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  550,  1350,  500,  450,  450,  500,  500,  400,  500,  450,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  1400,  450,  1400,  550,  1350,  500,  1400,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  0};
extern const uint16_t rawCool23On[] = {3900,  1850,  550,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  500,  450,  550,  1350,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  1400,  500,  1400,  500,  1350,  550,  400,  500,  450,  500,  1400,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  400,  550,  1350,  500,  450,  500,  450,  500,  450,  500,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  550,  1350,  500,  1400,  500,  1400,  500,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  550,  0};
extern const uint16_t rawCool24On[] = {3900,  1800,  550,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  400,  550,  1400,  500,  400,  550,  1350,  500,  450,  500,  1400,  500,  1400,  500,  400,  550,  1350,  500,  450,  500,  1400,  500,  1400,  500,  1350,  550,  1350,  500,  450,  500,  400,  550,  1350,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  500,  450,  500,  450,  550,  1350,  500,  1400,  500,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  400,  550,  400,  550,  1350,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  550,  1350,  500,  450,  450,  450,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  500,  450,  550,  1350,  500,  1400,  500,  1400,  500,  1350,  550,  400,  500,  450,  500,  450,  500,  1400,  500,  1350,  550,  400,  500,  450,  550,  0};
extern const uint16_t rawCool25On[] = {3900,  1850,  550,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  400,  550,  1400,  500,  400,  550,  1350,  500,  1400,  500,  400,  550,  1400,  500,  400,  550,  1350,  500,  1400,  500,  1400,  500,  1350,  550,  400,  500,  450,  550,  1350,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  550,  1350,  550,  1350,  500,  1400,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  1350,  550,  0};
extern const uint16_t rawCool26On[] = {3900,  1850,  550,  400,  550,  1350,  550,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  400,  550,  1400,  500,  1350,  550,  400,  550,  1350,  500,  400,  550,  1400,  500,  1350,  500,  1400,  500,  1400,  500,  400,  550,  400,  550,  1350,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  550,  1350,  550,  400,  500,  450,  500,  450,  450,  450,  550,  1400,  500,  400,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  400,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  550,  1350,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  500,  450,  550,  1350,  500,  1400,  500,  1400,  450,  1400,  550,  400,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  1350,  550,  0};
extern const uint16_t rawCool27On[] = {3900,  1850,  500,  450,  550,  1350,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  1400,  500,  1350,  550,  1350,  500,  450,  500,  450,  500,  1400,  500,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  500,  450,  550,  1350,  500,  1400,  500,  1400,  500,  1350,  550,  400,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  1350,  500,  1400,  550,  0};
extern const uint16_t rawCool28On[] = {3850,  1850,  550,  400,  550,  1350,  550,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  400,  550,  1400,  500,  1350,  550,  400,  550,  1350,  500,  400,  550,  1400,  500,  1350,  500,  1400,  500,  1400,  500,  400,  550,  400,  550,  1350,  500,  1400,  500,  450,  500,  400,  550,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  1400,  500,  400,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  1400,  500,  1400,  500,  1350,  500,  1400,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  550,  1350,  500,  1400,  550,  0};

extern const uint16_t rawCoolOff[] = {3900,  1850,  500,  450,  550,  1350,  500,  400,  550,  1350,  550,  400,  550,  1350,  500,  450,  500,  1400,  500,  400,  550,  1350,  500,  450,  550,  1350,  500,  1400,  500,  400,  550,  1350,  500,  450,  550,  1350,  500,  1400,  450,  1400,  550,  1350,  500,  450,  500,  400,  550,  1400,  500,  1350,  550,  400,  500,  450,  500,  450,  500,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  1400,  500,  1350,  550,  400,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  400,  550,  1400,  500,  400,  500,  450,  500,  450,  550,  1350,  500,  400,  550,  400,  550,  400,  550,  1350,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  1350,  550,  400,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  550,  1350,  500,  450,  500,  1400,  500,  400,  500,  450,  500,  450,  500,  450,  500,  450,  500,  450,  500,  400,  550,  400,  550,  400,  500,  450,  550,  1350,  500,  450,  500,  450,  500,  1400,  500,  1350,  550,  1350,  500,  1400,  500,  400,  550,  400,  550,  400,  500,  450,  500,  450,  500,  450,  500,  450,  550,  0};

// ────────────────────────  GLOBALS  ───────────────────────────────
MatterThermostat ThermostatEndpoint;

float measuredTemperature  = 24.0f;
float lastCoolingSetpoint  = 24.0f;
unsigned long lastSensorTs = 0;

// ────────────────────────  LED UTILS  ────────────────────────────
inline void ledShow(uint8_t g, uint8_t r, uint8_t b) {
    M5.dis.drawpix(0, (g << 16) | (r << 8) | b);
}

void ledFlash(uint8_t g, uint8_t r, uint8_t b, uint16_t dur = 120) {
    ledShow(g, r, b);
    delay(dur);
    ledShow(0, 0, 0);
}

// ────────────────────────  HELPERS  ──────────────────────────────
void sendTargetTemperature(int t) {

    struct RawMap {
        int                 temp;
        const uint16_t*     raw;
        uint16_t            len;
    };
    /* 温度 → raw 配列の対応表 */
    static const RawMap map[] PROGMEM = {
        {18, rawCool18On, sizeof(rawCool18On) / sizeof(uint16_t)},
        {19, rawCool19On, sizeof(rawCool19On) / sizeof(uint16_t)},
        {20, rawCool20On, sizeof(rawCool20On) / sizeof(uint16_t)},
        {21, rawCool21On, sizeof(rawCool21On) / sizeof(uint16_t)},
        {22, rawCool22On, sizeof(rawCool22On) / sizeof(uint16_t)},
        {23, rawCool23On, sizeof(rawCool23On) / sizeof(uint16_t)},
        {24, rawCool24On, sizeof(rawCool24On) / sizeof(uint16_t)},
        {25, rawCool25On, sizeof(rawCool25On) / sizeof(uint16_t)},
        {26, rawCool26On, sizeof(rawCool26On) / sizeof(uint16_t)},
        {27, rawCool27On, sizeof(rawCool27On) / sizeof(uint16_t)},
        {28, rawCool28On, sizeof(rawCool28On) / sizeof(uint16_t)},
    };

    /* 対応表から検索 */
    const RawMap* found = nullptr;
    for (auto &e : map) {
        if (e.temp == t) {
            found = &e;
            break;
        }
    }
    if (!found) {
        Serial.printf("[AC] Unsupported temp %d °C – IR code not sent\r\n", t);
        ledFlash(kLedBrightness, 0, 0);   // エラー表示
        return;
    }

    Serial.printf("[AC] Set cooling target to %d °C (IR)\r\n", t);
    ledFlash(0, 0, kLedBrightness);       // 送信確認

    /* 38 kHz のキャリアで送信 */
    IrSender.sendRaw(
        found->raw,
        found->len,
        38  // kHz
    );
    Serial.println("Sent IR code");
}

void sendOffCommand() {
    Serial.println("[AC] Turn OFF cooling (IR)");
    ledFlash(kLedBrightness, 0, 0);        // 送信確認

    /* 38 kHz キャリアで OFF コード送信 */
    IrSender.sendRaw(
        rawCoolOff,
        sizeof(rawCoolOff) / sizeof(uint16_t),
        38   // kHz
    );
    Serial.println("Sent IR code");
}

void connectWiFi() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(kSsid, kPassword);

    Serial.printf("Connecting to %s", kSsid);
    uint8_t tries = 0;
    while (WiFi.status() != WL_CONNECTED && tries < 20) {
        Serial.print('.');
        delay(500);
        ++tries;
    }
    if (WiFi.status() == WL_CONNECTED) {
        Serial.printf("\nConnected, IP=%s\r\n", WiFi.localIP().toString().c_str());
        ledFlash(0, kLedBrightness, 0);
    } else {
        Serial.println("\nWi-Fi connect failed, rebooting...");
        ledFlash(kLedBrightness, 0, 0, 300);
        delay(1000);
        ESP.restart();
    }
}

// ────────────────────────  SETUP  ────────────────────────────────
void setup() {
    M5.begin(false, false, true);  // UART無効, I2C無効, LED+Btn 有効
    ledShow(0, 0, kLedBrightness); // 青常灯
    Serial.begin(115200);
    delay(1200);
    ledShow(0, 0, 0);

    connectWiFi();

    // IR初期化
    IrSender.begin(IR_SEND_PIN);

    // Matter endpoint初期化
    ThermostatEndpoint.begin(MatterThermostat::THERMOSTAT_SEQ_OP_COOLING,
                             MatterThermostat::THERMOSTAT_AUTO_MODE_DISABLED);
    // Matter初期化
    Matter.begin();

    if (!Matter.isDeviceCommissioned()) {
        Serial.println("Matter node is not commissioned yet.");
        Serial.printf("Manual pairing code: %s\r\n", Matter.getManualPairingCode().c_str());
        Serial.printf("QR code URL: %s\r\n", Matter.getOnboardingQRCodeUrl().c_str());

        while (!Matter.isDeviceCommissioned()) {
            delay(100);
            Serial.print('.');
        }
        Serial.println("\nCommissioned. Ready.");
        ledFlash(0, kLedBrightness, 0);
    }

    ThermostatEndpoint.setMode(MatterThermostat::THERMOSTAT_MODE_OFF);

    esp_task_wdt_config_t wdt_config = {
        .timeout_ms = WDT_TIMEOUT * 1000,  // You had WDT_TIMEOUT = 10 sec → now in milliseconds
        .idle_core_mask = (1 << portNUM_PROCESSORS) - 1, // Watch all cores (dual-core ESP32)
        .trigger_panic = true,
    };
    esp_task_wdt_init(&wdt_config);
    esp_task_wdt_add(NULL); // add current task
}

// ────────────────────────  LOOP HELPERS  ─────────────────────────
void handleThermostat() {
    static float prevTarget = NAN;

    float requested = ThermostatEndpoint.getCoolingSetpoint();
    float target    = constrain(requested, kMinTargetTemp, kMaxTargetTemp);

    if (ThermostatEndpoint.getMode() != MatterThermostat::THERMOSTAT_MODE_OFF && target != prevTarget) {
        sendTargetTemperature(static_cast<int>(target));
        prevTarget = target;
        measuredTemperature = target;
        ThermostatEndpoint.setLocalTemperature(measuredTemperature);
    }

    static bool wasOff = true;
    bool isOff = (ThermostatEndpoint.getMode() == MatterThermostat::THERMOSTAT_MODE_OFF);

    if (isOff && !wasOff) {
        lastCoolingSetpoint = target;
        sendOffCommand();
    } else if (!isOff && wasOff) {
        Serial.println("[AC] Turn ON cooling");
        ledFlash(0, 0, kLedBrightness);
        sendTargetTemperature(static_cast<int>(lastCoolingSetpoint));
    }
    wasOff = isOff;
}

void handleButton() {
    // G39 フロントボタンの長押し判定 (kFactoryResetTimeoutMs 以上)
    if (M5.Btn.pressedFor(kFactoryResetTimeoutMs)) {
        Serial.println("Decommission requested → running Matter.decommission() ...");
        ledShow(kLedBrightness, 0, kLedBrightness); // 紫常灯
        Matter.decommission();
        delay(1500);
        ESP.restart();
    }
}

// ────────────────────────  MAIN LOOP  ────────────────────────────
void loop() {
    M5.update();
    handleThermostat();
    handleButton();
    esp_task_wdt_reset();
}

Discussion