Skip to content

Commit

Permalink
samples: SMF: LVGL: SMF-based Calculator
Browse files Browse the repository at this point in the history
This sample crates a touchscreen desk calculator based
on the sample state machine in 'Practical UML Statecharts in
C/C++' by Miro Samek.

Sample should build and run on any touchscreen-enabled board
with sufficient resources. Tested on disco_l475_iot1 board
with adafruit_2_8_tft_touch_v2 touchscreen.

Signed-off-by: Glenn Andrews <[email protected]>
  • Loading branch information
glenn-andrews committed Jul 7, 2024
1 parent a912bb6 commit b8f5869
Show file tree
Hide file tree
Showing 10 changed files with 1,118 additions and 0 deletions.
11 changes: 11 additions & 0 deletions samples/subsys/smf/smf_calculator/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-License-Identifier: Apache-2.0

set(CMAKE_CXX_FLAGS "-fstack-usage")
cmake_minimum_required(VERSION 3.20.0)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(smf_psicc2_demo)

target_sources(app PRIVATE src/main.c)
target_sources(app PRIVATE src/smf_console_cmds.c)
target_sources(app PRIVATE src/smf_calculator_thread.c)
96 changes: 96 additions & 0 deletions samples/subsys/smf/smf_calculator/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
.. zephyr:code-sample:: smf_calculator
:name: SMF Calculator
:relevant-api: smf

Create a simple desk calculator using the State Machine Framework.

Overview
********

This sample creates a basic desk calculator driven by a state machine written
with the :ref:`State Machine Framework <smf>`.

The 'business logic' of the calculator is based on the statechart given in
Fig 2.18 of *Practical UML Statecharts in C/C++* 2nd Edition by Miro Samek.
This uses a three-layer hierarchical statechart to handle situations such as
ignoring leading zeroes, and disallowing multiple decimal points.

The statechart has been slightly modified to display different output on the
screen in the ``op_entered`` state depending on if a previous result is
available or not.

.. figure:: img/smf_calculator.png
:align: center
:alt: SMF Calculator Statechart
:figclass: align-center

Statechart for the SMF Calculator business logic.

The graphical interface uses an LVGL buton matrix for input and text label for
output, based on the sample in samples/drivers/display. The state machine updates
the output text label after every call to :c:func:`smf_run_state`.

:kconfig:option:`CONFIG_LV_Z_VDB_SIZE` has been reduced to 14% to allow it to run
on RAM-constrained boards like the :ref:`disco_l475_iot1_board`.

Requirements
************

The GUI should work with any touchscreen display supported by Zephyr. The shield
must be passed to ``west build`` using the ``--shield`` option, e.g.
``--shield adafruit_2_8_tft_touch_v2``

List of Arduino-based touchscreen shields:

- :ref:`adafruit_2_8_tft_touch_v2`
- :ref:`buydisplay_2_8_tft_touch_arduino`
- :ref:`buydisplay_3_5_tft_touch_arduino`

The demo should also work on STM32 Discovery Kits with built-in touchscreens e.g.

- :ref:`stm32f412g_disco_board`
- :ref:`st25dv_mb1283_disco_board`
- :ref:`stm32f7508_dk_board`
- :ref:`stm32f769i_disco_board`

etc. These will not need a shield defined as the touchscreen is built-in.


Building and Running
********************

Below is an example on how to build for a :ref:`disco_l475_iot1_board` board with
a :ref:`adafruit_2_8_tft_touch_v2`.

.. zephyr-app-commands::
:zephyr-app: samples/subsys/smf/smf_calculator
:board: disco_l475_iot1
:goals: build
:shield: adafruit_2_8_tft_touch_v2
:compact:

For testing purpose without the need of any hardware, the :ref:`native_sim <native_sim>`
board is also supported and can be built as follows;

.. zephyr-app-commands::
:zephyr-app: samples/subsys/smf/smf_calculator
:board: native_sim
:goals: build
:compact:

CLI control
===========

As well as control through the GUI, the calculator can be controlled through the CLI.
The ``key <key>`` command sends a keypress to the state machine. Valid keys are
``0`` through ``9`` for numbers, ``.``, ``+``, ``-``, ``*``, ``/`` and ``=`` to
perform the expected function, ``C`` for Cancel, and ``E`` for Cancel Entry.

GUI update speed on the :ref:`disco_l475_iot1_board` is of the order of 0.8s due to the
small video buffer.

