본문 바로가기
ESP32

ESP32-C6-LCD-1.47 예제를 KODEX200 시세 표시기로 바꾸기 - 하

by 소나무기운 2026. 3. 30.
반응형

[2026/03/30] First Start.

소나무 기운 ,  전자제품 개발/생산

ESP32-C6-LCD-1.47 예제를 KODEX200 시세 표시기로 바꾸기 - 하

이번 프로젝트는 ESP32-C6-LCD-1.47 보드의 기본 예제를 바탕으로, KODEX200 ETF 값을 가져와 LCD에 표시하는 형태로 바꿔본 작업이다.

이번은 하편으로 화면 표시 방법과 추가로 등락 상태에 따라 RGB LED를 제어하는 기능을 넣었다.

바이브 코딩이라는 것을 시도해 보았다. 내가 접근한 방법에 대해서 공유해 보도록 한다.

 

  

2026.03.29 - [ESP32] - ESP32-C6-LCD-1.47 예제를 KODEX200 시세 표시기로 바꾸기 - 상

📸 완성 화면



4. 받은 데이터를 LCD에 어떻게 보여주도록 바꿨나

줄화면 구성은 main/LVGL_UI/LVGL_Example.c 에서 담당한다.

기존 LVGL 예제 화면을 기초로 하여 변경하였다. KODEX200과 KODEX 원자력 SMR의 가격과 변화량을 표시하는 정보형 UI로 바꾼 것이 핵심이다.

작은 LCD에서는 정보량보다 가독성이 더 중요하기 때문에, 숫자 포맷, 색상 구분, 갱신 상태 표시가 실제 체감 품질에 큰 영향을 준다.

예를 들어 main/LVGL_UI/LVGL_Example.c에서

static void format_price_with_comma(int32_t price, char *out, size_t out_size)

이 함수는 가격에 천 단위 구분기호를 붙이고, 등락값과 등락률을 부호와 함께 표시하기 위한 포맷팅 함수가 들어 있다. 숫자를 빠르게 읽히게 만들었다.

레이아웃 자체는 main/LVGL_UI/LVGL_Example.c Onboard_create()에서 만들어지고, 실제 값 갱신은 main/LVGL_UI/LVGL_Example.c example1_increase_lvgl_tick() 타이머 콜백에서 처리된다. 이 콜백에서는 최신 데이터가 들어왔는지 확인하고, 가격과 등락률 문자열을 다시 만들고, 상승과 하락에 따라 텍스트 색과 진행 바 색을 바꾼다. 값이 아직 없을 때는 Loading 대신 현재 상태 문자열을 보여주도록 처리한 점도 실제 사용성을 높여준다.

이 타이머 콜백에서 특히 좋은 부분은 ETF_Update_Sequence 를 이용한 갱신 감지다. 네트워크 태스크가 새 데이터를 가져올 때마다 시퀀스를 올리고, UI는 이 값이 바뀌었는지만 확인해 refresh bar 를 다시 채운다. 이 방식은 시간을 직접 계산하지 않고도 "새 데이터가 들어왔는지"를 UI 관점에서 간단히 판단할 수 있게 해준다. 임베디드 UI에서는 이런 작은 상태 플래그 하나가 전체 구조를 훨씬 단순하게 만든다.

 

    if (strcmp(s_last_smr_text, buf) != 0)
    {
      lv_label_set_text(FlashSize, buf);
      strncpy(s_last_smr_text, buf, sizeof(s_last_smr_text) - 1);
    }


코드에서는 새로 만든 문자열이 이전 문자열과 다를 때만 lv_label_set_text 를 호출한다. 이런 패턴은 LVGL 호출을 불필요하게 반복하지 않도록 해 주고, 작은 장치에서 잦은 UI 업데이트로 인한 부담을 줄이는 데 도움이 된다. 겉으로는 사소해 보여도, 실제 장치에서 화면 갱신이 많은 경우 꽤 의미 있는 습관이다.

 

