From 5114ddd6c02d38d8bffa940edc2c2c7afb09b10c Mon Sep 17 00:00:00 2001 From: Matteo Golin Date: Mon, 22 Jun 2026 20:49:27 -0400 Subject: [PATCH] examples/baromonitor: New LVGL example which integrates with barometer This example is an LVGL application which reads pressure and temperature data from a barometer and displays it on gauges. It is intended to provide an idea of what LVGL on NuttX can practically be used for in an application. Signed-off-by: Matteo Golin --- examples/baromonitor/CMakeLists.txt | 35 ++ examples/baromonitor/Kconfig | 38 +++ examples/baromonitor/Make.defs | 25 ++ examples/baromonitor/Makefile | 36 ++ examples/baromonitor/baromonitor_main.c | 419 ++++++++++++++++++++++++ 5 files changed, 553 insertions(+) create mode 100644 examples/baromonitor/CMakeLists.txt create mode 100644 examples/baromonitor/Kconfig create mode 100644 examples/baromonitor/Make.defs create mode 100644 examples/baromonitor/Makefile create mode 100644 examples/baromonitor/baromonitor_main.c diff --git a/examples/baromonitor/CMakeLists.txt b/examples/baromonitor/CMakeLists.txt new file mode 100644 index 000000000..e047922f8 --- /dev/null +++ b/examples/baromonitor/CMakeLists.txt @@ -0,0 +1,35 @@ +# ############################################################################## +# apps/examples/baromonitor/CMakeLists.txt +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. The ASF licenses this +# file to you under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ############################################################################## + +if(CONFIG_EXAMPLES_BAROMONITOR) + nuttx_add_application( + NAME + ${CONFIG_EXAMPLES_BAROMONITOR_PROGNAME} + PRIORITY + ${CONFIG_EXAMPLES_BAROMONITOR_PRIORITY} + STACKSIZE + ${CONFIG_EXAMPLES_BAROMONITOR_STACKSIZE} + MODULE + ${CONFIG_EXAMPLES_BAROMONITOR} + SRCS + baromonitor_main.c) +endif() diff --git a/examples/baromonitor/Kconfig b/examples/baromonitor/Kconfig new file mode 100644 index 000000000..7ef4f17bc --- /dev/null +++ b/examples/baromonitor/Kconfig @@ -0,0 +1,38 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config EXAMPLES_BAROMONITOR + tristate "Barometer monitoring dashboard example" + default n + depends on UORB + depends on LIBC_FLOATINGPOINT + depends on GRAPHICS_LVGL + depends on LV_USE_NUTTX + depends on LV_FONT_MONTSERRAT_24 + depends on LV_FONT_MONTSERRAT_32 + ---help--- + Enable the barometer monitoring dashboard. + +if EXAMPLES_BAROMONITOR + +config EXAMPLES_BAROMONITOR_PROGNAME + string "Program name" + default "baromonitor" + +config EXAMPLES_BAROMONITOR_PRIORITY + int "Barometer monitor task priority" + default 100 + +config EXAMPLES_BAROMONITOR_STACKSIZE + int "Baromonitor stack size" + default DEFAULT_TASK_STACKSIZE + +config EXAMPLES_BAROMONITOR_SAMPLERATE + int "Sample rate (ms)" + default 50 + ---help--- + Sample rate in milliseconds of barometer data. + +endif # EXAMPLES_BARO_DASHBOARD diff --git a/examples/baromonitor/Make.defs b/examples/baromonitor/Make.defs new file mode 100644 index 000000000..206ea679f --- /dev/null +++ b/examples/baromonitor/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/examples/baromonitor/Make.defs +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +ifneq ($(CONFIG_EXAMPLES_BAROMONITOR),) +CONFIGURED_APPS += $(APPDIR)/examples/baromonitor +endif diff --git a/examples/baromonitor/Makefile b/examples/baromonitor/Makefile new file mode 100644 index 000000000..8fe37e36f --- /dev/null +++ b/examples/baromonitor/Makefile @@ -0,0 +1,36 @@ +############################################################################ +# apps/examples/baromonitor/Makefile +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +include $(APPDIR)/Make.defs + +# BAROMONITOR Barometer sensor example built-in application info + +PROGNAME = $(CONFIG_EXAMPLES_BAROMONITOR_PROGNAME) +PRIORITY = $(CONFIG_EXAMPLES_BAROMONITOR_PRIORITY) +STACKSIZE = $(CONFIG_EXAMPLES_BAROMONITOR_STACKSIZE) +MODULE = $(CONFIG_EXAMPLES_BAROMONITOR) + +# BAROMONITOR Barometer sensor example + +MAINSRC = baromonitor_main.c + +include $(APPDIR)/Application.mk diff --git a/examples/baromonitor/baromonitor_main.c b/examples/baromonitor/baromonitor_main.c new file mode 100644 index 000000000..c3a93d6b9 --- /dev/null +++ b/examples/baromonitor/baromonitor_main.c @@ -0,0 +1,419 @@ +/**************************************************************************** + * apps/examples/baromonitor/baromonitor_main.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include + +#include + +#include + +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define PRESSURE_COLOUR 0xef4444 +#define TEMPERATURE_COLOUR 0x6366f1 + +#define LINE_WIDTH (8) +#define GAUGE_SIZE (600) + +/* Axis limits */ + +#define PRESS_MIN (960) +#define PRESS_MAX (1010) + +#define TEMP_MIN (-30) +#define TEMP_MAX (60) +#define TEMP_THRESH (TEMP_MAX - 10) + +#define PRESS_MAJOR_TICKS_EVERY (5) +#define TEMP_MAJOR_TICKS_EVERY (5) + +#define NUM_TEMP_LABELS ((TEMP_MAX - TEMP_MIN) / TEMP_MAJOR_TICKS_EVERY) + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static const char *g_temp_labels[NUM_TEMP_LABELS + 1]; +static char g_label_buf[NUM_TEMP_LABELS][8]; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +#ifdef CONFIG_LV_USE_NUTTX_LIBUV +static void lv_nuttx_uv_loop(uv_loop_t *loop, lv_nuttx_result_t *result) +{ + lv_nuttx_uv_t uv_info; + void *data; + + uv_loop_init(loop); + + lv_memset(&uv_info, 0, sizeof(uv_info)); + uv_info.loop = loop; + uv_info.disp = result->disp; + uv_info.indev = result->indev; +#ifdef CONFIG_UINPUT_TOUCH + uv_info.uindev = result->utouch_indev; +#endif + + data = lv_nuttx_uv_init(&uv_info); + uv_run(loop, UV_RUN_DEFAULT); + lv_nuttx_uv_deinit(&data); +} +#endif + +static void set_column_flex(lv_obj_t *obj) +{ + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_flex_main_place(obj, LV_FLEX_ALIGN_CENTER, 0); + lv_obj_set_style_flex_cross_place(obj, LV_FLEX_ALIGN_CENTER, 0); + lv_obj_set_style_flex_track_place(obj, LV_FLEX_ALIGN_CENTER, 0); +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * baromonitor_main + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + lv_nuttx_dsc_t info; + lv_nuttx_result_t result; + lv_obj_t *screen; + lv_obj_t *gauge_cont; + lv_obj_t *press_gauge; + lv_obj_t *press_line; + lv_obj_t *press_cont; + lv_obj_t *press_label; + lv_obj_t *temp_gauge; + lv_obj_t *temp_line; + lv_obj_t *temp_cont; + lv_obj_t *temp_label; + lv_obj_t *title_cont; + lv_obj_t *title; + lv_scale_section_t *section; + lv_style_t indicator_style; + lv_style_t minor_ticks_style; + lv_style_t main_line_style; + lv_style_t section_label_style; + lv_style_t section_minor_ticks_style; + lv_style_t section_main_line_style; + +#ifdef CONFIG_LV_USE_NUTTX_LIBUV + uv_loop_t ui_loop; +#endif + + int err; + int fd; + bool got_data; + struct sensor_baro sensor_data; + unsigned instance = 0; + char topic[32]; + + if (argc == 2) + { + instance = atoi(argv[1]); + } + + snprintf(topic, sizeof(topic), "sensor_baro%u", instance); + + fd = orb_open("sensor_baro", instance, O_RDONLY | O_NONBLOCK); + if (fd < 0) + { + fprintf(stderr, "Could not open %s: %d\n", topic, errno); + return EXIT_FAILURE; + } + + printf("Subscribed to %s\n", topic); + + /* LVGL setup */ + +#ifdef CONFIG_LV_USE_NUTTX_LIBUV + lv_memzero(&ui_loop, sizeof(ui_loop)); +#endif + + if (!lv_is_initialized()) + { + printf("Initializing LVGL...\n"); + lv_init(); + printf("LVGL initialized...\n"); + } + + lv_nuttx_dsc_init(&info); + lv_nuttx_init(&info, &result); + + if (result.disp == NULL) + { + fprintf(stderr, "Failed to initialize LVGL.\n"); + close(fd); + return EXIT_FAILURE; + } + + /* Set up stuff to render */ + + screen = lv_screen_active(); + set_column_flex(screen); + + /* Create containers for layout. We want a spanning title, followed by two + * side-by-side containers for the gauges. + * + * Gauge container is therefore a ROW flex, while its inner containers are + * column flex (scale followed by a label below) + */ + + title_cont = lv_obj_create(screen); + lv_obj_set_size(title_cont, LV_PCT(100), LV_PCT(10)); + lv_obj_align(title_cont, LV_ALIGN_CENTER, 0, 0); + title = lv_label_create(title_cont); + lv_label_set_text(title, "Barometer monitor"); + lv_obj_set_style_text_font(title, &lv_font_montserrat_32, 0); + lv_obj_align(title, LV_ALIGN_CENTER, 0, 0); + + gauge_cont = lv_obj_create(screen); + lv_obj_set_size(gauge_cont, LV_PCT(100), LV_PCT(90)); + lv_obj_set_flex_flow(gauge_cont, LV_FLEX_FLOW_ROW); + lv_obj_set_style_flex_main_place(gauge_cont, LV_FLEX_ALIGN_CENTER, 0); + lv_obj_set_style_flex_cross_place(gauge_cont, LV_FLEX_ALIGN_CENTER, 0); + lv_obj_set_style_flex_track_place(gauge_cont, LV_FLEX_ALIGN_CENTER, 0); + + press_cont = lv_obj_create(gauge_cont); + temp_cont = lv_obj_create(gauge_cont); + set_column_flex(press_cont); + set_column_flex(temp_cont); + lv_obj_set_size(press_cont, LV_PCT(50), LV_PCT(100)); + lv_obj_set_size(temp_cont, LV_PCT(50), LV_PCT(100)); + + /* TODO: Get the text labels on the gauges to be padded better */ + + /* Pressure gauge */ + + press_gauge = lv_scale_create(press_cont); + + lv_obj_set_size(press_gauge, GAUGE_SIZE, GAUGE_SIZE); + lv_scale_set_mode(press_gauge, LV_SCALE_MODE_ROUND_INNER); + lv_obj_set_style_bg_opa(press_gauge, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(press_gauge, + lv_palette_lighten(LV_PALETTE_RED, 4), 0); + lv_obj_set_style_radius(press_gauge, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_clip_corner(press_gauge, true, 0); + lv_obj_set_style_text_font(press_gauge, &lv_font_montserrat_24, 0); + lv_obj_align(press_gauge, LV_ALIGN_LEFT_MID, LV_PCT(5), 0); + + lv_scale_set_label_show(press_gauge, true); + + lv_scale_set_total_tick_count(press_gauge, (PRESS_MAX - PRESS_MIN)); + lv_scale_set_major_tick_every(press_gauge, PRESS_MAJOR_TICKS_EVERY); + + lv_obj_set_style_length(press_gauge, 5, LV_PART_ITEMS); + lv_obj_set_style_length(press_gauge, 10, LV_PART_INDICATOR); + lv_scale_set_range(press_gauge, PRESS_MIN, PRESS_MAX); + + lv_scale_set_angle_range(press_gauge, 270); + lv_scale_set_rotation(press_gauge, 135); + + press_line = lv_line_create(press_gauge); + + lv_obj_set_style_line_width(press_line, LINE_WIDTH, LV_PART_MAIN); + lv_obj_set_style_line_rounded(press_line, true, LV_PART_MAIN); + lv_obj_set_style_line_color(press_line, lv_color_black(), LV_PART_MAIN); + lv_obj_center(press_line); + + /* Temperature gauge */ + + temp_gauge = lv_scale_create(temp_cont); + lv_obj_set_size(temp_gauge, GAUGE_SIZE, GAUGE_SIZE); + lv_scale_set_label_show(temp_gauge, true); + lv_scale_set_mode(temp_gauge, LV_SCALE_MODE_ROUND_OUTER); + lv_obj_align(temp_gauge, LV_ALIGN_RIGHT_MID, LV_PCT(-5), 0); + + lv_scale_set_total_tick_count(temp_gauge, (TEMP_MAX - TEMP_MIN)); + lv_scale_set_major_tick_every(temp_gauge, TEMP_MAJOR_TICKS_EVERY); + lv_obj_set_style_length(temp_gauge, 5, LV_PART_ITEMS); + lv_obj_set_style_length(temp_gauge, 10, LV_PART_INDICATOR); + lv_scale_set_range(temp_gauge, TEMP_MIN, TEMP_MAX); + + lv_scale_set_angle_range(temp_gauge, 270); + lv_scale_set_rotation(temp_gauge, 135); + + temp_line = lv_line_create(temp_gauge); + + lv_obj_set_style_line_width(temp_line, LINE_WIDTH, LV_PART_MAIN); + lv_obj_set_style_line_rounded(temp_line, true, LV_PART_MAIN); + lv_obj_center(temp_line); + + /* Generate labels */ + + for (unsigned i = 0; i < NUM_TEMP_LABELS; i++) + { + snprintf(g_label_buf[i], sizeof(g_label_buf[i]), "%d °C", + TEMP_MIN + i * (TEMP_MAX - TEMP_MIN) / NUM_TEMP_LABELS); + g_temp_labels[i] = (const char *)g_label_buf[i]; + } + + g_temp_labels[NUM_TEMP_LABELS] = NULL; + + lv_scale_set_text_src(temp_gauge, g_temp_labels); + + /* Temp gauge styles */ + + lv_style_init(&indicator_style); + + lv_style_set_text_font(&indicator_style, &lv_font_montserrat_24); + lv_style_set_text_color(&indicator_style, + lv_palette_darken(LV_PALETTE_BLUE, 3)); + + lv_style_set_line_color(&indicator_style, + lv_palette_darken(LV_PALETTE_BLUE, 3)); + lv_style_set_width(&indicator_style, 10U); /* Tick length */ + lv_style_set_line_width(&indicator_style, 2U); /* Tick width */ + lv_obj_add_style(temp_gauge, &indicator_style, LV_PART_INDICATOR); + + lv_style_init(&minor_ticks_style); + lv_style_set_line_color(&minor_ticks_style, + lv_palette_lighten(LV_PALETTE_BLUE, 2)); + lv_style_set_width(&minor_ticks_style, 5U); /* Tick length */ + lv_style_set_line_width(&minor_ticks_style, 2U); /* Tick width */ + lv_obj_add_style(temp_gauge, &minor_ticks_style, LV_PART_ITEMS); + + lv_style_init(&main_line_style); + lv_style_set_arc_color(&main_line_style, + lv_palette_darken(LV_PALETTE_BLUE, 3)); + lv_style_set_arc_width(&main_line_style, 2U); /* Tick width */ + + /* Section on temperature gauge */ + + lv_style_init(§ion_label_style); + lv_style_init(§ion_minor_ticks_style); + lv_style_init(§ion_main_line_style); + + lv_style_set_text_font(§ion_label_style, &lv_font_montserrat_24); + lv_style_set_text_color(§ion_label_style, + lv_palette_darken(LV_PALETTE_RED, 3)); + + lv_style_set_line_color(§ion_label_style, + lv_palette_darken(LV_PALETTE_RED, 3)); + lv_style_set_line_width(§ion_label_style, 5U); /* Tick width */ + + lv_style_set_line_color(§ion_minor_ticks_style, + lv_palette_lighten(LV_PALETTE_RED, 2)); + lv_style_set_line_width(§ion_minor_ticks_style, 4U); /* Tick width */ + + lv_style_set_arc_color(§ion_main_line_style, + lv_palette_darken(LV_PALETTE_RED, 3)); + lv_style_set_arc_width(§ion_main_line_style, 4U); /* Tick width */ + + section = lv_scale_add_section(temp_gauge); + lv_scale_section_set_range(section, TEMP_THRESH, TEMP_MAX); + lv_scale_section_set_style(section, LV_PART_INDICATOR, + §ion_label_style); + lv_scale_section_set_style(section, LV_PART_ITEMS, + §ion_minor_ticks_style); + lv_scale_section_set_style(section, LV_PART_MAIN, + §ion_main_line_style); + + /* Data labels */ + + press_label = lv_label_create(press_cont); + lv_obj_align(press_label, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_text_font(press_label, &lv_font_montserrat_24, 0); + temp_label = lv_label_create(temp_cont); + lv_obj_align(temp_label, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_text_font(temp_label, &lv_font_montserrat_24, 0); + + /* Main loop */ + +#ifdef CONFIG_LV_USE_NUTTX_LIBUV + lv_nuttx_uv_loop(&ui_loop, &result); +#else + for (; ; ) + { + err = read(fd, &sensor_data, sizeof(sensor_data)); + if (err < 0) + { + fprintf(stderr, "Failed read data from %s: %d\n", topic, errno); + } + else if (err == 0) + { + fprintf(stderr, "No data from %s\n", topic); + } + else + { + got_data = true; + } + + if (got_data) + { + /* Update display if we obtained it successfully */ + + lv_label_set_text_fmt(temp_label, "Temperature: %2.f °C", + sensor_data.temperature); + lv_label_set_text_fmt(press_label, "Pressure: %2.f mbar", + sensor_data.pressure); + + lv_scale_set_line_needle_value(press_gauge, press_line, -10, + (int32_t)sensor_data.pressure); + lv_scale_set_line_needle_value(temp_gauge, temp_line, -10, + (int32_t)sensor_data.temperature); + + if (sensor_data.temperature < TEMP_THRESH) + { + lv_obj_set_style_line_color( + temp_line, lv_palette_darken(LV_PALETTE_BLUE, 3), + LV_PART_MAIN); + } + else + { + lv_obj_set_style_line_color( + temp_line, lv_palette_darken(LV_PALETTE_RED, 3), + LV_PART_MAIN); + } + + got_data = false; /* Reset for next loop */ + } + + lv_timer_handler(); + usleep(CONFIG_EXAMPLES_BAROMONITOR_SAMPLERATE * 1000); + + /* TODO: make this equivalent in LIBUV loop */ + } +#endif + + orb_close(fd); + lv_nuttx_deinit(&result); + lv_deinit(); + return EXIT_SUCCESS; +}