STM32 LVGL Arc(호) 그리기

RPM, 오일 그리고 냉각수 게이지를 디지털 판넬에 표시하기 위해 LVGL 라이브러리의 arc 관련 함수를 사용하면 쉽게 그릴 수 있는데, 기존 소스에서 게이지바와 눈금 바늘 사이에 약간에 오차가 있어 계산을 수정하면서 LVGL arc 함수를 정리하였다. 아래 이미지는 게이지바와 눈금 바늘을 표시하는 간단한 예제이다.

위 예제처럼 게이지바는 특정 범위에 들어오면 경고를 표시하기 위해 빨간 색으로 표시하는 구간이 있으며, 눈금 바늘은 게이지바의 끝에 위치해야 한다.

 

LVGL 라이브러리를 사용하여 호를 그리기 위해 lv_arc_create() 함수로 arc 인스턴스를 생성하도록 하고, 호의 위치와 크기를 지정하도록 한다.

    *arc = lv_arc_create(lv_scr_act());
    lv_obj_set_pos(*arc, x-radius, y-radius);
    lv_obj_set_size(*arc, radius*2, radius*2);

그리고 호는 전경 및 배경 색상을 선택할 수 있다. 전경 및 배경 색상을 이용하여 게이지의 경고 범위를 설정하려고 한다. LVGL 라이브러리의 원래 의도는 사용자 설정 상태를 표시하는 것인데, 이 의도로 사용하지 않고 설정된 값의 경고 유무를 표시하려고 한다.

    lv_obj_add_style(*arc, &arc_red_style, LV_PART_MAIN);
    lv_obj_add_style(*arc, &arc_blue_style, LV_PART_INDICATOR);

LV_PART_MAIN은 배경 색상이며 LV_PART_INDICATOR는 전경 색상을 설정하며, 이것은 lvgl style  인스턴스를 객체로 받아 설정한다. 정상 범위 값은 파란색 게이지바로 표시하며, 경고 범위는 빨간색 게이지바로 표시하도록 하였다.

 

LVGL 라이브러리의 호를 그릴 때 시작 위치는 원점의 오른쪽 반지름 위치에서 시작하며, 시작 위치를 이동하기 위해서는 lv_arc_set_rotation() 함수를 사용하여 설정하도록 한다.

    lv_arc_set_angles(*arc, 0, 0);
    lv_arc_set_bg_angles(*arc, 0, 0);
    lv_arc_set_rotation(*arc, arc_offset);

그리고 눈금 바늘은 약간의 꼼수로 표시하도록 한다. 전경 색상은 불투명도 0%(투명도 100%)로 설정하고 배경색상을 1도 각도만 출력하도록 하면 위 예제처럼 출력되는 것을 확인할 수 있다. 위 예제의 전체 코드는 아래와 같다.

#include "lvgl/lvgl.h"

#define RPM_VAL_RANGE        4000
#define RPM_VAL_WARNNING     3000
#define RPM_ARC_RANGE        240
#define RPM_ARC_OFFSET       150
#define RPM_ARC_WARNNING     180
#define RPM_ARC_RADIUS       212
#define RPM_MIDDLE_X         260
#define RPM_MIDDLE_Y         258

#define OIL_VAL_RANGE        100
#define OIL_VAL_WARNNING     10
#define OIL_ARC_RANGE        90
#define OIL_ARC_OFFSET       180
#define OIL_ARC_RADIUS       196
#define OIL_MIDDLE_X         716
#define OIL_MIDDLE_Y         240

#define COOLANT_VAL_RANGE    120
#define COOLANT_VAL_WARNNING 100
#define COOLANT_ARC_RANGE    90
#define COOLANT_ARC_OFFSET   180
#define COOLANT_ARC_WARNNING 75
#define COOLANT_ARC_RADIUS   196
#define COOLANT_MIDDLE_X     969
#define COOLANT_MIDDLE_Y     240


lv_style_t arc_red_style;
lv_style_t arc_blue_style;
lv_style_t arc_mark_style;
lv_style_t arc_opa0_style;

lv_obj_t *label_rpm;
lv_obj_t *arc_rpm;
lv_obj_t *mark_rpm;

lv_obj_t *label_oil;
lv_obj_t *arc_oil;
lv_obj_t *mark_oil;

lv_obj_t *label_coolant;
lv_obj_t *arc_coolant;
lv_obj_t *mark_coolant;

static void gauge_init_style(void)
{
    lv_style_init(&arc_mark_style);
    lv_style_set_arc_opa(&arc_mark_style, LV_OPA_100);
    lv_style_set_arc_rounded(&arc_mark_style, 0);
    lv_style_set_arc_color(&arc_mark_style, lv_color_make(250, 20, 20));
    lv_style_set_arc_width(&arc_mark_style, 55);

    lv_style_init(&arc_blue_style);
    lv_style_set_arc_opa(&arc_blue_style, LV_OPA_50);
    lv_style_set_arc_rounded(&arc_blue_style, 0);
    lv_style_set_arc_color(&arc_blue_style, lv_color_make(0, 190, 255));
    lv_style_set_arc_width(&arc_blue_style, 20);

    lv_style_init(&arc_red_style);
    lv_style_set_arc_opa(&arc_red_style, LV_OPA_50);
    lv_style_set_arc_rounded(&arc_red_style, 0);
    lv_style_set_arc_color(&arc_red_style, lv_color_make(250, 20, 20));
    lv_style_set_arc_width(&arc_red_style, 20);

    lv_style_init(&arc_opa0_style);
    lv_style_set_arc_opa(&arc_opa0_style, LV_OPA_0);
    lv_style_set_arc_rounded(&arc_opa0_style, 0);
}