static void Onboard_create(lv_obj_t *parent)
{
  lv_obj_t *panel1 = lv_obj_create(parent);
  lv_obj_set_size(panel1, lv_pct(100), lv_pct(100));
  lv_obj_set_style_pad_all(panel1, 14, 0);
  lv_obj_set_style_pad_row(panel1, 14, 0);
  lv_obj_set_flex_flow(panel1, LV_FLEX_FLOW_COLUMN);
  lv_obj_clear_flag(panel1, LV_OBJ_FLAG_SCROLLABLE);
  lv_obj_set_scrollbar_mode(panel1, LV_SCROLLBAR_MODE_OFF);

  kodex200_title_label = lv_label_create(panel1);
  lv_label_set_text(kodex200_title_label, "KODEX200");
  lv_obj_add_style(kodex200_title_label, &style_symbol, 0);

  SD_Size = lv_label_create(panel1);
  lv_obj_set_width(SD_Size, lv_pct(100));
  lv_label_set_long_mode(SD_Size, LV_LABEL_LONG_CLIP);
  lv_label_set_text(SD_Size, "Loading...");
  lv_obj_add_style(SD_Size, &style_value, 0);

  kodexsmr_title_label = lv_label_create(panel1);
  lv_label_set_text(kodexsmr_title_label, "KODEX SMR");
  lv_obj_add_style(kodexsmr_title_label, &style_symbol, 0);

  FlashSize = lv_label_create(panel1);
  lv_obj_set_width(FlashSize, lv_pct(100));
  lv_label_set_long_mode(FlashSize, LV_LABEL_LONG_CLIP);
  lv_label_set_text(FlashSize, "Loading...");
  lv_obj_add_style(FlashSize, &style_value, 0);

  refresh_bar = lv_bar_create(panel1);
  lv_obj_set_width(refresh_bar, lv_pct(100));
  lv_obj_set_height(refresh_bar, 6);
  lv_bar_set_range(refresh_bar, 0, CONFIG_APP_STOCK_UPDATE_PERIOD_SEC);
  lv_bar_set_value(refresh_bar, CONFIG_APP_STOCK_UPDATE_PERIOD_SEC, LV_ANIM_OFF);
  lv_obj_set_style_bg_color(refresh_bar, lv_color_hex(0xD7D9DC), LV_PART_MAIN);
  lv_obj_set_style_bg_opa(refresh_bar, LV_OPA_70, LV_PART_MAIN);
  lv_obj_set_style_bg_color(refresh_bar, lv_color_hex(0x505050), LV_PART_INDICATOR);
  lv_obj_set_style_bg_opa(refresh_bar, LV_OPA_COVER, LV_PART_INDICATOR);
  lv_obj_set_style_radius(refresh_bar, LV_RADIUS_CIRCLE, LV_PART_MAIN | LV_PART_INDICATOR);

  KODEX200_Field = SD_Size;
  KODEXSMR_Field = FlashSize;

  auto_step_timer = lv_timer_create(example1_increase_lvgl_tick, 1000, NULL);
}


LVGL 예제 화면을 KODEX200 시세 보드 형태로 바꾸는 레이아웃 생성 구간. 제목, 값 표시 영역, 갱신 바를 한 화면에 배치했다. 예제를 바이브 코딩으로 변경하다보니 기존 변수 이름이 그대로 사용되고 있다. SD_Size, FlashSize등
화면을 화려하게 꾸미기보다, 작은 LCD에서 정보를 빠르게 읽히게 만드는 방향으로 레이아웃을 단순화했다.

이 부분은 실제 데이터가 화면으로 반영되는 과정을 설명할 때 가장 좋은 캡처다. ETF_Update_Sequence 로 새 데이터 도착 여부를 확인하고, refresh bar 값을 조절하고, 가격과 등락률 문자열을 만든 뒤, 상승과 하락에 따라 텍스트 색을 바꾸는 흐름이 모두 들어 있다.

 