References
**********

*Practical UML Statecharts in C/C++* 2nd Edition by Miro Samek
https://www.state-machine.com/psicc2
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions samples/subsys/smf/smf_calculator/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
CONFIG_DEBUG=y
CONFIG_LOG=y
CONFIG_SHELL=y

# Enable the state machine framework
CONFIG_SMF=y
CONFIG_SMF_ANCESTOR_SUPPORT=y
CONFIG_SMF_INITIAL_TRANSITION=y

# Enable floating point support
CONFIG_PICOLIBC_IO_FLOAT=y
CONFIG_CBPRINTF_FP_SUPPORT=y

# Enable thread awareness for debugging tools supporting it
CONFIG_DEBUG_THREAD_INFO=y

# enable to use thread names
CONFIG_THREAD_NAME=y

# Display Options
CONFIG_DISPLAY=y
CONFIG_DISPLAY_LOG_LEVEL_ERR=y

# LVGL Options
CONFIG_LV_Z_MEM_POOL_SIZE=8192
CONFIG_LV_Z_SHELL=y
CONFIG_MAIN_STACK_SIZE=2048
# Percentage of screen size for a video buffer
# 14% ~= 32k on adafruit_2_8_tft_touch_v2
CONFIG_LV_Z_VDB_SIZE=14

CONFIG_LVGL=y
CONFIG_LV_MEM_CUSTOM=y
CONFIG_LV_USE_LOG=y
CONFIG_LV_USE_LABEL=y
CONFIG_LV_USE_BTN=y
CONFIG_LV_USE_ARC=y
CONFIG_LV_USE_IMG=y
CONFIG_LV_USE_MONKEY=y
CONFIG_LV_FONT_MONTSERRAT_14=y
22 changes: 22 additions & 0 deletions samples/subsys/smf/smf_calculator/sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
sample:
description: SMF Calculator GUI Application
name: lvgl
tests:
sample.display.smf.calculator:
filter: dt_chosen_enabled("zephyr,display")
# Sample takes ~300k on disco_l475_iot1 board, add 50k just in case.
# Ram usage is 60k on disco_l475_iot1 board with adafruit_2_8_tft_touch_v2.
# Can be reduced by changing CONFIG_LV_Z_VDB_SIZE.
min_flash: 350
min_ram: 64
harness: none
tags:
- samples
- display
- gui
- lvgl
- smf
modules:
- lvgl
integration_platforms:
- native_sim/native/64
149 changes: 149 additions & 0 deletions samples/subsys/smf/smf_calculator/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright (c) 2024 Glenn Andrews
*
* SPDX-License-Identifier: Apache-2.0
*/

#include "main.h"
#include <zephyr/kernel.h>
#include "smf_calculator_thread.h"
#include <zephyr/drivers/display.h>
#include <lvgl.h>
#include <stdio.h>
#include <string.h>
#include <lvgl_input_device.h>
#include <zephyr/logging/log.h>

#define LOG_LEVEL CONFIG_LOG_DEFAULT_LEVEL
LOG_MODULE_REGISTER(main_app);

K_MSGQ_DEFINE(output_msgq, CALCULATOR_STRING_LENGTH, 2, 1);

#define CALCULATOR_BUTTON_LABEL_LENGTH 4
struct calculator_button {
char label[CALCULATOR_BUTTON_LABEL_LENGTH];
struct calculator_event event;
};

/* clang-format off */
struct calculator_button buttons[] = {
{.label = "CE", .event = {.event_id = CANCEL_ENTRY, .operand = 'E'}},
{.label = "C", .event = {.event_id = CANCEL_BUTTON, .operand = 'C'}},
{.label = "7", .event = {.event_id = DIGIT_1_9, .operand = '7'}},
{.label = "8", .event = {.event_id = DIGIT_1_9, .operand = '8'}},
{.label = "9", .event = {.event_id = DIGIT_1_9, .operand = '9'}},
{.label = "/", .event = {.event_id = OPERATOR, .operand = '/'}},
{.label = "4", .event = {.event_id = DIGIT_1_9, .operand = '4'}},
{.label = "5", .event = {.event_id = DIGIT_1_9, .operand = '5'}},
{.label = "6", .event = {.event_id = DIGIT_1_9, .operand = '6'}},
{.label = "*", .event = {.event_id = OPERATOR, .operand = '*'}},
{.label = "1", .event = {.event_id = DIGIT_1_9, .operand = '1'}},
{.label = "2", .event = {.event_id = DIGIT_1_9, .operand = '2'}},
{.label = "3", .event = {.event_id = DIGIT_1_9, .operand = '3'}},
{.label = "-", .event = {.event_id = OPERATOR, .operand = '-'}},
{.label = "0", .event = {.event_id = DIGIT_0, .operand = '0'}},
{.label = ".", .event = {.event_id = DECIMAL_POINT, .operand = '.'}},
{.label = "=", .event = {.event_id = EQUALS, .operand = '='}},
{.label = "+", .event = {.event_id = OPERATOR, .operand = '+'}},
};
/* clang-format on */

