Menu as Protocol: menu.yaml ↔ mqtt_contract.yaml ↔ Portal¶
Three files — three roles¶
| File | Owner | Describes |
|---|---|---|
src/menu/menu.yaml |
your product | device menu: parameters, actions, structure |
contracts/mqtt_contract.yaml |
idryer-core | list of known meanings: canonical_roles with labels in several languages |
frontend-v2/src/contracts/mqtt-api.types.ts |
generated | TypeScript types for the portal |
role: — a semantic name for a menu item. The firmware says "I have iheater.heat_temp" rather than "I have item number 35". Internal firmware names can change, role: stays fixed.
The menu is the mirror of the device settings. The firmware publishes every item of menu.yaml, with or without role:. The portal draws each item by its type: value, toggle, action, submenu. role: gives the item a label from the contract in the user's language; without role: the portal shows the name the device sent.
The menu does not build the device card. What the card shows and which operations it starts is described by the card manifest — see Device card: the card manifest. A card action can take the limits and the default of its parameter from a menu item.
1. Firmware build (pio run)¶
menu.yaml → menu_gen.py checks every role: against canonical_roles in the contract → if a role is unknown, the build fails with an error and a list of valid roles → the generator writes C++ files into src/menu/.
2. Updating TypeScript for the portal (regen.sh)¶
mqtt_contract.yaml → gen_ts_types.py generates mqtt-api.types.ts and the role labels roles.{lang}.json → the files are copied to the portal.
Run it when the contract changes. Commit the result.
3. Runtime: device ↔ portal¶
The firmware publishes the menu to the config topic (the product code does it on the get_config command; iDryer products also do it when they come online) → the portal backend stores it → the portal gets it with GET /devices/:id/menu-config → each item is drawn by its type t (val, tog, act, sub), the label is canonical_roles[r].labels[lang], then English, then the name n from the device.
Parameters (min, max, val) come from the menu item itself — the firmware knows the current values.
The portal changes a value with commands/set { "id": <id>, "val": <value> }.
How to add a setting (NVS parameter)¶
- id: my_param
type: value
role: my.param # optional: a label from the contract
title: { ru: "ПАРАМЕТР", en: "PARAM" }
unit: { ru: "°C", en: "°C" }
vtype: uint16
min: 0
max: 100
step: 1
bind: my_param # NVS key (≤ 15 chars)
persist: true
scope: global
default: 50
bind = NVS key. persist: true = the value survives a reboot.
role: is not a free-form field: the value must come from canonical_roles in the contract, otherwise the build fails. See the list in contracts/mqtt_contract.yaml → canonical_roles or in menu.template.yaml. A new role is added to the contract first, then regen.sh.
How to add an operation to the device card¶
Operations (start, stop, heat, light) are declared as card actions, not as menu items. Their limits can be taken from a menu item:
idryer::card_menu::attach(s_link.card());
s_link.card().action("storage", "STORAGE", onStorage)
.param("temperature", "target_temperature", MENU_TARGET_TEMP);
s_link.card().action("stop", "IDLE", onStop);
The full description — Device card: the card manifest.
What NOT to do¶
- Don't add
widget:tomenu.yaml. Thewidgetfield ofcanonical_rolesis reference data: the portal and the app do not read it. - Don't edit
mqtt-api.types.tsby hand — it is generated byregen.sh. - Don't touch
Config.hasXxxflags for new actions — those are only for telemetry (sensors, states).