void example1_increase_lvgl_tick(lv_timer_t *t)
{
  char buf[100] = {0};
  char numbuf[32] = {0};
  char diffbuf[32] = {0};
  char pctbuf[24] = {0};
  int32_t diff = 0;
  lv_color_t flat_color = lv_color_hex(0x202020);

  if (ETF_Update_Sequence != s_last_update_sequence)
  {
    s_last_update_sequence = ETF_Update_Sequence;
    s_refresh_remaining_sec = CONFIG_APP_STOCK_UPDATE_PERIOD_SEC;
  }
  else if (s_refresh_remaining_sec > 0)
  {
    s_refresh_remaining_sec--;
  }

  lv_bar_set_value(refresh_bar, s_refresh_remaining_sec, LV_ANIM_OFF);
  lv_obj_set_style_bg_color(refresh_bar, get_trend_color(KODEX200_Change), LV_PART_INDICATOR);

  if (ETF_Price_Valid)
  {
    format_price_with_comma(KODEX200_Price, numbuf, sizeof(numbuf));
    diff = KODEX200_Change;
    format_signed_with_comma(diff, diffbuf, sizeof(diffbuf));
    format_signed_percent_bp(KODEX200_ChangeBp, pctbuf, sizeof(pctbuf));
    snprintf(buf, sizeof(buf), "%s KRW (%s, %s%%)", numbuf, diffbuf, pctbuf);
    if (strcmp(s_last_kodex200_text, buf) != 0)
    {
      lv_label_set_text(SD_Size, buf);
      strncpy(s_last_kodex200_text, buf, sizeof(s_last_kodex200_text) - 1);
    }
    lv_obj_set_style_text_color(SD_Size, get_trend_color(diff), 0);

    format_price_with_comma(KODEXSMR_Price, numbuf, sizeof(numbuf));
    diff = KODEXSMR_Change;
    format_signed_with_comma(diff, diffbuf, sizeof(diffbuf));
    format_signed_percent_bp(KODEXSMR_ChangeBp, pctbuf, sizeof(pctbuf));
    snprintf(buf, sizeof(buf), "%s KRW (%s, %s%%)", numbuf, diffbuf, pctbuf);
    if (strcmp(s_last_smr_text, buf) != 0)
    {
      lv_label_set_text(FlashSize, buf);
      strncpy(s_last_smr_text, buf, sizeof(s_last_smr_text) - 1);
    }
    lv_obj_set_style_text_color(FlashSize, get_trend_color(diff), 0);
  }
  else
  {
    snprintf(buf, sizeof(buf), "%.95s", ETF_Status);
    if (strcmp(s_last_kodex200_text, buf) != 0)
    {
      lv_label_set_text(SD_Size, buf);
      strncpy(s_last_kodex200_text, buf, sizeof(s_last_kodex200_text) - 1);
    }
    lv_obj_set_style_text_color(SD_Size, flat_color, 0);

    snprintf(buf, sizeof(buf), "%.95s", ETF_Status);
    if (strcmp(s_last_smr_text, buf) != 0)
    {
      lv_label_set_text(FlashSize, buf);
      strncpy(s_last_smr_text, buf, sizeof(s_last_smr_text) - 1);
    }
    lv_obj_set_style_text_color(FlashSize, flat_color, 0);
  }
}


UI 객체 생성과 데이터 반영 로직을 분리해 두었기 때문에, 화면 구조를 설명하는 코드와 실제 갱신 코드를 따로 읽을 수 있다". 문자열이 바뀐 경우에만 lv_label_set_text 를 호출한다.

 

 

5. 화면 말고도 상태를 어떻게 표현했나

이 프로젝트는 화면 출력 외에도 RGB LED를 보조 상태 표시로 활용한다.

