> For the complete documentation index, see [llms.txt](https://docs.aic-eec.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aic-eec.com/interfacing-with-infineon-psoc-tm-edge/hmi-development/sensor-to-hmi-display/hardware-interfacing-workshops/chart-dashboard.md).

# Chart Dashboard

## Lab 11: Real Chart Dashboard

### Part 2 - Sensor Visualization

### 1. โครงสร้างภาพรวมของ Lab

#### Why? - ทำไมต้องเรียนรู้เรื่องนี้

* **Multi-Chart Visualization**: ในระบบ monitoring จริง ข้อมูล sensor เดียวกันแสดงได้หลายรูปแบบ ต้องเลือก chart type ที่เหมาะกับลักษณะข้อมูล
* **Active Tab Optimization**: เมื่อมี chart หลายตัว การอัพเดทเฉพาะ chart ที่มองเห็นช่วยลด CPU load อย่างมาก (ลดได้ 75%)
* **Custom Draw Events**: เทคนิค faded area chart ใช้ draw callback ซึ่งเป็นความสามารถขั้นสูงของ LVGL ที่ใช้ในระบบ production จริง
* **Tilt Analysis**: รวม Complementary Filter (Accel + Gyro fusion) เพื่อคำนวณ Roll/Pitch angle ที่แม่นยำ
* **Professional Dashboard**: รูปแบบ multi-chart dashboard ที่ใช้ใน SCADA, Process Control, และ Industrial IoT

#### What? - จะได้เรียนรู้อะไร

1. **Bar Chart (Real Data)**: เปรียบเทียบ Accel X/Y/Z แบบ side-by-side ด้วย `LV_CHART_TYPE_BAR` + colored series
2. **Area Chart (Faded)**: แสดง Tilt Magnitude พร้อม gradient fill ด้วย custom draw callback (`LV_EVENT_DRAW_TASK_ADDED`)
3. **Scatter Chart (2D Mapping)**: แสดง Roll vs Pitch เป็น "Tilt Ball" ด้วย `LV_CHART_TYPE_SCATTER` + `lv_chart_set_next_value2()`
4. **Line Chart (Multi-Series)**: แสดง Gyroscope Roll/Pitch/Yaw เป็น time-series
5. **TabView LEFT Layout**: จัดวาง 4 charts ใน tabs ด้านซ้าย (vertical tab bar 80px)
6. **Active Tab Optimization**: อัพเดทเฉพาะ tab ที่มองเห็นด้วย `lv_tabview_get_tab_active()`
7. **Tilt Integration**: ใช้ `aic_tilt_init()` + `aic_tilt_update_from_imu()` สำหรับ Area และ Scatter tabs

#### How? - จะทำอย่างไร

1. เรียก `aic_sensors_init()` + `aic_tilt_init(NULL)` ก่อนสร้าง UI
2. สร้าง TabView แบบ LEFT tab bar (80px) พร้อม 4 tabs: Bar, Area, Scatter, Line
3. แต่ละ tab สร้าง chart ด้วยฟังก์ชันแยก (`ex11_create_xxx_tab()`)
4. Timer callback (100ms) อ่าน IMU + Gyro + Tilt แล้วอัพเดทเฉพาะ active tab
5. Tab Area ใช้ custom draw callback สำหรับ faded gradient effect

<figure><img src="https://1856353139-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MClo3nC-1US0rbK8Qau%2Fuploads%2F9PgM0BTaQuCW5nQOzfo9%2Fweek4_ex6_chart_dashboard.gif?alt=media&amp;token=2928fac6-dffb-4875-adcc-68dc764060bb" alt=""><figcaption></figcaption></figure>

***

### 2. หลักการทำงานและ Flowchart

#### 2.1 Dashboard Architecture

```
┌─────────────────────────────────────────────────────────────┐
│              CHART DASHBOARD ARCHITECTURE                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌──────────────────────────────────────────────────────┐  │
│   │                 SENSOR LAYER                         │  │
│   │                                                      │  │
│   │   ┌───────────────┐    ┌───────────────┐             │  │
│   │   │  Accelerometer│    │   Gyroscope   │             │  │
│   │   │   ax, ay, az  │    │   gx, gy, gz  │             │  │
│   │   └───────┬───────┘    └───────┬───────┘             │  │
│   │           │                    │                     │  │
│   │           └────────┬───────────┘                     │  │
│   │                    │                                 │  │
│   │           ┌────────▼────────┐                        │  │
│   │           │  Tilt Analysis  │                        │  │
│   │           │   Roll, Pitch   │                        │  │
│   │           └────────┬────────┘                        │  │
│   └────────────────────┼─────────────────────────────────┘  │
│                        │                                    │
│   ┌────────────────────▼─────────────────────────────────┐  │
│   │              TIMER CALLBACK                          │  │
│   │                                                      │  │
│   │   1. Read all sensors                                │  │
│   │   2. Get active_tab = lv_tabview_get_tab_active()    │  │
│   │   3. switch(active_tab) - Update ONLY that chart     │  │
│   │                                                      │  │
│   └──────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

#### 2.2 Tab Layout (Side Tabs)

```
┌─────────────────────────────────────────────────────────────┐
│  ┌────────┐                                                 │
│  │  Bar   │  ╔═══════════════════════════════════════════╗  │
│  │ Chart  │  ║         Tab Content (400x320)             ║  │
│  ├────────┤  ║                                           ║  │
│  │  Area  │  ║   Tab 0: Bar Chart (Accel X/Y/Z)          ║  │
│  │ Chart  │  ║      [====X====]  X: +0.5                 ║  │
│  ├────────┤  ║      [====Y====]  Y: -0.3                 ║  │
│  │Scatter │  ║      [====Z====]  Z: +9.8                 ║  │
│  │ Chart  │  ║                                           ║  │
│  ├────────┤  ║   Tab 1: Area Chart (Tilt Magnitude)      ║  │
│  │  Line  │  ║      ╱╲    ╱╲                             ║  │
│  │ Chart  │  ║     ╱  ╲  ╱  ╲╱╲                          ║  │
│  └────────┘  ║    ╱    ╲╱      ╲  (faded fill)           ║  │
│              ║                                           ║  │
│   Tab Bar    ║   Tab 2: Scatter (Roll vs Pitch)          ║  │
│    80px      ║           •  •                            ║  │
│              ║        •   •   •  (motion pattern)        ║  │
│              ║                                           ║  │
│              ║   Tab 3: Line Chart (Gyroscope)           ║  │
│              ║      ─── Roll                             ║  │
│              ║      ─── Pitch                            ║  │
│              ║      ─── Yaw                              ║  │
│              ╚═══════════════════════════════════════════╝  │
└─────────────────────────────────────────────────────────────┘
```

#### 2.3 Chart Data Sources

```
┌─────────────────────────────────────────────────────────────┐
│                    DATA SOURCES PER TAB                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Tab 0: Bar Chart                                          │
│     Source: aic_imu_read_accel(&ax, &ay, &az)               │
│     Mapping: -5..+5 m/s² → 0..100 (sensitive)               │
│                                                             │
│   Tab 1: Area Chart                                         │
│     Source: aic_tilt_get_roll(), aic_tilt_get_pitch()       │
│     Mapping: magnitude = sqrt(roll² + pitch²) degrees       │
│     Range: 0..90 degrees                                    │
│                                                             │
│   Tab 2: Scatter Chart                                      │
│     Source: Roll, Pitch from Tilt Analysis                  │
│     X-axis: Roll (-90..+90 → 0..200)                        │
│     Y-axis: Pitch (-90..+90 → 0..200)                       │
│     Center = 100,100 (level position)                       │
│                                                             │
│   Tab 3: Line Chart                                         │
│     Source: aic_imu_read_gyro(&gx, &gy, &gz)                │
│     Mapping: -2..+2 rad/s → -200..+200                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

***

### 3. ฟังก์ชันสำคัญ

#### 3.1 Sensor & Tilt APIs

<table><thead><tr><th width="315.70098876953125">Function</th><th width="87.8572998046875">Return</th><th>Description</th></tr></thead><tbody><tr><td><code>aic_sensors_init()</code></td><td>void</td><td>Initialize sensor subsystem (เรียกครั้งเดียว)</td></tr><tr><td><code>aic_tilt_init(NULL)</code></td><td>void</td><td>Initialize Complementary Filter สำหรับ Tilt</td></tr><tr><td><code>aic_imu_read_accel(&#x26;ax, &#x26;ay, &#x26;az)</code></td><td>bool</td><td>อ่าน accelerometer (m/s2)</td></tr><tr><td><code>aic_imu_read_gyro(&#x26;gx, &#x26;gy, &#x26;gz)</code></td><td>bool</td><td>อ่าน gyroscope (rad/s)</td></tr><tr><td><code>aic_tilt_update_from_imu()</code></td><td>bool</td><td>อัพเดท tilt estimation (accel + gyro fusion)</td></tr><tr><td><code>aic_tilt_get_roll()</code></td><td>float</td><td>อ่าน roll angle (degrees)</td></tr><tr><td><code>aic_tilt_get_pitch()</code></td><td>float</td><td>อ่าน pitch angle (degrees)</td></tr></tbody></table>

#### 3.2 Chart Type APIs

<table><thead><tr><th width="130.30963134765625">Chart Type</th><th width="250.8011474609375">Create</th><th>Update</th><th>Description</th></tr></thead><tbody><tr><td>BAR</td><td><code>lv_chart_set_type(chart, LV_CHART_TYPE_BAR)</code></td><td><code>lv_chart_set_value_by_id(chart, ser, id, val)</code></td><td>แท่งเปรียบเทียบค่า</td></tr><tr><td>LINE</td><td><code>lv_chart_set_type(chart, LV_CHART_TYPE_LINE)</code></td><td><code>lv_chart_set_next_value(chart, ser, val)</code></td><td>เส้น time-series</td></tr><tr><td>SCATTER</td><td><code>lv_chart_set_type(chart, LV_CHART_TYPE_SCATTER)</code></td><td><code>lv_chart_set_next_value2(chart, ser, x, y)</code></td><td>จุด X-Y correlation</td></tr></tbody></table>

#### 3.3 TabView APIs

<table><thead><tr><th width="440.62640380859375">Function</th><th>Description</th></tr></thead><tbody><tr><td><code>lv_tabview_create(parent)</code></td><td>สร้าง TabView</td></tr><tr><td><code>lv_tabview_set_tab_bar_position(tv, LV_DIR_LEFT)</code></td><td>Tab bar ด้านซ้าย</td></tr><tr><td><code>lv_tabview_set_tab_bar_size(tv, 80)</code></td><td>ความกว้าง tab bar 80px</td></tr><tr><td><code>lv_tabview_add_tab(tv, "name")</code></td><td>เพิ่ม tab ใหม่</td></tr><tr><td><code>lv_tabview_get_tab_active(tv)</code></td><td>อ่าน active tab index (0, 1, 2, 3)</td></tr><tr><td><code>lv_tabview_get_tab_bar(tv)</code></td><td>อ่าน tab button bar object</td></tr><tr><td><code>lv_tabview_get_content(tv)</code></td><td>อ่าน content container</td></tr></tbody></table>

#### 3.4 Draw Callback APIs (สำหรับ Faded Area)

<table><thead><tr><th width="361.688232421875">Function</th><th>Description</th></tr></thead><tbody><tr><td><code>lv_obj_add_event_cb(obj, cb, LV_EVENT_DRAW_TASK_ADDED, NULL)</code></td><td>Register draw callback</td></tr><tr><td><code>lv_obj_add_flag(obj, LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS)</code></td><td>Enable draw task events</td></tr><tr><td><code>lv_event_get_draw_task(e)</code></td><td>อ่าน draw task จาก event</td></tr><tr><td><code>lv_draw_task_get_draw_dsc(task)</code></td><td>อ่าน draw descriptor</td></tr><tr><td><code>lv_draw_task_get_type(task)</code></td><td>อ่าน draw type (LINE, RECT, etc.)</td></tr><tr><td><code>lv_draw_triangle(layer, &#x26;dsc)</code></td><td>วาดสามเหลี่ยม (gradient fill)</td></tr><tr><td><code>lv_draw_rect(layer, &#x26;dsc, &#x26;area)</code></td><td>วาดสี่เหลี่ยม (fade to transparent)</td></tr></tbody></table>

#### 3.5 Include Files

```c
#include "lvgl.h"
#include <math.h>
#include <stdio.h>

#ifdef CYBSP_ENABLED
#include "aic-eec/aic-eec.h"
#include "aic-eec/sensors.h"
#include "aic-eec/tilt.h"
#include "../../shared/imu_shared.h"
#endif
```

***

### 4. โค้ดตัวอย่าง

#### 4.1 Global Variables & Defines

```c
#define EX11_CHART_POINTS    50
#define EX11_SCATTER_POINTS  30

/* Tab 1: Bar Chart - current X, Y, Z values with colors */
static lv_obj_t * ex11_bar_chart;
static lv_chart_series_t * ex11_bar_ser_x;  /* Red for X */
static lv_chart_series_t * ex11_bar_ser_y;  /* Green for Y */
static lv_chart_series_t * ex11_bar_ser_z;  /* Blue for Z */
static lv_obj_t * ex11_bar_labels[3];

/* Tab 2: Area Chart - tilt magnitude over time */
static lv_obj_t * ex11_area_chart;
static lv_chart_series_t * ex11_area_ser;
static lv_obj_t * ex11_area_label;

/* Tab 3: Scatter Chart - Roll vs Pitch (Tilt Ball) */
static lv_obj_t * ex11_scatter_chart;
static lv_chart_series_t * ex11_scatter_ser;
static lv_obj_t * ex11_scatter_label;

/* Tab 4: Line Chart - Gyroscope time series */
static lv_obj_t * ex11_line_chart;
static lv_chart_series_t * ex11_line_ser[3];
static lv_obj_t * ex11_line_labels[3];

/* Timer and TabView reference */
static lv_timer_t * ex11_chart_timer;
static lv_obj_t * ex11_tabview;
```

#### 4.2 Simulation Helpers (PC Simulator)

```c
#ifndef CYBSP_ENABLED
static bool simulate_imu_accel(float *ax, float *ay, float *az)
{
    static float t = 0.0f;
    t += 0.05f;
    *ax = 2.0f * sinf(t);
    *ay = 1.5f * cosf(t * 0.7f);
    *az = 9.81f + 0.5f * sinf(t * 2.0f);
    return true;
}

static bool simulate_imu_gyro(float *gx, float *gy, float *gz)
{
    static float t = 0.0f;
    t += 0.03f;
    *gx = 0.8f * sinf(t * 1.2f);
    *gy = 0.5f * cosf(t * 0.8f);
    *gz = 0.3f * sinf(t * 1.5f);
    return true;
}

static float sim_roll = 0.0f, sim_pitch = 0.0f;

static bool simulate_tilt_update(void)
{
    static float t = 0.0f;
    t += 0.05f;
    sim_roll = 30.0f * sinf(t * 0.5f);
    sim_pitch = 20.0f * cosf(t * 0.3f);
    return true;
}
#endif
```

#### 4.3 Timer Callback - Active Tab Optimization

นี่คือหัวใจสำคัญของ Lab นี้ -- timer callback ที่อ่านข้อมูลจากทุก sensor แต่อัพเดทเฉพาะ chart ของ tab ที่กำลังแสดงผลอยู่:

```c
static void ex11_chart_timer_cb(lv_timer_t * timer)
{
    (void)timer;

    /* ===== Read ALL sensor data ===== */
    float ax, ay, az;
    float gx, gy, gz;

#ifdef CYBSP_ENABLED
    if(!aic_imu_read_accel(&ax, &ay, &az)) {
        ax = 0; ay = 0; az = 9.81f;
    }
    if(!aic_imu_read_gyro(&gx, &gy, &gz)) {
        gx = 0; gy = 0; gz = 0;
    }
    aic_tilt_update_from_imu();
    float roll = aic_tilt_get_roll();
    float pitch = aic_tilt_get_pitch();
#else
    simulate_imu_accel(&ax, &ay, &az);
    simulate_imu_gyro(&gx, &gy, &gz);
    simulate_tilt_update();
    float roll = sim_roll;
    float pitch = sim_pitch;
#endif

    /* ===== Get active tab index ===== */
    /* 0=Bar, 1=Area, 2=Scatter, 3=Line */
    uint32_t active_tab = lv_tabview_get_tab_active(ex11_tabview);

    /* ===== Update ONLY the active tab ===== */
    switch(active_tab) {

        case 0: /* Tab 1: Bar Chart - Accel X/Y/Z */
        {
            /* Scale: -5 to +5 m/s2 -> 0 to 100 */
            int32_t bar_x = (int32_t)((ax + 5.0f) * 10.0f);
            int32_t bar_y = (int32_t)((ay + 5.0f) * 10.0f);
            int32_t bar_z = (int32_t)((az + 5.0f) * 10.0f);

            /* Clamp to range */
            if(bar_x < 0) bar_x = 0; if(bar_x > 100) bar_x = 100;
            if(bar_y < 0) bar_y = 0; if(bar_y > 100) bar_y = 100;
            if(bar_z < 0) bar_z = 0; if(bar_z > 100) bar_z = 100;

            /* Update each colored bar */
            lv_chart_set_value_by_id(ex11_bar_chart,
                ex11_bar_ser_x, 0, bar_x);
            lv_chart_set_value_by_id(ex11_bar_chart,
                ex11_bar_ser_y, 0, bar_y);
            lv_chart_set_value_by_id(ex11_bar_chart,
                ex11_bar_ser_z, 0, bar_z);
            lv_chart_refresh(ex11_bar_chart);

            lv_label_set_text_fmt(ex11_bar_labels[0],
                "X: %.1f", (double)ax);
            lv_label_set_text_fmt(ex11_bar_labels[1],
                "Y: %.1f", (double)ay);
            lv_label_set_text_fmt(ex11_bar_labels[2],
                "Z: %.1f", (double)az);
            break;
        }

        case 1: /* Tab 2: Area Chart - Tilt Magnitude */
        {
            /* Tilt magnitude: sqrt(roll^2 + pitch^2) */
            float tilt_mag = sqrtf(roll * roll + pitch * pitch);
            int32_t tilt_scaled = (int32_t)(tilt_mag);
            if(tilt_scaled > 90) tilt_scaled = 90;

            lv_chart_set_next_value(ex11_area_chart,
                ex11_area_ser, tilt_scaled);
            lv_label_set_text_fmt(ex11_area_label,
                "Tilt: %.1f deg (R:%.1f P:%.1f)",
                (double)tilt_mag, (double)roll, (double)pitch);
            break;
        }

        case 2: /* Tab 3: Scatter - Roll vs Pitch */
        {
            /* Map: -90..+90 -> 0..200 (center=100) */
            int32_t scatter_x = (int32_t)(
                (roll + 90.0f) * (200.0f / 180.0f));
            int32_t scatter_y = (int32_t)(
                (pitch + 90.0f) * (200.0f / 180.0f));

            if(scatter_x < 0) scatter_x = 0;
            if(scatter_x > 200) scatter_x = 200;
            if(scatter_y < 0) scatter_y = 0;
            if(scatter_y > 200) scatter_y = 200;

            lv_chart_set_next_value2(ex11_scatter_chart,
                ex11_scatter_ser, scatter_x, scatter_y);
            lv_label_set_text_fmt(ex11_scatter_label,
                "Roll: %.1f  Pitch: %.1f",
                (double)roll, (double)pitch);
            break;
        }

        case 3: /* Tab 4: Line Chart - Gyroscope */
        {
            /* Scale by 100 (Ex7 pattern) */
            int32_t gx_scaled = (int32_t)(gx * 100);
            int32_t gy_scaled = (int32_t)(gy * 100);
            int32_t gz_scaled = (int32_t)(gz * 100);

            lv_chart_set_next_value(ex11_line_chart,
                ex11_line_ser[0], gx_scaled);
            lv_chart_set_next_value(ex11_line_chart,
                ex11_line_ser[1], gy_scaled);
            lv_chart_set_next_value(ex11_line_chart,
                ex11_line_ser[2], gz_scaled);

            lv_label_set_text_fmt(ex11_line_labels[0],
                "Gx: %.2f", (double)gx);
            lv_label_set_text_fmt(ex11_line_labels[1],
                "Gy: %.2f", (double)gy);
            lv_label_set_text_fmt(ex11_line_labels[2],
                "Gz: %.2f", (double)gz);
            break;
        }
    }
}
```

#### 4.4 Tab 1: Bar Chart - Real Accel X/Y/Z

```c
static void ex11_create_bar_chart_tab(lv_obj_t * parent)
{
    /* Title */
    lv_obj_t * title = lv_label_create(parent);
    lv_label_set_text(title, "Bar: Real Accel X/Y/Z");
    lv_obj_set_style_text_font(title, LV_FONT_DEFAULT, 0);
    lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 5);

    /* Create Bar Chart - 3 colored bars for X, Y, Z */
    ex11_bar_chart = lv_chart_create(parent);
    lv_obj_set_size(ex11_bar_chart, 390, 280);
    lv_obj_align(ex11_bar_chart, LV_ALIGN_CENTER, 0, 0);
    lv_chart_set_type(ex11_bar_chart, LV_CHART_TYPE_BAR);
    lv_chart_set_point_count(ex11_bar_chart, 1);
    lv_chart_set_range(ex11_bar_chart,
        LV_CHART_AXIS_PRIMARY_Y, 0, 100);

    /* Add 3 colored series: X=Red, Y=Green, Z=Blue */
    ex11_bar_ser_x = lv_chart_add_series(ex11_bar_chart,
        lv_palette_main(LV_PALETTE_RED),
        LV_CHART_AXIS_PRIMARY_Y);
    ex11_bar_ser_y = lv_chart_add_series(ex11_bar_chart,
        lv_palette_main(LV_PALETTE_GREEN),
        LV_CHART_AXIS_PRIMARY_Y);
    ex11_bar_ser_z = lv_chart_add_series(ex11_bar_chart,
        lv_palette_main(LV_PALETTE_BLUE),
        LV_CHART_AXIS_PRIMARY_Y);

    /* Set initial values */
    lv_chart_set_value_by_id(ex11_bar_chart,
        ex11_bar_ser_x, 0, 50);
    lv_chart_set_value_by_id(ex11_bar_chart,
        ex11_bar_ser_y, 0, 50);
    lv_chart_set_value_by_id(ex11_bar_chart,
        ex11_bar_ser_z, 0, 98);

    /* Style: spacing between colored bars */
    lv_obj_set_style_pad_column(ex11_bar_chart, 30, 0);

    /* Value labels below chart */
    const char *axis_names[] = {"X", "Y", "Z"};
    lv_color_t colors[] = {
        lv_palette_main(LV_PALETTE_RED),
        lv_palette_main(LV_PALETTE_GREEN),
        lv_palette_main(LV_PALETTE_BLUE)
    };

    for(int i = 0; i < 3; i++) {
        ex11_bar_labels[i] = lv_label_create(parent);
        lv_label_set_text_fmt(ex11_bar_labels[i],
            "%s: 0.00", axis_names[i]);
        lv_obj_set_style_text_color(
            ex11_bar_labels[i], colors[i], 0);
        lv_obj_align(ex11_bar_labels[i],
            LV_ALIGN_BOTTOM_LEFT, 30 + i * 120, -5);
    }
}
```

#### 4.5 Faded Area Draw Callback

นี่คือ draw callback ที่สร้าง gradient fill ใต้เส้น chart ซึ่งอ้างอิงจาก `lv_example_chart_5` ของ LVGL:

```c
static void ex11_area_draw_cb(lv_event_t * e)
{
    lv_draw_task_t * draw_task = lv_event_get_draw_task(e);
    lv_draw_dsc_base_t * base_dsc =
        lv_draw_task_get_draw_dsc(draw_task);

    /* Only process line items (chart line segments) */
    if(base_dsc->part != LV_PART_ITEMS) return;
    if(lv_draw_task_get_type(draw_task) !=
        LV_DRAW_TASK_TYPE_LINE) return;

    lv_obj_t * obj = lv_event_get_target(e);
    lv_area_t coords;
    lv_obj_get_coords(obj, &coords);

    /* Get series color for gradient */
    const lv_chart_series_t * ser =
        lv_chart_get_series_next(obj, NULL);
    lv_color_t ser_color =
        lv_chart_get_series_color(obj, ser);

    lv_draw_line_dsc_t * draw_line_dsc =
        lv_draw_task_get_draw_dsc(draw_task);

    /* ===== TRIANGLE: Fill between line and lowest point ===== */
    lv_draw_triangle_dsc_t tri_dsc;
    lv_draw_triangle_dsc_init(&tri_dsc);
    tri_dsc.p[0].x = draw_line_dsc->p1.x;
    tri_dsc.p[0].y = draw_line_dsc->p1.y;
    tri_dsc.p[1].x = draw_line_dsc->p2.x;
    tri_dsc.p[1].y = draw_line_dsc->p2.y;
    tri_dsc.p[2].x = draw_line_dsc->p1.y < draw_line_dsc->p2.y
        ? draw_line_dsc->p1.x : draw_line_dsc->p2.x;
    tri_dsc.p[2].y = LV_MAX(draw_line_dsc->p1.y,
                             draw_line_dsc->p2.y);
    tri_dsc.bg_grad.dir = LV_GRAD_DIR_VER;

    /* Calculate opacity based on Y position */
    int32_t full_h = lv_obj_get_height(obj);
    int32_t fract_upper = (int32_t)(
        LV_MIN(draw_line_dsc->p1.y, draw_line_dsc->p2.y)
        - coords.y1) * 255 / full_h;
    int32_t fract_lower = (int32_t)(
        LV_MAX(draw_line_dsc->p1.y, draw_line_dsc->p2.y)
        - coords.y1) * 255 / full_h;

    tri_dsc.bg_grad.stops[0].color = ser_color;
    tri_dsc.bg_grad.stops[0].opa = 255 - fract_upper;
    tri_dsc.bg_grad.stops[0].frac = 0;
    tri_dsc.bg_grad.stops[1].color = ser_color;
    tri_dsc.bg_grad.stops[1].opa = 255 - fract_lower;
    tri_dsc.bg_grad.stops[1].frac = 255;

    lv_draw_triangle(base_dsc->layer, &tri_dsc);

    /* ===== RECTANGLE: Fade from lowest point to bottom ===== */
    lv_draw_rect_dsc_t rect_dsc;
    lv_draw_rect_dsc_init(&rect_dsc);
    rect_dsc.bg_grad.dir = LV_GRAD_DIR_VER;
    rect_dsc.bg_grad.stops[0].color = ser_color;
    rect_dsc.bg_grad.stops[0].frac = 0;
    rect_dsc.bg_grad.stops[0].opa = 255 - fract_lower;
    rect_dsc.bg_grad.stops[1].color = ser_color;
    rect_dsc.bg_grad.stops[1].frac = 255;
    rect_dsc.bg_grad.stops[1].opa = 0;

    lv_area_t rect_area;
    rect_area.x1 = (int32_t)draw_line_dsc->p1.x;
    rect_area.x2 = (int32_t)draw_line_dsc->p2.x - 1;
    rect_area.y1 = (int32_t)LV_MAX(draw_line_dsc->p1.y,
                                     draw_line_dsc->p2.y) - 1;
    rect_area.y2 = (int32_t)coords.y2;
    lv_draw_rect(base_dsc->layer, &rect_dsc, &rect_area);
}
```

#### 4.6 Tab 2: Area Chart - Tilt Magnitude

```c
static void ex11_create_area_chart_tab(lv_obj_t * parent)
{
    /* Title */
    lv_obj_t * title = lv_label_create(parent);
    lv_label_set_text(title, "Area: Tilt Magnitude (deg)");
    lv_obj_set_style_text_font(title, LV_FONT_DEFAULT, 0);
    lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 5);

    /* Create Area Chart - tilt angle over time */
    ex11_area_chart = lv_chart_create(parent);
    lv_obj_set_size(ex11_area_chart, 390, 285);
    lv_obj_align(ex11_area_chart, LV_ALIGN_CENTER, 0, 0);
    lv_obj_set_style_pad_all(ex11_area_chart, 0, 0);
    lv_chart_set_type(ex11_area_chart, LV_CHART_TYPE_LINE);
    lv_chart_set_point_count(ex11_area_chart,
        EX11_CHART_POINTS);
    lv_chart_set_range(ex11_area_chart,
        LV_CHART_AXIS_PRIMARY_Y, 0, 90);

    /* Add horizontal reference lines */
    lv_chart_set_div_line_count(ex11_area_chart, 5, 8);

    /* CRITICAL: Enable draw task events for faded area */
    lv_obj_add_event_cb(ex11_area_chart,
        ex11_area_draw_cb,
        LV_EVENT_DRAW_TASK_ADDED, NULL);
    lv_obj_add_flag(ex11_area_chart,
        LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS);

    /* Add series */
    ex11_area_ser = lv_chart_add_series(ex11_area_chart,
        lv_palette_main(LV_PALETTE_RED),
        LV_CHART_AXIS_PRIMARY_Y);

    /* Show points and line */
    lv_obj_set_style_size(ex11_area_chart, 4, 4,
        LV_PART_INDICATOR);
    lv_obj_set_style_line_width(ex11_area_chart, 3,
        LV_PART_ITEMS);

    /* Value label */
    ex11_area_label = lv_label_create(parent);
    lv_label_set_text(ex11_area_label, "Tilt: 0.0 deg");
    lv_obj_set_style_text_color(ex11_area_label,
        lv_palette_main(LV_PALETTE_RED), 0);
    lv_obj_align(ex11_area_label,
        LV_ALIGN_BOTTOM_MID, 0, -5);
}
```

#### 4.7 Tab 3: Scatter Chart - Roll vs Pitch (Tilt Ball)

```c
static void ex11_create_scatter_chart_tab(lv_obj_t * parent)
{
    /* Title */
    lv_obj_t * title = lv_label_create(parent);
    lv_label_set_text(title,
        "Scatter: Roll vs Pitch (Tilt Ball)");
    lv_obj_set_style_text_font(title, LV_FONT_DEFAULT, 0);
    lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 5);

    /* Create Scatter Chart - square-ish for scatter */
    ex11_scatter_chart = lv_chart_create(parent);
    lv_obj_set_size(ex11_scatter_chart, 290, 280);
    lv_obj_align(ex11_scatter_chart,
        LV_ALIGN_CENTER, 0, 0);
    lv_chart_set_type(ex11_scatter_chart,
        LV_CHART_TYPE_SCATTER);
    lv_chart_set_point_count(ex11_scatter_chart,
        EX11_SCATTER_POINTS);

    /* X and Y ranges */
    lv_chart_set_range(ex11_scatter_chart,
        LV_CHART_AXIS_PRIMARY_X, 0, 200);
    lv_chart_set_range(ex11_scatter_chart,
        LV_CHART_AXIS_PRIMARY_Y, 0, 200);

    /* Add scatter series */
    ex11_scatter_ser = lv_chart_add_series(
        ex11_scatter_chart,
        lv_palette_main(LV_PALETTE_BLUE),
        LV_CHART_AXIS_PRIMARY_Y);

    /* Style: larger points, semi-transparent */
    lv_obj_set_style_size(ex11_scatter_chart,
        10, 10, LV_PART_INDICATOR);
    lv_obj_set_style_bg_opa(ex11_scatter_chart,
        LV_OPA_70, LV_PART_INDICATOR);

    /* Grid lines */
    lv_chart_set_div_line_count(ex11_scatter_chart, 5, 5);

    /* Value label */
    ex11_scatter_label = lv_label_create(parent);
    lv_label_set_text(ex11_scatter_label,
        "Roll: 0.0  Pitch: 0.0");
    lv_obj_set_style_text_color(ex11_scatter_label,
        lv_palette_main(LV_PALETTE_BLUE), 0);
    lv_obj_align(ex11_scatter_label,
        LV_ALIGN_BOTTOM_MID, 0, -5);
}
```

#### 4.8 Tab 4: Line Chart - Gyroscope R/P/Y

```c
static void ex11_create_line_chart_tab(lv_obj_t * parent)
{
    /* Title */
    lv_obj_t * title = lv_label_create(parent);
    lv_label_set_text(title, "Line: Real Gyro R/P/Y");
    lv_obj_set_style_text_font(title, LV_FONT_DEFAULT, 0);
    lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 5);

    /* Create Line Chart - Ex7's stable pattern */
    ex11_line_chart = lv_chart_create(parent);
    lv_obj_set_size(ex11_line_chart, 390, 275);
    lv_obj_align(ex11_line_chart,
        LV_ALIGN_CENTER, 0, -5);
    lv_chart_set_type(ex11_line_chart,
        LV_CHART_TYPE_LINE);
    lv_chart_set_point_count(ex11_line_chart,
        EX11_CHART_POINTS);

    /* Range: -1500 to 1500 (same as Ex7) */
    lv_chart_set_range(ex11_line_chart,
        LV_CHART_AXIS_PRIMARY_Y, -1500, 1500);

    /* Hide points, line width 2px */
    lv_obj_set_style_size(ex11_line_chart, 0, 0,
        LV_PART_INDICATOR);
    lv_obj_set_style_line_width(ex11_line_chart, 2,
        LV_PART_ITEMS);

    /* Grid lines */
    lv_chart_set_div_line_count(ex11_line_chart, 5, 5);

    /* Add 3 series: Roll(Red), Pitch(Green), Yaw(Blue) */
    lv_color_t colors[] = {
        lv_palette_main(LV_PALETTE_RED),
        lv_palette_main(LV_PALETTE_GREEN),
        lv_palette_main(LV_PALETTE_BLUE)
    };
    const char *names[] = {"Roll", "Pitch", "Yaw"};

    for(int i = 0; i < 3; i++) {
        ex11_line_ser[i] = lv_chart_add_series(
            ex11_line_chart, colors[i],
            LV_CHART_AXIS_PRIMARY_Y);

        ex11_line_labels[i] = lv_label_create(parent);
        lv_label_set_text_fmt(ex11_line_labels[i],
            "%s: 0.00", names[i]);
        lv_obj_set_style_text_color(
            ex11_line_labels[i], colors[i], 0);
        lv_obj_align(ex11_line_labels[i],
            LV_ALIGN_BOTTOM_LEFT, 20 + i * 125, -5);
    }
}
```

#### 4.9 Main Function - Assembling the Dashboard

```c
void part2_ex11_real_chart_dashboard(void)
{
    /* ===== HARDWARE INITIALIZATION ===== */
#ifdef CYBSP_ENABLED
    aic_sensors_init();
    aic_tilt_init(NULL);  /* Complementary Filter */
#endif

    /* ===== Create TabView (LEFT tab bar) ===== */
    ex11_tabview = lv_tabview_create(lv_screen_active());
    lv_obj_t * tabview = ex11_tabview;
    lv_tabview_set_tab_bar_position(tabview, LV_DIR_LEFT);
    lv_tabview_set_tab_bar_size(tabview, 80);

    /* Style the TabView background */
    lv_obj_set_style_bg_color(tabview,
        lv_palette_lighten(LV_PALETTE_BLUE, 2), 0);

    /* Style tab buttons */
    lv_obj_t * tab_btns = lv_tabview_get_tab_bar(tabview);
    lv_obj_set_style_bg_color(tab_btns,
        lv_palette_darken(LV_PALETTE_GREY, 3), 0);
    lv_obj_set_style_text_color(tab_btns,
        lv_palette_lighten(LV_PALETTE_GREY, 5), 0);
    lv_obj_set_style_border_side(tab_btns,
        LV_BORDER_SIDE_RIGHT,
        LV_PART_ITEMS | LV_STATE_CHECKED);

    /* ===== Create 4 Tabs ===== */
    lv_obj_t * tab_bar = lv_tabview_add_tab(tabview, "Bar");
    lv_obj_t * tab_area = lv_tabview_add_tab(tabview, "Area");
    lv_obj_t * tab_scatter = lv_tabview_add_tab(tabview,
        "Scatter");
    lv_obj_t * tab_line = lv_tabview_add_tab(tabview, "Line");

    /* Style tab content backgrounds */
    lv_obj_t * tabs[] = {tab_bar, tab_area,
                         tab_scatter, tab_line};
    for(int i = 0; i < 4; i++) {
        lv_obj_set_style_bg_color(tabs[i],
            lv_color_hex(0xF5F5F5), 0);
        lv_obj_set_style_bg_opa(tabs[i],
            LV_OPA_COVER, 0);
    }

    /* ===== Populate each tab ===== */
    ex11_create_bar_chart_tab(tab_bar);
    ex11_create_area_chart_tab(tab_area);
    ex11_create_scatter_chart_tab(tab_scatter);
    ex11_create_line_chart_tab(tab_line);

    /* CRITICAL: Disable scrolling on content */
    lv_obj_remove_flag(
        lv_tabview_get_content(tabview),
        LV_OBJ_FLAG_SCROLLABLE);

    /* ===== Timer: 100ms update ===== */
    ex11_chart_timer = lv_timer_create(
        ex11_chart_timer_cb, 100, NULL);

    /* ===== Footer ===== */
    aic_create_footer(lv_screen_active());

    printf("[Part2] Ex11: Real Chart Dashboard started\r\n");
}
```

***

### 5. องค์ความรู้และเทคนิค

#### 5.1 Active Tab Optimization - ทำไมต้องอัพเดทเฉพาะ Tab ที่มองเห็น

```c
/* ===== Problem: Update ทุก chart ทุกรอบ ===== */
/* 4 charts x 100ms = CPU load สูง, frame rate ต่ำ */
/* chart ที่ซ่อนอยู่ก็ต้อง redraw ทั้งที่ไม่มีคนเห็น */

/* ===== Solution: Update เฉพาะ active tab ===== */
uint32_t active = lv_tabview_get_tab_active(tabview);

switch(active) {
    case 0: update_bar_chart();     break;
    case 1: update_area_chart();    break;
    case 2: update_scatter_chart(); break;
    case 3: update_line_chart();    break;
}

/* ===== ข้อดี ===== */
/* 1. CPU load ลดลง 75% (update 1 chart แทน 4) */
/* 2. Frame rate สูงขึ้น */
/* 3. Sensor data ยังอ่านทุกรอบ (ไม่พลาดข้อมูล) */

/* ===== ข้อเสีย ===== */
/* 1. Tab ที่ซ่อนจะไม่มีข้อมูลใหม่ */
/* 2. เมื่อสลับ tab, chart จะแสดงข้อมูลเก่า */
/* 3. Area/Line chart จะขาดช่วง (gap) */

/* ===== Trade-off: เมื่อไหร่ใช้ pattern นี้ ===== */
/* - 4+ charts              -> ใช้ Active Tab Optimization */
/* - 2-3 charts (เบา ๆ)      -> อัพเดททุก chart ก็ได้ */
/* - Real-time critical     -> ต้องอัพเดททุกตัว */
```

#### 5.2 Area Chart - Custom Draw Callback อธิบาย

```c
/* ===== ทำไม Area Chart ไม่ใช่ built-in feature? ===== */
/* LVGL v9 ไม่มี chart type "AREA" โดยตรง */
/* ต้องสร้างเองโดยวาด shape ใต้เส้น line chart */

/* ===== ขั้นตอนการทำงาน ===== */

/* 1. Register callback + enable flag */
lv_obj_add_event_cb(chart, draw_cb,
    LV_EVENT_DRAW_TASK_ADDED, NULL);
lv_obj_add_flag(chart,
    LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS);

/* 2. ใน callback: ตรวจว่าเป็น line segment */
if(base_dsc->part == LV_PART_ITEMS &&
   lv_draw_task_get_type(task) == LV_DRAW_TASK_TYPE_LINE)
{
    /* 3. ดึง P1, P2 ของ line segment */
    /* P1 = จุดเริ่มต้น, P2 = จุดสิ้นสุด */

    /* 4. วาด TRIANGLE ระหว่าง P1, P2, จุดต่ำสุด */
    /* Triangle มี vertical gradient: */
    /*   - ด้านบน: opacity สูง (เห็นชัด) */
    /*   - ด้านล่าง: opacity ต่ำ (จาง) */

    /* 5. วาด RECTANGLE จากจุดต่ำสุดถึงก้น chart */
    /* Rectangle fade จาก opacity ต่ำ → โปร่งใส */
}

/* ===== ผลลัพธ์ ===== */
/* เส้น chart มี gradient fill ใต้เส้น */
/* ค่าสูง = สีเข้ม, ค่าต่ำ = สีจาง */
/* สวยงามและอ่านง่าย */
```

#### 5.3 Scatter Chart - 2D Mapping Pattern

```c
/* ===== Scatter Chart ต่างจาก Line Chart อย่างไร? ===== */

/* Line Chart: set_next_value(chart, ser, y_value) */
/* - X-axis = auto (time index: 0, 1, 2, ...) */
/* - Y-axis = value ที่กำหนด */
/* - เหมาะกับ time-series */

/* Scatter Chart: set_next_value2(chart, ser, x_val, y_val) */
/* - X-axis = value ที่กำหนด (ไม่ใช่ time index) */
/* - Y-axis = value ที่กำหนด */
/* - เหมาะกับ X-Y correlation / 2D mapping */

/* ===== Roll vs Pitch Mapping ===== */
/* Roll:  -90 to +90 degrees -> 0 to 200 (center = 100) */
/* Pitch: -90 to +90 degrees -> 0 to 200 (center = 100) */

int32_t scatter_x = (int32_t)((roll + 90.0f) * (200.0f / 180.0f));
int32_t scatter_y = (int32_t)((pitch + 90.0f) * (200.0f / 180.0f));

/* เมื่อบอร์ดวางราบ: scatter_x = 100, scatter_y = 100 (center) */
/* เอียงซ้าย: scatter_x < 100 */
/* เอียงขวา: scatter_x > 100 */
/* เอียงหน้า: scatter_y < 100 */
/* เอียงหลัง: scatter_y > 100 */

/* ===== ต้องตั้ง range ทั้ง X และ Y ===== */
lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_X, 0, 200);
lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, 0, 200);

/* ===== Point Count สำหรับ Scatter ===== */
/* 30 points = trail effect (เห็นทิศทางการเคลื่อนที่) */
/* เมื่อเกิน 30 จุด, จุดเก่าสุดจะหายไป */
lv_chart_set_point_count(chart, 30);
```

#### 5.4 Data Scaling Patterns

```c
/* ===== Pattern 1: Linear Mapping ===== */
/* Map value from range [in_min, in_max] to [out_min, out_max] */
/* Formula: out = (val - in_min) / (in_max - in_min) * (out_max - out_min) + out_min */

/* Accel (-5..+5) -> Bar (0..100) */
int32_t bar = (int32_t)((ax + 5.0f) * 10.0f);
/* When ax=-5: bar=0, ax=0: bar=50, ax=+5: bar=100 */

/* ===== Pattern 2: Scale Factor ===== */
/* Multiply by constant to convert float to int */
/* Gyro (rad/s, small values) -> Chart int */
int32_t gx_scaled = (int32_t)(gx * 100);
/* gx=0.12 -> 12, gx=-1.50 -> -150 */

/* ===== Pattern 3: Magnitude Calculation ===== */
/* Combine multiple axes into single value */
float tilt_mag = sqrtf(roll * roll + pitch * pitch);
/* Range: 0 (flat) to ~127 (max tilt) */
/* Clamp to 0-90 for chart */

/* ===== Pattern 4: Center Mapping ===== */
/* Map bipolar range to positive range with center */
/* Roll (-90..+90) -> Scatter (0..200, center=100) */
int32_t x = (int32_t)((roll + 90.0f) * (200.0f / 180.0f));

/* ===== Always Clamp! ===== */
if(val < min) val = min;
if(val > max) val = max;
```

#### 5.5 Bar Chart: set\_value\_by\_id vs set\_next\_value

```c
/* ===== set_next_value (สำหรับ Line/Area) ===== */
/* เพิ่มจุดใหม่ทางขวา, จุดเก่าเลื่อนซ้าย */
/* เหมาะกับ time-series ที่ข้อมูลเข้ามาเรื่อย ๆ */
lv_chart_set_next_value(chart, series, value);

/* ===== set_value_by_id (สำหรับ Bar) ===== */
/* อัพเดทค่าที่ตำแหน่ง (id) ที่กำหนด */
/* เหมาะกับ Bar chart ที่แต่ละแท่งแทน category */
lv_chart_set_value_by_id(chart, series, 0, value);
/* id=0 = แท่งแรก (และแท่งเดียว เพราะ point_count=1) */

/* ===== สำคัญ: lv_chart_refresh() ===== */
/* หลัง set_value_by_id ต้องเรียก refresh */
/* เพราะ set_value_by_id ไม่ trigger redraw อัตโนมัติ */
lv_chart_set_value_by_id(chart, ser_x, 0, bar_x);
lv_chart_set_value_by_id(chart, ser_y, 0, bar_y);
lv_chart_set_value_by_id(chart, ser_z, 0, bar_z);
lv_chart_refresh(chart);  /* MUST call after set_value_by_id */

/* set_next_value ไม่ต้อง refresh (ทำอัตโนมัติ) */
```

#### 5.6 Tilt Analysis: Complementary Filter

```c
/* ===== ทำไมไม่ใช้ Accelerometer อย่างเดียว? ===== */
/* Accel: วัดทิศทางแรงโน้มถ่วง -> คำนวณมุมได้ */
/* แต่: noisy มาก + ถ้ามี vibration จะผิดพลาด */

/* ===== ทำไมไม่ใช้ Gyroscope อย่างเดียว? ===== */
/* Gyro: วัดความเร็วเชิงมุม -> integrate ได้มุม */
/* แต่: drift ตลอดเวลา (มุมเพิ่มขึ้นเรื่อย ๆ) */

/* ===== Complementary Filter: Best of Both ===== */
/* angle = alpha * (angle + gyro*dt) + (1-alpha) * accel_angle */
/* alpha = 0.98 (trust gyro 98% for short-term) */
/* 1-alpha = 0.02 (trust accel 2% for long-term) */

/* ===== ใน Lab นี้: ===== */
/* 1. aic_tilt_init(NULL)           -> Initialize filter */
/* 2. aic_tilt_update_from_imu()    -> Fusion step (ทุก 100ms) */
/* 3. aic_tilt_get_roll()           -> Filtered roll angle */
/* 4. aic_tilt_get_pitch()          -> Filtered pitch angle */

/* ===== ใช้ใน Tab ไหน? ===== */
/* Tab 1 (Area): magnitude = sqrt(roll^2 + pitch^2) */
/* Tab 2 (Scatter): x = roll, y = pitch */
```

***

### 6. แบบฝึกหัด

#### Exercise 1: FFT Frequency Display Tab

**โจทย์:** เพิ่ม Tab ที่ 5 "FFT" แสดง Frequency Spectrum ของ accelerometer Z-axis:

**Requirements:**

* เก็บข้อมูล Z-axis 64 samples ล่าสุด (circular buffer)
* คำนวณ Simple DFT (Discrete Fourier Transform) ขนาด 64 จุด
* แสดงเป็น Bar chart: แกน X = frequency bin (0-32), แกน Y = magnitude
* อัพเดทผลลัพธ์ทุก 64 samples ใหม่ (ไม่ใช่ทุก sample)
* แสดง dominant frequency (ความถี่ที่มี magnitude สูงสุด)

**Layout:**

```bash
FFT Tab:
  "FFT: Z-axis Frequency Spectrum"
  ┌──────────────────────────────┐
  │  BAR CHART (32 bars)         │
  │  ████                        │
  │  ████ ██                     │
  │  ████ ██ ██ ██               │
  │  0Hz  5Hz 10Hz 15Hz          │
  └──────────────────────────────┘
  "Dominant: 3.5 Hz | Power: 42.3"
```

**การประยุกต์ใช้งานจริง:**

* Vibration Analysis: ตรวจจับ resonance frequency ของเครื่องจักร
* Predictive Maintenance: ตรวจสอบ bearing wear จาก vibration spectrum
* Structural Health Monitoring: ตรวจสอบ natural frequency ของสะพาน/อาคาร

**Hint:**

* DFT bin k: `mag[k] = sqrt(real[k]^2 + imag[k]^2)` where `real[k] = sum(x[n] * cos(2*PI*k*n/N))` and `imag[k] = sum(x[n] * sin(2*PI*k*n/N))`
* Frequency resolution: `f_res = sample_rate / N = 10Hz / 64 = 0.156 Hz/bin`
* ใช้ `lv_chart_set_point_count(chart, 32)` (แสดงเฉพาะครึ่งแรกของ DFT)
* `lv_chart_set_value_by_id()` สำหรับอัพเดทแต่ละ bar

***

#### Exercise 2: Recording and Playback

**โจทย์:** เพิ่มปุ่ม Record/Playback สำหรับบันทึกข้อมูล sensor แล้วเล่นซ้ำ:

**Requirements:**

* ปุ่ม "REC" สีแดง: เริ่มบันทึกข้อมูล (accel + gyro + tilt) สูงสุด 500 samples
* ปุ่ม "STOP" สีเทา: หยุดบันทึก
* ปุ่ม "PLAY" สีเขียว: เล่นข้อมูลที่บันทึกไว้กลับใน chart
* แสดง recording status: "Recording... 123/500" หรือ "Playing... 45/123"
* เมื่อ playback, timer อ่านจาก buffer แทน sensor จริง

**Layout:**

```bash
เพิ่มบริเวณล่างของแต่ละ tab:
  [REC] [STOP] [PLAY]  "Idle | 0 samples"
```

**การประยุกต์ใช้งานจริง:**

* Flight Data Recorder: บันทึกข้อมูลการบินสำหรับวิเคราะห์ภายหลัง
* Sports Analytics: บันทึก motion ของนักกีฬาแล้วเล่นซ้ำเพื่อวิเคราะห์ท่าทาง
* Quality Control: บันทึก vibration pattern สำหรับเปรียบเทียบกับ reference

**Hint:**

* ใช้ struct สำหรับ sample: `typedef struct { float ax, ay, az, gx, gy, gz, roll, pitch; } sensor_sample_t;`
* Static array: `static sensor_sample_t recording[500];`
* State machine: `IDLE -> RECORDING -> STOPPED -> PLAYING -> IDLE`
* Playback timer: ใช้ timer เดิม แต่เปลี่ยน data source

***

### 7. References

* [LVGL Chart](https://docs.lvgl.io/9.2/widgets/chart.html)
* [lv\_example\_chart\_5](https://github.com/lvgl/lvgl/blob/release/v9.2/examples/widgets/chart/lv_example_chart_5.c) (Area effect)
* [LVGL TabView](https://docs.lvgl.io/9.2/widgets/tabview.html)

***
