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: what each role: means and how the portal displays it |
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_start" rather than "I have button number 35". This is the stable contract between device and portal — internal firmware names can change, role: stays fixed.
Widget — how the portal displays this item: a button, slider, toggle, or a complex component (color picker, profile editor). Determined by the contract via role:, not by the firmware.
A menu item with role: is visible to the portal. Without role: — private, shown only on the device display.
1. Firmware build (pio run)¶
menu.yaml → pre_gen_menu.py validates 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 → menu_gen.py generates C++ files into src/menu/
Validation is built into the build step — it is physically impossible to use a non-existent role silently.
2. Updating TypeScript for the portal (regen.sh)¶
mqtt_contract.yaml → gen_ts_types.py generates mqtt-api.types.ts → file is copied to frontend-v2/src/contracts/
Run manually when the contract changes. Commit the result.
3. Runtime: device ↔ portal¶
Device connects → publishes menu to MQTT topic config → portal reads each item with field r: → looks up CanonicalRoles[r].widget → renders widget from WIDGET_REGISTRY.
Parameters (min, max, val) come from the menu item itself — the firmware knows the current values.
How to add a new action to the portal dashboard¶
role: is not a free-form field. The value must come from the closed list in canonical_roles in the contract. You cannot invent a role on the fly — the build will fail. See available roles in contracts/mqtt_contract.yaml → canonical_roles section, or in menu.template.yaml.
1. Pick a role from the contract. If none fits — add it to mqtt_contract.yaml → canonical_roles first, then run regen.sh:
2. Add an item to menu.yaml:
3. Handle it in firmware (main.cpp):
pio run → validation → C++ → firmware publishes r: "my.action" → portal renders a button.
How to add a setting (NVS parameter)¶
- id: my_param
type: value
role: my.param # only if it should appear on the portal; omit for display-only
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 = value survives reboot.
Portal changes the value via commands/set { "id": <id>, "val": <value> }.
What NOT to do¶
- Don't add
widget:tomenu.yaml— the widget is determined by the contract viarole:, not by the firmware - 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)