[PATCH 37/40] expo: Support building an expo from a description file

Simon Glass sjg at chromium.org
Thu Jun 1 18:23:01 CEST 2023


The only way to create an expo at present is by calling the functions to
create each object. It is useful to have more data-driven approach, where
the objects can be specified in a suitable file format and created from
that. This makes testing easier as well.

Add support for describing an expo in a devicetree node. This allows more
complex tests to be set up, as well as providing an easier format for
users. It also provides a better basis for the upcoming configuration
editor.

Signed-off-by: Simon Glass <sjg at chromium.org>
---

 arch/sandbox/dts/cedit.dtsi |  64 ++++++
 arch/sandbox/dts/test.dts   |   5 +
 boot/Makefile               |   2 +-
 boot/expo.c                 |   1 +
 boot/expo_build.c           | 400 ++++++++++++++++++++++++++++++++++++
 doc/develop/expo.rst        | 282 ++++++++++++++++++++++++-
 include/expo.h              |  14 ++
 include/test/cedit-test.h   |  29 +++
 test/boot/expo.c            |  80 ++++++++
 9 files changed, 870 insertions(+), 7 deletions(-)
 create mode 100644 arch/sandbox/dts/cedit.dtsi
 create mode 100644 boot/expo_build.c
 create mode 100644 include/test/cedit-test.h

diff --git a/arch/sandbox/dts/cedit.dtsi b/arch/sandbox/dts/cedit.dtsi
new file mode 100644
index 000000000000..a9eb4c2d594a
--- /dev/null
+++ b/arch/sandbox/dts/cedit.dtsi
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Expo definition for the configuration editor
+ *
+ * This used for testing building an expo from a data file. This devicetree
+ * provides a description of the objects to be created.
+ */
+
+#include <test/cedit-test.h>
+
+&cedit {
+	dynamic-start = <ID_DYNAMIC_START>;
+
+	scenes {
+		main {
+			id = <ID_SCENE1>;
+
+			/* value refers to the matching id in /strings */
+			title-id = <ID_SCENE1_TITLE>;
+
+			/* simple string is used as it is */
+			prompt = "UP and DOWN to choose, ENTER to select";
+
+			/* defines a menu within the scene */
+			cpu-speed {
+				type = "menu";
+				id = <ID_CPU_SPEED>;
+
+				/*
+				 * has both string and ID. The string is ignored
+				 * if the ID is present and points to a string
+				 */
+				title = "CPU speed";
+				title-id = <ID_CPU_SPEED_TITLE>;
+
+				/* menu items as simple strings */
+				item-label = "2 GHz", "2.5 GHz", "3 GHz";
+
+				/* IDs for the menu items */
+				item-id = <ID_CPU_SPEED_1 ID_CPU_SPEED_2
+					ID_CPU_SPEED_3>;
+			};
+
+			power-loss {
+				type = "menu";
+				id = <ID_POWER_LOSS>;
+
+				title = "AC Power";
+				item-label = "Always Off", "Always On",
+					"Memory";
+
+				item-id = <ID_AC_OFF ID_AC_ON ID_AC_MEMORY>;
+			};
+		};
+	};
+
+	strings {
+		title {
+			id = <ID_SCENE1_TITLE>;
+			value = "Test Configuration";
+			value-es = "configuración de prueba";
+		};
+	};
+};
diff --git a/arch/sandbox/dts/test.dts b/arch/sandbox/dts/test.dts
index 38d5739421f6..442222e4b995 100644
--- a/arch/sandbox/dts/test.dts
+++ b/arch/sandbox/dts/test.dts
@@ -141,6 +141,9 @@
 		};
 	};
 
+	cedit: cedit {
+	};
+
 	fuzzing-engine {
 		compatible = "sandbox,fuzzing-engine";
 	};
@@ -1830,3 +1833,5 @@
 #ifdef CONFIG_SANDBOX_VPL
 #include "sandbox_vpl.dtsi"
 #endif
+
+#include "cedit.dtsi"
diff --git a/boot/Makefile b/boot/Makefile
index f94c31d922de..28c4e55ca656 100644
--- a/boot/Makefile
+++ b/boot/Makefile
@@ -50,7 +50,7 @@ ifdef CONFIG_SPL_BUILD
 obj-$(CONFIG_SPL_LOAD_FIT) += common_fit.o
 endif
 