void _RGB_Example(void *arg)
{
    while (1)
    {
        if (!ETF_Price_Valid)
        {
            Set_RGB(20, 20, 20);
        }
        else if (KODEX200_Change > 0)
        {
            Set_RGB(96, 0, 0);
        }
        else if (KODEX200_Change < 0)
        {
            Set_RGB(0, 0, 96);
        }
        else
        {
            Set_RGB(32, 32, 32);
        }

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

데이터가 아직 유효하지 않을 때는 중립색을 켜고, KODEX200이 상승이면 붉은 계열, 하락이면 푸른 계열로 LED 색을 바꾸도록 되어 있다. 즉, 숫자를 직접 읽지 않아도 현재 상태를 한눈에 파악할 수 있게 만든 것이다.

구현 자체는 단순하지만 구조적으로는 꽤 깔끔하다. main/RGB/RGB.c 의 _RGB_Example() 태스크는 별도의 계산을 하지 않고 ETF_Price_Valid 와 KODEX200_Change 라는 공용 상태만 읽는다. 다시 말해, LED 모듈은 데이터 수집 방식이나 UI 구조를 몰라도 되고, 오직 "지금 상태가 무엇인가"만 알면 된다. 이런 방식은 모듈 간 결합도를 낮추고, 나중에 LED 정책만 따로 바꾸기도 쉽게 만든다.

 



6. 바이브 코딩으로 만들었지만, 검증은 따로 필요했다

이번 작업은 상당 부분을 바이브 코딩 방식으로 빠르게 밀어붙였다. 이미 동작하는 예제를 기반으로 필요한 기능을 덧붙이고, 결과를 보면서 구조를 정리하는 식으로 진행한 것이다. 이런 방식은 초반 속도가 빠르다는 장점이 있다. 화면이 먼저 살아나고, 네트워크가 붙고, 값이 보이기 시작하면 프로젝트가 금방 실체를 갖는다.

하지만 임베디드에서는 여기서 끝내기 어렵다. 초기화 순서가 조금만 어긋나도 화면이 비정상적으로 동작할 수 있고, 시간 동기화가 안 되면 HTTPS가 실패할 수 있고, 값이 아직 없을 때의 UI 처리를 놓치면 결과가 쉽게 어색해진다. 그래서 이번 프로젝트에서는 AI를 활용해 빠르게 전진하되, 초기화 순서, 연결 실패 케이스, UI 갱신 타이밍, 실제 보드 출력은 결국 직접 확인해야 했다.

실제 코드 기준으로 보면 사람이 꼭 확인해야 하는 지점도 비교적 명확하다. main/Wireless/Wireless.c 이후의 HTTP 처리 코드는 서버 응답 길이와 상태 코드를 전제로 동작하므로, 응답 형식이 바뀌거나 길이 정보가 비정상적일 때 어떤 로그가 남는지 확인해야 한다. 또 main/LVGL_UI/LVGL_Example.c 의 UI 타이머는 1초 주기로 돌아가므로, 실제 업데이트 주기와 화면 잔상이 자연스러운지 장치에서 봐야 한다. 결국 이런 부분은 코드를 읽는 것만으로 충분하지 않고, 로그와 실제 화면을 함께 봐야 검증이 끝난다.

이 경험을 정리하면, 바이브 코딩은 출발 속도를 높이는 데는 확실히 유용하지만, 하드웨어가 얽히는 순간부터는 검증을 대신해 주지 않는다. 특히 임베디드 프로젝트에서는 "컴파일된다"와 "장치에서 안정적으로 동작한다" 사이의 간격이 꽤 크다는 점을 다시 느꼈다.

 

개발중인 화면 캡쳐

 

 

7. 마무리

정리하면 이번 작업은 ESP32-C6-LCD-1.47 예제를 출발점으로 삼아, 외부에서 KODEX200 시세를 가져와 LCD에 표시하는 형태로 확장한 프로젝트였다. 단순히 값을 띄우는 데서 끝나지 않고, Wi-Fi 연결, 시간 동기화, 데이터 포맷팅, UI 갱신, 상태 표현까지 함께 다뤄야 했다는 점에서 예제 이상의 의미가 있었다. 예제 코드를 내 목적에 맞게 변형하는 과정 자체가 꽤 좋은 학습 경험이었고, 이후에는 종목 수를 늘리거나 입력 인터페이스를 붙이는 방향으로도 충분히 확장할 수 있을 것 같다.

 

 

참고문헌

마무리5

 

 
 

 

 

틀린 부분이나 질문은 댓글 달아주세요.

즐거운 하루 보내세요. 감사합니다.

 

 

반응형

댓글