Pular para conteúdo

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.yamlpre_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.yamlgen_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.yamlcanonical_roles section, or in menu.template.yaml.

1. Pick a role from the contract. If none fits — add it to mqtt_contract.yamlcanonical_roles first, then run regen.sh:

canonical_roles:
  my.action: { type: action, widget: button }

2. Add an item to menu.yaml:

- id: my_action
  type: action
  role: my.action
  title: { ru: "МОЁ ДЕЙСТВИЕ", en: "MY ACTION" }

3. Handle it in firmware (main.cpp):

if (action == "my.action") { /* do the thing */ }

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: to menu.yaml — the widget is determined by the contract via role:, not by the firmware
  • Don't edit mqtt-api.types.ts by hand — it is generated by regen.sh
  • Don't touch Config.hasXxx flags for new actions — those are only for telemetry (sensors, states)