-obj-$(CONFIG_$(SPL_TPL_)EXPO) += expo.o scene.o scene_menu.o
+obj-$(CONFIG_$(SPL_TPL_)EXPO) += expo.o scene.o scene_menu.o expo_build.o
 
 obj-$(CONFIG_$(SPL_TPL_)BOOTMETH_VBE) += vbe.o
 obj-$(CONFIG_$(SPL_TPL_)BOOTMETH_VBE_REQUEST) += vbe_request.o
diff --git a/boot/expo.c b/boot/expo.c
index 8c6fbc0e30bc..db837f7b4924 100644
--- a/boot/expo.c
+++ b/boot/expo.c
@@ -58,6 +58,7 @@ void expo_destroy(struct expo *exp)
 
 uint resolve_id(struct expo *exp, uint id)
 {
+	log_debug("resolve id %d\n", id);
 	if (!id)
 		id = exp->next_id++;
 	else if (id >= exp->next_id)
diff --git a/boot/expo_build.c b/boot/expo_build.c
new file mode 100644
index 000000000000..7e61ab06a8da
--- /dev/null
+++ b/boot/expo_build.c
@@ -0,0 +1,400 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Building an expo from an FDT description
+ *
+ * Copyright 2022 Google LLC
+ * Written by Simon Glass <sjg at chromium.org>
+ */
+
+#define LOG_CATEGORY	LOGC_EXPO
+
+#include <common.h>
+#include <expo.h>
+#include <fdtdec.h>
+#include <log.h>
+#include <malloc.h>
+#include <dm/ofnode.h>
+#include <linux/libfdt.h>
+
+/**
+ * struct build_info - Information to use when building
+ *
+ * @str_for_id: String for each ID in use, NULL if empty. The string is NULL
+ *	if there is nothing for this ID. Since ID 0 is never used, the first
+ *	element of this array is always NULL
+ * @str_count: Number of entries in @str_for_id
+ */
+struct build_info {
+	const char **str_for_id;
+	int str_count;
+};
+
+/**
+ * add_txt_str - Add a string or lookup its ID, then add to expo
+ *
+ * @info: Build information
+ * @node: Node describing scene
+ * @scn: Scene to add to
+ * @find_name: Name to look for (e.g. "title"). This will find a property called
+ * "title" if it exists, else will look up the string for "title-id"
+ * Return: ID of added string, or -ve on error
+ */
+int add_txt_str(struct build_info *info, ofnode node, struct scene *scn,
+		const char *find_name, uint obj_id)
+{
+	const char *text;
+	uint str_id;
+	int ret;
+
+	text = ofnode_read_string(node, find_name);
+	if (!text) {
+		char name[40];
+		u32 id;
+
+		snprintf(name, sizeof(name), "%s-id", find_name);
+		ret = ofnode_read_u32(node, name, &id);
+		if (ret)
+			return log_msg_ret("id", -EINVAL);
+
+		if (id >= info->str_count)
+			return log_msg_ret("id", -E2BIG);
+		text = info->str_for_id[id];
+		if (!text)
+			return log_msg_ret("id", -EINVAL);
+	}
+
+	ret = expo_str(scn->expo, find_name, 0, text);
+	if (ret < 0)
+		return log_msg_ret("add", ret);
+	str_id = ret;
+
+	ret = scene_txt_str(scn, find_name, obj_id, str_id, text, NULL);
+	if (ret < 0)
+		return log_msg_ret("add", ret);
+
+	return ret;
+}
+
+/**
+ * add_txt_str_list - Add a list string or lookup its ID, then add to expo
+ *
+ * @info: Build information
+ * @node: Node describing scene
+ * @scn: Scene to add to
+ * @find_name: Name to look for (e.g. "title"). This will find a string-list
+ * property called "title" if it exists, else will look up the string in the
+ * "title-id" string list.
+ * Return: ID of added string, or -ve on error
+ */
+int add_txt_str_list(struct build_info *info, ofnode node, struct scene *scn,
+		     const char *find_name, int index, uint obj_id)
+{
+	const char *text;
+	uint str_id;
+	int ret;
+
+	ret = ofnode_read_string_index(node, find_name, index, &text);
+	if (ret) {
+		char name[40];
+		u32 id;
+
+		snprintf(name, sizeof(name), "%s-id", find_name);
+		ret = ofnode_read_u32_index(node, name, index, &id);
+		if (ret)
+			return log_msg_ret("id", -ENOENT);
+
+		if (id >= info->str_count)
+			return log_msg_ret("id", -E2BIG);
+		text = info->str_for_id[id];
+		if (!text)
+			return log_msg_ret("id", -EINVAL);
+	}
+
+	ret = expo_str(scn->expo, find_name, 0, text);
+	if (ret < 0)
+		return log_msg_ret("add", ret);
+	str_id = ret;
+
+	ret = scene_txt_str(scn, find_name, obj_id, str_id, text, NULL);
+	if (ret < 0)
+		return log_msg_ret("add", ret);
+
+	return ret;
+}
+
+/*
+ * build_element() - Handle creating a text object from a label
+ *
+ * Look up a property called @label or @label-id and create a string for it
+ */
+int build_element(void *ldtb, int node, const char *label)
+{
+	return 0;
+}
+
+/**
+ * read_strings() - Read in the list of strings
+ *
+ * Read the strings into an ID-indexed list, so they can be used for building
+ * an expo. The strings are in a /strings node and each has its own subnode
+ * containing the ID and the string itself:
+ *
+ * example {
+ *    id = <123>;
+ *    value = "This is a test";
+ * };
+ *
+ * Future work may add support for unicode and multiple languages
+ *
+ * @info: Build information
+ * @root: Root node to read from
+ * Returns: 0 if OK, -ENOMEM if out of memory, -EINVAL if there is a format
+ * error
+ */
+static int read_strings(struct build_info *info, ofnode root)
+{
+	ofnode strings, node;
+
+	strings = ofnode_find_subnode(root, "strings");
+	if (!ofnode_valid(strings))
+		return log_msg_ret("str", -EINVAL);
+
+	ofnode_for_each_subnode(node, strings) {
+		const char *val;
+		int ret;
+		u32 id;
+
+		ret = ofnode_read_u32(node, "id", &id);
+		if (ret)
+			return log_msg_ret("id", -EINVAL);
+		val = ofnode_read_string(node, "value");
+		if (!val)
+			return log_msg_ret("val", -EINVAL);
+
+		if (id >= info->str_count) {
+			int new_count = info->str_count + 20;
+			void *new_arr;
+
+			new_arr = realloc(info->str_for_id,
+					  new_count * sizeof(char *));
+			if (!new_arr)
+				return log_msg_ret("id", -ENOMEM);
+			memset(new_arr + info->str_count, '\0',
+			       (new_count - info->str_count) * sizeof(char *));
+			info->str_for_id = new_arr;
+			info->str_count = new_count;
+		}
+
+		info->str_for_id[id] = val;
+	}
+
+	return 0;
+}
+
+/**
+ * list_strings() - List the available strings with their IDs
+ *
+ * @info: Build information
+ */
+static void list_strings(struct build_info *info)
+{
+	int i;
+
+	for (i = 0; i < info->str_count; i++) {
+		if (info->str_for_id[i])
+			printf("%3d %s\n", i, info->str_for_id[i]);
+	}
+}
+
+/**
+ * menu_build() - Build a menu and add it to a scene
+ *
+ * See doc/developer/expo.rst for a description of the format
+ *
+ * @info: Build information
+ * @node: Node containing the menu description
+ * @scn: Scene to add the menu to
+ * Returns: 0 if OK, -ENOMEM if out of memory, -EINVAL if there is a format
+ * error, -ENOENT if there is a references to a non-existent string
+ */
+static int menu_build(struct build_info *info, ofnode node, struct scene *scn)
+{
+	struct scene_obj_menu *menu;
+	uint title_id, menu_id;
+	const u32 *item_ids;
+	int ret, size, i;
+	const char *name;
+	u32 id;
+
+	name = ofnode_get_name(node);
+	ret = ofnode_read_u32(node, "id", &id);
+	if (ret)
+		return log_msg_ret("id", -EINVAL);
+
+	ret = scene_menu(scn, name, id, &menu);
+	if (ret < 0)
+		return log_msg_ret("men", ret);
+	menu_id = ret;
+
+	/* Set the title */
+	ret = add_txt_str(info, node, scn, "title", 0);
+	if (ret < 0)
+		return log_msg_ret("tit", ret);
+	title_id = ret;
+	ret = scene_menu_set_title(scn, menu_id, title_id);
+
+	item_ids = ofnode_read_prop(node, "item-id", &size);
+	if (!item_ids)
+		return log_msg_ret("itm", -EINVAL);
+	if (!size || size % sizeof(u32))
+		return log_msg_ret("isz", -EINVAL);
+	size /= sizeof(u32);
+
+	for (i = 0; i < size; i++) {
+		struct scene_menitem *item;
+		uint label, key, desc;
+
+		ret = add_txt_str_list(info, node, scn, "item-label", i, 0);
+		if (ret < 0 && ret != -ENOENT)
+			return log_msg_ret("lab", ret);
+		label = max(0, ret);
+
+		ret = add_txt_str_list(info, node, scn, "key-label", i, 0);
+		if (ret < 0 && ret != -ENOENT)
+			return log_msg_ret("key", ret);
+		key = max(0, ret);
+
+		ret = add_txt_str_list(info, node, scn, "desc-label", i, 0);
+		if (ret < 0  && ret != -ENOENT)
+			return log_msg_ret("lab", ret);
+		desc = max(0, ret);
+
+		ret = scene_menuitem(scn, menu_id, simple_xtoa(i),
+				     fdt32_to_cpu(item_ids[i]), key, label,
+				     desc, 0, 0, &item);
+		if (ret < 0)
+			return log_msg_ret("mi", ret);
+	}
+
+	return 0;
+}
+
+/**
+ * menu_build() - Build an expo object and add it to a scene
+ *
+ * See doc/developer/expo.rst for a description of the format
+ *
+ * @info: Build information
+ * @node: Node containing the object description
+ * @scn: Scene to add the object to
+ * Returns: 0 if OK, -ENOMEM if out of memory, -EINVAL if there is a format
+ * error, -ENOENT if there is a references to a non-existent string
+ */
+static int obj_build(struct build_info *info, ofnode node, struct scene *scn)
+{
+	const char *type;
+	u32 id;
+	int ret;
+
+	log_debug("- object %s\n", ofnode_get_name(node));
+	ret = ofnode_read_u32(node, "id", &id);
+	if (ret)
+		return log_msg_ret("id", -EINVAL);
+
+	type = ofnode_read_string(node, "type");
+	if (!type)
+		return log_msg_ret("typ", -EINVAL);
+
+	if (!strcmp("menu", type))
+		ret = menu_build(info, node, scn);
+	 else
+		ret = -EINVAL;
+	if (ret)
+		return log_msg_ret("bld", ret);
+
+	return 0;
+}
+
+/**
+ * scene_build() - Build a scene and all its objects
+ *
+ * See doc/developer/expo.rst for a description of the format
+ *
+ * @info: Build information
+ * @node: Node containing the scene description
+ * @scn: Scene to add the object to
+ * Returns: 0 if OK, -ENOMEM if out of memory, -EINVAL if there is a format
+ * error, -ENOENT if there is a references to a non-existent string
+ */
+static int scene_build(struct build_info *info, ofnode scn_node,
+		       struct expo *exp)
+{
+	const char *name;
+	struct scene *scn;
+	uint id, title_id;
+	ofnode node;
+	int ret;
+
+	name = ofnode_get_name(scn_node);
+	log_debug("Building scene %s\n", name);
+	ret = ofnode_read_u32(scn_node, "id", &id);
+	if (ret)
+		return log_msg_ret("id", -EINVAL);
+
+	ret = scene_new(exp, name, id, &scn);
+	if (ret < 0)
+		return log_msg_ret("scn", ret);
+
+	ret = add_txt_str(info, scn_node, scn, "title", 0);
+	if (ret < 0)
+		return log_msg_ret("tit", ret);
+	title_id = ret;
+	scene_title_set(scn, title_id);
+
+	ret = add_txt_str(info, scn_node, scn, "prompt", 0);
+	if (ret < 0)
+		return log_msg_ret("pr", ret);
+
+	ofnode_for_each_subnode(node, scn_node) {
+		ret = obj_build(info, node, scn);
+		if (ret < 0)
+			return log_msg_ret("mit", ret);
+	}
+
+	return 0;
+}
+
+int expo_build(ofnode root, struct expo **expp)
+{
+	struct build_info info;
+	ofnode scenes, node;
+	struct expo *exp;
+	u32 dyn_start;
+	int ret;
+
+	memset(&info, '\0', sizeof(info));
+	ret = read_strings(&info, root);
+	if (ret)
+		return log_msg_ret("str", ret);
+	list_strings(&info);
+
+	ret = expo_new("name", NULL, &exp);
+	if (ret)
+		return log_msg_ret("exp", ret);
+
+	if (!ofnode_read_u32(root, "dynamic-start", &dyn_start))
+		expo_set_dynamic_start(exp, dyn_start);
+
+	scenes = ofnode_find_subnode(root, "scenes");
+	if (!ofnode_valid(scenes))
+		return log_msg_ret("sno", -EINVAL);
+
+	ofnode_for_each_subnode(node, scenes) {
+		ret = scene_build(&info, node, exp);
+		if (ret < 0)
+			return log_msg_ret("scn", ret);
+	}
+	*expp = exp;
+
+	return 0;
+}
diff --git a/doc/develop/expo.rst b/doc/develop/expo.rst
index bd593dc2b3f5..f5caadbfd98c 100644
--- a/doc/develop/expo.rst
+++ b/doc/develop/expo.rst
@@ -100,10 +100,13 @@ objects first, then create the menu item, passing in the relevant IDs.
 Creating an expo
 ----------------
 
-To create an expo, use `expo_new()` followed by `scene_new()` to create a scene.
-Then add objects to the scene, using functions like `scene_txt_str()` and
-`scene_menu()`. For every menu item, add text and image objects, then create
-the menu item with `scene_menuitem()`, referring to those objects.
+To create an expo programmatically, use `expo_new()` followed by `scene_new()`
+to create a scene. Then add objects to the scene, using functions like
+`scene_txt_str()` and `scene_menu()`. For every menu item, add text and image
+objects, then create the menu item with `scene_menuitem()`, referring to those
+objects.
+
+To create an expo using a description file, see :ref:`expo_format` below.
 
 Layout
 ------
@@ -168,6 +171,273 @@ menu-inset
 menuitem-gap-y
     Number of pixels between menu items
 
+.. _expo_format:
+
+Pop-up mode
+-----------
+
+Expos support two modes. The simple mode is used for selecting from a single
+menu, e.g. when choosing with OS to boot. In this mode the menu items are shown
+in a list (label, > pointer, key and description) and can be chosen using arrow
+keys and enter::
+
+   U-Boot Boot Menu
+
+   UP and DOWN to choose, ENTER to select
+
+   mmc1           > 0  Fedora-Workstation-armhfp-31-1.9
+   mmc3             1  Armbian
+
+The popup mode allows multiple menus to be present in a scene. Each is shown
+just as its title and label, as with the `CPU Speed` and `AC Power` menus here::
+
+              Test Configuration
+
+
+   CPU Speed        <2 GHz>  (highlighted)
+
+   AC Power         Always Off
+
+
+     UP and DOWN to choose, ENTER to select
+
+
+Expo Format
+-----------
+
+It can be tedious to create a complex expo using code. Expo supports a
+data-driven approach, where the expo description is in a devicetree file. This
+makes it easier and faster to create and edit the description. An expo builder
+is provided to convert this format into an expo structure.
+
+Layout of the expo scenes is handled automatically, based on a set of simple
+rules.
+
+Top-level node
+~~~~~~~~~~~~~~
+
+The top-level node has the following properties:
+
+dynamic-start
+    type: u32, optional
+
+    Specifies the start of the dynamically allocated objects. This results in
+    a call to expo_set_dynamic_start().
+
+The top-level node has the following subnodes:
+
+scenes
+    Specifies the scenes in the expo, each one being a subnode
+
+strings
+    Specifies the strings in the expo, each one being a subnode
+
+`scenes` node
+~~~~~~~~~~~~~
+
+Contains a list of scene subnodes. The name of each subnode is passed as the
+name to `scene_new()`.
+
+`strings` node
+~~~~~~~~~~~~~~
+
+Contains a list of string subnodes. The name of each subnode is ignored.
+
+`strings` subnodes
+~~~~~~~~~~~~~~~~~~
+
+Each subnode defines a string which can be used by scenes and objects. Each
+string has an ID number which is used to refer to it.
+
+The `strings` subnodes have the following properties:
+
+id
+    type: u32, required
+
+    Specifies the ID number for the string.
+
+value:
+    type: string, required
+
+    Specifies the string text. For now only a single value is supported. Future
+    work may add support for multiple languages by using a value for each
+    language.
+
+Scene nodes (`scenes` subnodes)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Each subnode of the `scenes` node contains a scene description.
+
+Most properties can use either a string or a string ID. For example, a `title`
+property can be used to provide the title for a menu; alternatively a `title-id`
+property can provide the string ID of the title. If both are present, the
+ID takes preference, except that if a string with that ID does not exist, it
+falls back to using the string from the property (`title` in this example). The
+description below shows these are alternative properties with the same
+description.
+
+The scene nodes have the following properties:
+
+id
+    type: u32, required
+
+    Specifies the ID number for the string.
+
+title / title-id
+    type: string / u32, required
+
+    Specifies the title of the scene. This is shown at the top of the scene.
+
+prompt / prompt-id
+    type: string / u32, required
+
+    Specifies a prompt for the scene. This is shown at the bottom of the scene.
+
+The scene nodes have a subnode for each object in the scene.
+
+Object nodes
+~~~~~~~~~~~~
+
+The object-node name is used as the name of the object, e.g. when calling
+`scene_menu()` to create a menu.
+
+Object nodes have the following common properties:
+
+type
+    type: string, required
+
+    Specifies the type of the object. Valid types are:
+
+    "menu"
+        Menu containing items which can be selected by the user
+
+id
+    type: u32, required
+
+    Specifies the ID of the object. This is used when referring to the object.
+
+
+Menu nodes have the following additional properties:
+
+title / title-id
+    type: string / u32, required
+
+    Specifies the title of the menu. This is shown to the left of the area for
+    this menu.
+
+item-id
+    type: u32 list, required
+
+    Specifies the ID for each menu item. These are used for checking which item
+    has been selected.
+
+item-label / item-label-id
+    type: string list / u32 list, required
+
+    Specifies the label for each item in the menu. These are shown to the user.
+    In 'popup' mode these form the items in the menu.
+
+key-label / key-label-id
+    type: string list / u32 list, optional
+
+    Specifies the key for each item in the menu. These are currently only
+    intended for use in simple mode.
+
+desc-label / desc-label-id
+    type: string list / u32 list, optional
+
+    Specifies the description for each item in the menu. These are currently
+    only intended for use in simple mode.
+
+
+Expo layout
+~~~~~~~~~~~
+
+The `expo_arrange()` function can be called to arrange the expo objects in a
+suitable manner. For each scene it puts the title at the top, the prompt at the
+bottom and the objects in order from top to bottom.
+
+Expo format example
+~~~~~~~~~~~~~~~~~~~
+
+This example shows an expo with a single scene consisting of two menus. The
+scene title is specified using a string from the strings table, but all other
+strings are provided inline in the nodes where they are used.
+
+::
+
+    #define ID_PROMPT           1
+    #define ID_SCENE1           2
+    #define ID_SCENE1_TITLE     3
+
+    #define ID_CPU_SPEED        4
+    #define ID_CPU_SPEED_TITLE  5
+    #define ID_CPU_SPEED_1      6
+    #define ID_CPU_SPEED_2      7
+    #define ID_CPU_SPEED_3      8
+
+    #define ID_POWER_LOSS       9
+    #define ID_AC_OFF           10
+    #define ID_AC_ON            11
+    #define ID_AC_MEMORY        12
+
+    #define ID_DYNAMIC_START    13
+
+    &cedit {
+        dynamic-start = <ID_DYNAMIC_START>;
+
+        scenes {
+            main {
+                id = <ID_SCENE1>;
+
+                /* value refers to the matching id in /strings */
+                title-id = <ID_SCENE1_TITLE>;
+
+                /* simple string is used as it is */
+                prompt = "UP and DOWN to choose, ENTER to select";
+
+                /* defines a menu within the scene */
+                cpu-speed {
+                    type = "menu";
+                    id = <ID_CPU_SPEED>;
+
+                    /*
+                     * has both string and ID. The string is ignored
+                     * if the ID is present and points to a string
+                     */
+                    title = "CPU speed";
+                    title-id = <ID_CPU_SPEED_TITLE>;
+
+                    /* menu items as simple strings */
+                    item-label = "2 GHz", "2.5 GHz", "3 GHz";
+
+                    /* IDs for the menu items */
+                    item-id = <ID_CPU_SPEED_1 ID_CPU_SPEED_2
+                        ID_CPU_SPEED_3>;
+                };
+
+                power-loss {
+                    type = "menu";
+                    id = <ID_POWER_LOSS>;
+
+                    title = "AC Power";
+                    item-label = "Always Off", "Always On",
+                        "Memory";
+
+                    item-id = <ID_AC_OFF ID_AC_ON ID_AC_MEMORY>;
+                };
+            };
+        };
+
+        strings {
+            title {
+                id = <ID_SCENE1_TITLE>;
+                value = "Test Configuration";
+                value-es = "configuración de prueba";
+            };
+        };
+    };
+
 
 API documentation
 -----------------
@@ -180,11 +450,10 @@ Future ideas
 Some ideas for future work:
 
 - Default menu item and a timeout
-- Higher-level / automatic / more flexible layout of objects
 - Image formats other than BMP
 - Use of ANSI sequences to control a serial terminal
 - Colour selection
-- Better support for handling lots of settings, e.g. with radio/option widgets
+- Support for more widgets, e.g. text, numeric, radio/option
 - Mouse support
 - Integrate Nuklear, NxWidgets or some other library for a richer UI
 - Optimise rendering by only updating the display with changes since last render
@@ -194,6 +463,7 @@ Some ideas for future work:
 - Support both graphical and text menus at the same time on different devices
 - Support unicode
 - Support curses for proper serial-terminal menus
+- Add support for large menus which need to scroll
 
 .. Simon Glass <sjg at chromium.org>
 .. 7-Oct-22
diff --git a/include/expo.h b/include/expo.h
index f7febe1c9ae7..9fec4d0cd84e 100644
--- a/include/expo.h
+++ b/include/expo.h
@@ -653,4 +653,18 @@ int expo_action_get(struct expo *exp, struct expo_action *act);
  */
 int expo_apply_theme(struct expo *exp, ofnode node);
 
+/**
+ * expo_build() - Build an expo from an FDT description
+ *
+ * Build a complete expo from a description in the provided devicetree.
+ *
+ * See doc/developer/expo.rst for a description of the format
+ *
+ * @root: Root node for expo description
+ * @expp: Returns the new expo
+ * Returns: 0 if OK, -ENOMEM if out of memory, -EINVAL if there is a format
+ * error, -ENOENT if there is a references to a non-existent string
+ */
+int expo_build(ofnode root, struct expo **expp);
+
 #endif /*__SCENE_H */
diff --git a/include/test/cedit-test.h b/include/test/cedit-test.h
new file mode 100644
index 000000000000..349df75b16d4
--- /dev/null
+++ b/include/test/cedit-test.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Binding shared between cedit.dtsi and test/boot/expo.c
+ *
+ * Copyright 2023 Google LLC
+ * Written by Simon Glass <sjg at chromium.org>
+ */
+
+#ifndef __cedit_test_h
+#define __cedit_test_h
+
+#define ID_PROMPT		1
+#define ID_SCENE1		2
+#define ID_SCENE1_TITLE		3
+
+#define ID_CPU_SPEED		4
+#define ID_CPU_SPEED_TITLE	5
+#define ID_CPU_SPEED_1		6
+#define ID_CPU_SPEED_2		7
+#define ID_CPU_SPEED_3		8
+
+#define ID_POWER_LOSS		9
+#define ID_AC_OFF		10
+#define ID_AC_ON		11
+#define ID_AC_MEMORY		12
+
+#define ID_DYNAMIC_START	13
+
+#endif
diff --git a/test/boot/expo.c b/test/boot/expo.c
index c34eaeedd66f..e7148024fe30 100644
--- a/test/boot/expo.c
+++ b/test/boot/expo.c
@@ -13,6 +13,7 @@
 #include <test/suites.h>
 #include <test/ut.h>
 #include "bootstd_common.h"
+#include <test/cedit-test.h>
 #include "../../boot/scene_internal.h"
 
 enum {
@@ -588,3 +589,82 @@ static int expo_render_image(struct unit_test_state *uts)
 	return 0;
 }
 BOOTSTD_TEST(expo_render_image, UT_TESTF_DM | UT_TESTF_SCAN_FDT);
+
+/* Check building an expo from a devicetree description */
+static int expo_test_build(struct unit_test_state *uts)
+{
+	struct scene_obj_menu *menu;
+	struct scene_menitem *item;
+	struct scene_obj_txt *txt;
+	struct scene_obj *obj;
+	struct scene *scn;
+	struct expo *exp;
+	int count;
+	ofnode node;
+
+	node = ofnode_path("/cedit");
+	ut_assert(ofnode_valid(node));
+	ut_assertok(expo_build(node, &exp));
+
+	ut_asserteq_str("name", exp->name);
+	ut_asserteq(0, exp->scene_id);
+	ut_asserteq(ID_DYNAMIC_START + 20, exp->next_id);
+	ut_asserteq(false, exp->popup);
+
+	/* check the scene */
+	scn = expo_lookup_scene_id(exp, ID_SCENE1);
+	ut_assertnonnull(scn);
+	ut_asserteq_str("main", scn->name);
+	ut_asserteq(ID_SCENE1, scn->id);
+	ut_asserteq(ID_DYNAMIC_START + 1, scn->title_id);
+	ut_asserteq(0, scn->highlight_id);
+
+	/* check the title */
+	txt = scene_obj_find(scn, scn->title_id, SCENEOBJT_NONE);
+	ut_assertnonnull(txt);
+	obj = &txt->obj;
+	ut_asserteq_ptr(scn, obj->scene);
+	ut_asserteq_str("title", obj->name);
+	ut_asserteq(scn->title_id, obj->id);
+	ut_asserteq(SCENEOBJT_TEXT, obj->type);
+	ut_asserteq(0, obj->flags);
+	ut_asserteq_str("Test Configuration", expo_get_str(exp, txt->str_id));
+
+	/* check the menu */
+	menu = scene_obj_find(scn, ID_CPU_SPEED, SCENEOBJT_NONE);
+	obj = &menu->obj;
+	ut_asserteq_ptr(scn, obj->scene);
+	ut_asserteq_str("cpu-speed", obj->name);
+	ut_asserteq(ID_CPU_SPEED, obj->id);
+	ut_asserteq(SCENEOBJT_MENU, obj->type);
+	ut_asserteq(0, obj->flags);
+
+	txt = scene_obj_find(scn, menu->title_id, SCENEOBJT_NONE);
+	ut_asserteq_str("CPU speed", expo_get_str(exp, txt->str_id));
+
+	ut_asserteq(0, menu->cur_item_id);
+	ut_asserteq(0, menu->pointer_id);
+
+	/* check the items */
+	item = list_first_entry(&menu->item_head, struct scene_menitem,
+				sibling);
+	ut_asserteq_str("00", item->name);
+	ut_asserteq(ID_CPU_SPEED_1, item->id);
+	ut_asserteq(0, item->key_id);
+	ut_asserteq(0, item->desc_id);
+	ut_asserteq(0, item->preview_id);
+	ut_asserteq(0, item->flags);
+
+	txt = scene_obj_find(scn, item->label_id, SCENEOBJT_NONE);
+	ut_asserteq_str("2 GHz", expo_get_str(exp, txt->str_id));
+
+	count = 0;
+	list_for_each_entry(item, &menu->item_head, sibling)
+		count++;
+	ut_asserteq(3, count);
+
+	expo_destroy(exp);
+
+	return 0;
+}
+BOOTSTD_TEST(expo_test_build, UT_TESTF_DM);
-- 
2.41.0.rc0.172.g3f132b7071-goog



More information about the U-Boot mailing list