/* Where the result is printed */
static lv_obj_t *result_label;

void update_display(const char *output)
{
while (k_msgq_put(&output_msgq, output, K_NO_WAIT) != 0) {
k_msgq_purge(&output_msgq);
}
}

static void lv_btn_matrix_click_callback(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t *obj = lv_event_get_target(e);

if (code == LV_EVENT_PRESSED) {
uint32_t id = lv_btnmatrix_get_selected_btn(obj);

if (id > ARRAY_SIZE(buttons)) {
LOG_ERR("Invalid button: %d", id);
return;
}

int rc = post_calculator_event(&buttons[id].event, K_FOREVER);

if (rc != 0) {
LOG_ERR("could not post to msgq: %d", rc);
}
}
}

int setup_display(void)
{
const struct device *display_dev;

display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
if (!device_is_ready(display_dev)) {
LOG_ERR("Device not ready, aborting setup");
return -ENODEV;
}

/* clang-format off */
static const char *const btnm_map[] = {
buttons[0].label, buttons[1].label, "\n",
buttons[2].label, buttons[3].label, buttons[4].label, buttons[5].label, "\n",
buttons[6].label, buttons[7].label, buttons[8].label, buttons[9].label, "\n",
buttons[10].label, buttons[11].label, buttons[12].label, buttons[13].label, "\n",
buttons[14].label, buttons[15].label, buttons[16].label, buttons[17].label, "\n"
};
/* clang-format on */

lv_obj_t *btn_matrix = lv_btnmatrix_create(lv_scr_act());

lv_obj_align(btn_matrix, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_btnmatrix_set_map(btn_matrix, (const char **)btnm_map);
lv_obj_set_size(btn_matrix, lv_pct(CALC_BTN_WIDTH_PCT), lv_pct(CALC_BTN_HEIGHT_PCT));
lv_obj_add_event_cb(btn_matrix, lv_btn_matrix_click_callback, LV_EVENT_ALL, NULL);

result_label = lv_label_create(lv_scr_act());
lv_obj_set_width(result_label, lv_pct(CALC_RESULT_WIDTH_PCT));
lv_obj_set_style_text_align(result_label, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_align(result_label, LV_ALIGN_TOP_MID, 0, lv_pct(CALC_RESULT_OFFSET_PCT));

static lv_style_t style_shadow;

lv_style_init(&style_shadow);
lv_style_set_shadow_width(&style_shadow, 5);
lv_style_set_shadow_spread(&style_shadow, 2);
lv_style_set_shadow_color(&style_shadow, lv_palette_main(LV_PALETTE_GREY));
lv_obj_add_style(result_label, &style_shadow, 0);
update_display("0");

lv_task_handler();
display_blanking_off(display_dev);

return 0;
}

int main(void)
{
printk("SMF Desk Caclualtor Demo\n");

int rc = setup_display();

if (rc != 0) {
return rc;
}

while (1) {
char output[CALCULATOR_STRING_LENGTH];

if (k_msgq_get(&output_msgq, output, K_MSEC(50)) == 0) {
lv_label_set_text(result_label, output);
}

lv_task_handler();
}
return 0;
}
13 changes: 13 additions & 0 deletions samples/subsys/smf/smf_calculator/src/main.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2024 Glenn Andrews
*
* SPDX-License-Identifier: Apache-2.0
*/

/* Constants for control sizes as a percentage of screen size. */
#define CALC_BTN_WIDTH_PCT 90
#define CALC_BTN_HEIGHT_PCT 80
#define CALC_RESULT_WIDTH_PCT 70
#define CALC_RESULT_OFFSET_PCT 10

void update_display(const char *output);
Loading

0 comments on commit b8f5869

Please sign in to comment.