diff --git a/firmware/README-menu.md b/firmware/README-menu.md new file mode 100644 index 0000000..bf4b2b2 --- /dev/null +++ b/firmware/README-menu.md @@ -0,0 +1,257 @@ +# menu.json Reference + +The `storage/menu.json` file defines the complete menu structure loaded from SPIFFS at runtime. +It is parsed by the `Mercedes` component and rendered by `Hermes`. + +--- + +## Top-level structure + +```json +{ + "screens": [ , ... ] +} +``` + +| Field | Type | Required | Description | +|-----------|-----------------|----------|------------------------------------| +| `screens` | array of Screen | yes | List of all screens in the menu | + +The **first** screen in the array becomes the root/home screen. + +--- + +## Screen + +```json +{ + "id": "main_menu", + "title": "Hauptmenü", + "items": [ , ... ] +} +``` + +| Field | Type | Required | Description | +|---------|----------------|----------|----------------------------------------------| +| `id` | string | yes | Unique identifier, referenced by `targetScreenId` | +| `title` | string | yes | Displayed as the screen heading | +| `items` | array of Item | yes | List of menu items shown on this screen | + +--- + +## Item (common fields) + +Every item shares these base fields: + +| Field | Type | Required | Description | +|----------------|---------|----------|----------------------------------------------------------------| +| `id` | string | yes | Unique identifier across all screens | +| `type` | string | yes | Item type — see types below | +| `label` | string | yes | Text shown in the menu | +| `persistent` | bool | no | If `true`, value is read/written to NVS and broadcast via WebSocket. Default: `false` | +| `valueType` | string | no | NVS storage type: `"bool"`, `"int"`, or `"string"`. Required when `persistent: true` for toggle and selection items | +| `visibleWhen` | object | no | Conditional visibility — see below | + +--- + +## Item types + +### `label` + +Read-only text. Can display a static string or a dynamic value via the `ItemValueProvider`. + +```json +{ + "id": "mac_suffix", + "type": "label", + "label": "Device-ID" +} +``` + +No additional fields. Never persistent. + +--- + +### `action` + +Executes an action when SELECT is pressed. Fires `action_manager_execute` with the `actionTopic`. + +```json +{ + "id": "ota_update", + "type": "action", + "label": "OTA Einspielen", + "actionTopic": "home/settings/ota_update" +} +``` + +| Field | Type | Required | Description | +|---------------|--------|----------|------------------------------------------| +| `actionTopic` | string | yes | Action key passed to `action_manager_execute` | + +--- + +### `submenu` + +Navigates to another screen when SELECT is pressed. + +```json +{ + "id": "menu_lights", + "type": "submenu", + "label": "Lichtsteuerung", + "targetScreenId": "lights_menu" +} +``` + +| Field | Type | Required | Description | +|------------------|--------|----------|-----------------------------------| +| `targetScreenId` | string | yes | `id` of the screen to navigate to | + +--- + +### `toggle` + +Boolean on/off switch. Toggled with SELECT. Displays an indicator when active. + +```json +{ + "id": "light_active", + "type": "toggle", + "label": "Einschalten", + "persistent": true, + "valueType": "bool", + "actionTopic": "home/lights/activate" +} +``` + +| Field | Type | Required | Description | +|---------------|--------|----------|------------------------------------------------------------------| +| `persistent` | bool | no | Saves state to NVS and broadcasts via WebSocket | +| `valueType` | string | yes (if persistent) | Must be `"bool"` | +| `actionTopic` | string | no | If set, fires `action_manager_execute` on change | + +--- + +### `selection` + +Cycles through a list of options using LEFT/RIGHT. The selected `value` is used for NVS storage. + +```json +{ + "id": "light_mode", + "type": "selection", + "label": "Modus", + "persistent": true, + "valueType": "int", + "items": [ + { "value": "1", "label": "Tag" }, + { "value": "2", "label": "Nacht" }, + { "value": "0", "label": "Simulation" } + ] +} +``` + +| Field | Type | Required | Description | +|--------------|-------------------------|----------|------------------------------------------------------------------| +| `persistent` | bool | no | Saves selected value to NVS and broadcasts via WebSocket | +| `valueType` | string | yes (if persistent) | `"int"` or `"string"` depending on NVS storage type | +| `actionTopic`| string | no | If set, fires `action_manager_execute` with the selected value | +| `items` | array of SelectionItem | yes | At least one entry required | + +**SelectionItem:** + +| Field | Type | Required | Description | +|---------|--------|----------|--------------------------------------------------| +| `value` | string | yes | Value written to NVS (converted via `valueType`) | +| `label` | string | yes | Text shown in the menu | + +The **first** item in the array is the default when no NVS value exists. + +--- + +## `visibleWhen` + +Makes an item visible only when another item has a specific value. + +```json +{ + "visibleWhen": { + "itemId": "light_mode", + "value": "0" + } +} +``` + +| Field | Type | Required | Description | +|----------|--------|----------|----------------------------------------------------------| +| `itemId` | string | yes | `id` of the controlling item (must be on any screen) | +| `value` | string | yes | The controlling item's value that makes this item visible | + +Works with `toggle` (compare against `"true"`/`"false"`) and `selection` (compare against the selected `value`). +Hidden items are skipped during UP/DOWN navigation. + +--- + +## Complete example + +```json +{ + "screens": [ + { + "id": "main_menu", + "title": "Hauptmenü", + "items": [ + { + "id": "menu_lights", + "type": "submenu", + "label": "Lichtsteuerung", + "targetScreenId": "lights_menu" + } + ] + }, + { + "id": "lights_menu", + "title": "Lichtsteuerung", + "items": [ + { + "id": "light_active", + "type": "toggle", + "label": "Einschalten", + "persistent": true, + "valueType": "bool", + "actionTopic": "home/lights/activate" + }, + { + "id": "light_mode", + "type": "selection", + "label": "Modus", + "persistent": true, + "valueType": "int", + "items": [ + { "value": "1", "label": "Tag" }, + { "value": "2", "label": "Nacht" }, + { "value": "0", "label": "Simulation" } + ] + }, + { + "id": "light_variant", + "type": "selection", + "label": "Variante", + "persistent": true, + "valueType": "int", + "visibleWhen": { + "itemId": "light_mode", + "value": "0" + }, + "items": [ + { "value": "1", "label": "Standard" }, + { "value": "2", "label": "Warm" }, + { "value": "3", "label": "Natur" } + ] + } + ] + } + ] +} +```