static void gauge_draw(lv_obj_t **label, lv_obj_t **arc, lv_obj_t **mark, uint16_t x, uint16_t y, uint16_t radius, uint16_t arc_offset)
{
    /* Label */
    *label = lv_label_create(lv_scr_act());
    lv_obj_set_pos(*label, x, y);
    lv_label_set_text(*label, "0");

    /* Gauge bar */
    *arc = lv_arc_create(lv_scr_act());
    lv_arc_set_angles(*arc, 0, 0);
    lv_arc_set_bg_angles(*arc, 0, 0);
    lv_arc_set_rotation(*arc, arc_offset);

    lv_obj_set_pos(*arc, x-radius, y-radius);
    lv_obj_set_size(*arc, radius*2, radius*2);
    lv_obj_add_style(*arc, &arc_red_style, LV_PART_MAIN);
    lv_obj_add_style(*arc, &arc_blue_style, LV_PART_INDICATOR);

    lv_obj_remove_style(*arc, NULL, LV_PART_KNOB);
    lv_obj_clear_flag(*arc, LV_OBJ_FLAG_CLICKABLE);

    /* Gauge arrow */
    *mark = lv_arc_create(lv_scr_act());
    lv_arc_set_angles(*mark, 0, 0);
    lv_arc_set_bg_angles(*mark, 0, 0);
    lv_arc_set_rotation(*mark, arc_offset);

    lv_obj_set_pos(*mark, x-radius, y-radius);
    lv_obj_set_size(*mark, radius*2, radius*2);
    lv_obj_add_style(*mark, &arc_opa0_style, LV_PART_MAIN);
    lv_obj_add_style(*mark, &arc_mark_style, LV_PART_INDICATOR);

    lv_obj_remove_style(*mark, NULL, LV_PART_KNOB);
    lv_obj_clear_flag(*mark, LV_OBJ_FLAG_CLICKABLE);
}

static void gauge_update(uint8_t mode, uint16_t value)
{
    char val_string[8] = {0, };

    uint8_t  val_check = 0;
    uint16_t val_range, val_warn;
    uint16_t arc_range, arc_offset, arc_warn, arc_value;

    lv_obj_t *label, *arc, *mark;

    switch(mode)
    {
        case 0:
            val_range  = RPM_VAL_RANGE;
            val_warn   = RPM_VAL_WARNNING;
            arc_range  = RPM_ARC_RANGE;
            arc_offset = RPM_ARC_OFFSET;
            arc_warn   = RPM_ARC_WARNNING;

            label = label_rpm;
            arc   = arc_rpm;
            mark  = mark_rpm;
            break;

        case 1:
            val_range  = OIL_VAL_RANGE;
            val_warn   = OIL_VAL_WARNNING;
            arc_range  = OIL_ARC_RANGE;
            arc_offset = OIL_ARC_OFFSET;
            arc_warn   = 0;

            label = label_oil;
            arc   = arc_oil;
            mark  = mark_oil;
            break;

        case 2:
            val_range  = COOLANT_VAL_RANGE;
            val_warn   = COOLANT_VAL_WARNNING;
            arc_range  = COOLANT_ARC_RANGE;
            arc_offset = COOLANT_ARC_OFFSET;
            arc_warn   = COOLANT_ARC_WARNNING;

            label = label_coolant;
            arc   = arc_coolant;
            mark  = mark_coolant;
            break;

        default:
            printg("ERROR : invalid mode = %d\r\n", mode);
            return;
    }

    sprintf(val_string, "%d", value);
    lv_label_set_text(label, val_string);

    arc_value = (value * arc_range) / val_range;
    if(mode == 1) val_check = value <= val_warn;
    else          val_check = value > val_warn;

    if(val_check)
    {
        lv_arc_set_angles(arc, 0, arc_warn);
        lv_arc_set_bg_angles(arc, arc_warn, arc_value);
    }
    else
    {
        lv_arc_set_angles(arc, 0, arc_value);
        lv_arc_set_bg_angles(arc, 0, 0);
    }
    lv_arc_set_angles(mark, arc_value, arc_value+1);
}

void exam_gague_draw(void)
{
    gauge_init_style();
    gauge_draw(&label_rpm, &arc_rpm, &mark_rpm, RPM_MIDDLE_X, RPM_MIDDLE_Y, RPM_ARC_RADIUS, RPM_ARC_OFFSET);
    gauge_draw(&label_oil, &arc_oil, &mark_oil, OIL_MIDDLE_X, OIL_MIDDLE_Y, OIL_ARC_RADIUS, OIL_ARC_OFFSET);
    gauge_draw(&label_coolant, &arc_coolant, &mark_coolant, COOLANT_MIDDLE_X, COOLANT_MIDDLE_Y, COOLANT_ARC_RADIUS, COOLANT_ARC_OFFSET);
}

uint8_t val1 = 0;
uint8_t val2 = 0;
void exam_gauge_update(void)
{
    if(val1 <= 100)
    {
        gauge_update(0, val1*4000/100);
        gauge_update(1, val1);
        gauge_update(2, val1*120/100);
        usleep(50 * 1000);
        val1++;
        if(val1 == 100) val2 = 100;
    }
    else
    {
        if(val2 > 0)
        {
            val2--;
            if(val2 == 0) val1 = 0;
            gauge_update(0, val2*4000/100);
            gauge_update(1, val2);
            gauge_update(2, val2*120/100);
            usleep(50 * 1000);
        }
    }

}

기존 코드가 복잡하게 되어 있어, 사용방법이 까다로운 것 같았지만, 실제 코드를 정리해보니 기본 원리만 이해되면 간단히 사용할 수 있었다. 나중에 기억나지 않을 때 참고하면 될 듯 하다.