デバイスカード: card マニフェスト¶
ポータルとモバイルアプリは、どのデバイスのカードもその card マニフェスト から組み立てます。マニフェストはファームウェアが自分自身について公開する記述で、何を表示し、何を操作できるかを示します。新しいデバイスタイプに、ポータルやアプリのコードは必要ありません。
マニフェストは iDryer::Link ファサード(s_link.card())の一部です。低レベルのランタイム(IdryerRuntime)はマニフェストを公開しません。
メニューとカードは別物です。メニューはデバイスの設定の鏡で、ポータルで変更した値はデバイスのメモリに書き込まれます。カードは測定値を表示し、操作を開始します。起動パラメータは 1 回のコマンドと一緒に送られ、メニューには書き込まれません。起動パラメータは、範囲とデフォルト値をメニュー項目から取ることができます。
仕組み¶
ファームウェア: s_link.card() の宣言
│ コアが JSON を組み立て、idryer/{key}/card に retained (QoS 1) で公開
▼
ポータルのバックエンド: マニフェストを検証(上限、許可された型とフィールド)して保存
▼
ポータルとアプリ: カードを描画
│ ユーザーがボタンを押す
▼
idryer/{key}/commands/invoke {"unitId":"U1","action":"card.<id>","args":{…}}
▼
コアが "card.<id>" をあなたのコールバックに渡す
コアは MQTT 接続が確立した後にマニフェストを公開し、宣言や、宣言が依存するメニュー項目が変わると再公開します。
エンティティ: 何を表示するか¶
エコシステムの語彙にあるセンサーは Config のフラグから追加されます。宣言は不要です:
Config フラグ |
カードのセル |
|---|---|
hasAirTemp |
空気温度 |
hasAirHumidity |
湿度 |
hasHeaterTemp |
ヒーター温度 |
hasHeater |
ヒーター出力 |
hasFan |
ファン オン / オフ |
hasServo |
ダンパー 開 / 閉 |
hasWeight |
重量モジュール(topic weights) |
hasRfid |
ユニットに RFID リーダーがある |
独自の値: テレメトリに追加し、JSON パス付きのセンサーを宣言します:
s_link.onTelemetryPublish([](JsonObject root) {
root["units"][0]["co2ppm"] = g_co2;
});
s_link.card().sensor("co2", "CO2", "ppm", "units[0].co2ppm");
シンプルな操作部品もエンティティです: button、number、select。ユーザーが値を変えるとすぐに送信され、コアがコールバックを呼びます:
static const char* kModes[] = { "auto", "on", "off" };
s_link.card().select("mode", "Mode", kModes, 3, [](const char* opt) { onMode(opt); });
s_link.card().number("threshold", "Threshold", 100, 400, 10, "", [](float v) { onThreshold(v); });
s_link.card().button("purge", "Purge", []() { onPurge(); });
アクション: 起動パラメータ付きの操作¶
アクションはデバイスの操作です: 乾燥の開始、加熱、照明の点灯、停止。アクションには モード(アクション後のユニットのモード、status.units[].mode)と、意味(purpose)を持つ パラメータ があります。何を表示するかは、カードがユニットの現在のモードで決めます:
- ユニットのモードがあるアクションのモードと一致 → デバイスはその作業中: セッションブロックと停止ボタン(モード
IDLEのアクション); - それ以外 → 起動フォーム。起動アクションが複数あればモード切り替え。
例: ESP32 1 台で作る加温式の保管キャビネット。目標温度はメニュー項目 target_temp(30〜50 °C、デフォルト 45)です。
#include <menu_meta.h>
#include <menu_cache.h>
#include <menu_bindings.h> // menu_sync_state_to_cache
#include <card/card_menu_bridge.h>
static void onStorage(uint8_t unit, JsonObjectConst args) {
s_targetC = args["temperature"].as<float>(); // すでに 30..50 の範囲内
s_link.status.mode[unit] = iDryer::UnitMode::Storage;
s_link.status.targetTempC[unit] = s_targetC;
s_link.publishStatusNow();
}
static void onStop(uint8_t unit, JsonObjectConst) {
s_link.status.mode[unit] = iDryer::UnitMode::Idle;
s_link.status.targetTempC[unit] = 0.0f;
s_link.publishStatusNow();
}
void setup() {
menu.initDefaults();
menu.loadFromNVS();
menu_sync_state_to_cache(); // メニューの値をカードが読むキャッシュへ
s_link.begin();
auto& card = s_link.card();
idryer::card_menu::attach(card);
card.action("storage", "STORAGE", onStorage)
.name("ru", "Хранение").name("en", "Storage")
.param("temperature", "target_temperature", MENU_TARGET_TEMP);
card.action("stop", "IDLE", onStop)
.name("ru", "Стоп").name("en", "Stop");
}
コールバックが受け取るもの:
- 数値はユニットの範囲に収められ、欠けている数値はデフォルト値に置き換えられる;
unit—unitIdから得たユニットのインデックス("U1"→ 0)。デバイスにないユニット宛てのコマンドは無視される;- 起動後は
status.modeを設定してpublishStatusNow()を呼ぶ。ステータスを見てカードがセッションブロックに切り替わる。
アクション API¶
| 呼び出し | 動作 |
|---|---|
card.action(id, mode, cb) |
アクション。mode — 実行後のユニットのモード、nullptr — モードは変わらない |
.param(id, purpose, MENU_ID) |
数値: 範囲、ステップ、デフォルト値、単位をメニュー項目から取る |
.param(id, purpose, min, max, step, def[, unit]) |
独自の範囲を持つ数値 |
.ceiling(MENU_ID) |
直前のパラメータの上限を、そのメニュー項目の値にする(例: 最大空気温度) |
.stages(id, "stages", MENU_ID) |
プロファイルの段階 [{temperature, ramp, hold}]、秒。段階の温度はメニュー項目の範囲内 |
.select(id, purpose, options, count[, def]) |
リストからの選択。リスト外の値はデフォルトに置き換え |
.color(id, purpose, "#FFFFFF") |
色 #RRGGBB |
.deviceClass("identify")、.deviceClass("clear_errors") |
ヘッダーの常設ボタン: デバイスの呼び出し、エラーのクリア |
.name("ru", "…").name("en", "…") |
カードがモードを知らないときのアクション名 |
purpose の値: target_temperature、target_humidity、duration(分、0 — 無制限)、stages、start_stage(0 から)、effect、rgb_color。カードはこれをフィールドのラベルとポータルの機能に使います。乾燥プリセットはモード DRYING のアクションの target_temperature と duration を埋め、乾燥プロファイルは stages を埋めます。
コールバックは関数、またはキャプチャなしのラムダです。name の文字列と選択肢はポインタで保持されるため、リテラルか静的配列を使ってください。
ポータルに送られる内容¶
上のキャビネットは次を公開します(Config: hasAirTemp、hasAirHumidity、hasHeaterTemp、hasHeater、hasFan):
{
"v": 2,
"entities": [
{"id": "temp", "type": "sensor", "device_class": "temperature", "unit": "°C", "source": "telemetry", "path": "units[0].temperature"},
{"id": "humidity", "type": "sensor", "device_class": "humidity", "unit": "%", "source": "telemetry", "path": "units[0].humidity"},
{"id": "heater_temp", "type": "sensor", "device_class": "heater_temp", "unit": "°C", "source": "telemetry", "path": "units[0].heaterTemp"},
{"id": "power", "type": "sensor", "device_class": "power", "unit": "%", "source": "telemetry", "path": "units[0].heaterPower"},
{"id": "fan", "type": "binary_sensor", "device_class": "fan", "source": "telemetry", "path": "units[0].fanStatus"}
],
"actions": [
{"id": "storage", "mode": "STORAGE", "name": {"ru": "Хранение", "en": "Storage"}, "action": "card.storage",
"params": [{"id": "temperature", "purpose": "target_temperature", "type": "number",
"limits": [30, 50], "step": 1, "default": 45, "unit": "°C"}]},
{"id": "stop", "mode": "IDLE", "name": {"ru": "Стоп", "en": "Stop"}, "action": "card.stop"}
]
}
limits と default はメニュー項目から来ています。.ceiling() を使うと、パラメータに max_by(上限を切り下げたメニュー項目のタイトル)も付き、値が上限を超えたときにカードがその名前を示します。
ポータルとアプリが描画する内容¶
スクリーンショットではなく図です。待機中:
┌─ DIY Storage Cabinet ─────────── [アイドル] ─┐
│ | 24.8 °C | 41 % | 25.1 °C | │ ← 温度、湿度、ヒーター
│ | 0 % | オフ | │ ← 出力、ファン(グレー: 0 / オフ)
│ [温度 45 °C ] [保管] │ ← アクション "storage"
└──────────────────────────────────────────────┘
起動後、デバイスはモード STORAGE を報告し、同じカードがセッションブロック(目標温度と保管の経過時間)と停止ボタン(モード IDLE のアクション)を表示します。
両方に共通のルール:
- 値がないセルはグレー。出力は 0 のときもグレー。ファンとダンパーはオフ / 閉のときグレー;
- 範囲外の値は置き換えられない。起動はブロックされ、フォームの下に理由が表示される;
- マニフェストにアクションがあるファームウェアでは、呼び出しとエラークリアのボタンは
identify/clear_errorsのアクションを宣言したときだけ表示される。
カードが表示される場所:
| ポータル | アプリ | |
|---|---|---|
| iDryer 製品ではないデバイスタイプ | マニフェストから作るダッシュボードのカード(アクション付き)。デバイスページ — 測定値とアクション(マニフェストにアクションがある場合) | ホーム — 測定値。アクション — デバイスページ |
deviceType = Dryer |
同じマニフェストのアクションを持つ乾燥機カード | ホーム — 監視。アクション — デバイスページ |
上限¶
| コア | ポータルのバックエンド | |
|---|---|---|
| エンティティ | 宣言 16 + 自動 | 32 |
| layout の行 | 8 | 16 |
| 1 行の id | 4 | 4 |
| アクション | 8 | 8 |
| アクションのパラメータ | 4(IDRYER_CARD_MAX_PARAMS) |
6 |
name の言語 |
ru、en |
最大 4 |
id: 小文字のラテン文字、数字、_、最大 24 文字。エンティティとアクションは同じ名前空間を共有します。マニフェストのドキュメントは 4096 バイトまでで、収まらない場合、コアはログに manifest overflows と出力して公開しません。
フォーマットそのもの — contracts/mqtt_contract.yaml、セクション mqtt_only、suffix: card。