Waveform Widget¶
The Waveform widget is a real-time strip-chart / scope plot — one or
more scrolling or sweeping traces over a live or historical value buffer,
the bedside-monitor and signal-generator display. Init installs the whole
look (theme-tracked background/grid/axis, a settable size) with the SCROLL
display mode; add_channel() attaches a caller-owned sample-value ring
(zero heap — the widget never copies or allocates), while
add_channel_live() attaches a SMALL ring meant to be drained every frame
rather than sized to the full plot width, trading history for RAM.
push()/the emit helpers write new samples; set_range()/set_grid()
pick the value window and division count, set_channel_range()/
set_channel_style() override those per channel for multi-parameter plots,
and set_mode() switches between SCROLL (the whole trace slides left) and
SWEEP (a monitor-style write cursor with an erase gap, cheaper to draw, no
scrollable history). set_freeze() holds the display while the buffer
keeps recording; while frozen, ugui_waveform_on_event() accepts
KEY_UP/DOWN to zoom the Y-range and KEY_LEFT/RIGHT to pan back through
recorded history (SCROLL mode only). add_label() draws a text overlay
pinned inside the plot (a lead name, a LIVE/FROZEN readout, a time marker)
without needing a separate widget.
Every example below is complete — the file-scope storage, the callbacks and
the build calls — beside the exact frame it renders on a 480×320 panel. Each
compiles against nothing but the released library: drop it into the
Application Skeleton and it runs as-is. The icons come from
inc/icons/, the lexend fonts from inc/fonts/ — both ship with the library.
Examples¶
Default: Low-RAM Live ECG (Sweep)¶
The simplest live monitor: init() + add_channel_live() with a SMALL input ring, then push. The default SWEEP mode draws a bedside-monitor cursor across the full plot width; the ring only has to hold the samples that arrive between two draws (~128 bytes here) instead of a full-width column buffer (~640 bytes) — what you push is what you see, at the cost of not surviving a full repaint.

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10
};
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
#define ECG_RING 64u
static ugui_widget_t s_plot;
static ugui_waveform_ext_t s_plot_ext;
static int16_t s_buf_ecg[ECG_RING];
#define BTN_H 34
#define BTN_GAP 8
#define BTN_Y (UGUI_SCREEN_H - BTN_H - BTN_GAP)
#define PLOT_X 10
#define PLOT_Y 38
#define PLOT_W (UGUI_SCREEN_W - 2 * 10)
#define PLOT_H (BTN_Y - PLOT_Y - BTN_GAP)
#endif
/* --- build it (in your screen's init) --- */
ugui_widget_waveform_init(&s_root, &s_plot, &s_plot_ext);
ugui_widget_set_area(&s_plot, PLOT_X, PLOT_Y, PLOT_W, PLOT_H);
ugui_widget_set_id (&s_plot, WDGT_ID_DEMO);
(void)ugui_widget_waveform_add_channel_live(&s_plot, ugui_color_hex(0x00E07Au),
s_buf_ecg, ECG_RING);
ugui_screen_add_widget(&s_screen, &s_plot);
Y-Range, Grid & Built-In Zoom¶
set_range() picks the value window mapped top..bottom and set_grid() sets the division count (10×6 here). The Zoom +/− buttons drive the widget's OWN zoom handler by feeding it KEY_UP/KEY_DOWN through ugui_waveform_on_event() — exactly what a keypad would do — so the visible Y-range shrinks or grows around the trace without touching the data.

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10
};
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_plot;
static ugui_waveform_ext_t s_plot_ext;
static int16_t s_buf[UGUI_SCREEN_W];
#define BTN_H 34
#define BTN_GAP 8
#define BTN_Y (UGUI_SCREEN_H - BTN_H - BTN_GAP)
#define PLOT_X 10
#define PLOT_Y 38
#define PLOT_W (UGUI_SCREEN_W - 2 * 10)
#define PLOT_H (BTN_Y - PLOT_Y - BTN_GAP)
#endif
/* --- build it (in your screen's init) --- */
ugui_widget_waveform_init(&s_root, &s_plot, &s_plot_ext);
ugui_widget_set_area(&s_plot, PLOT_X, PLOT_Y, PLOT_W, PLOT_H);
ugui_widget_set_id (&s_plot, WDGT_ID_DEMO);
ugui_widget_waveform_set_range(&s_plot, -120, 120); /* value window */
ugui_widget_waveform_set_grid (&s_plot, 10u, 6u); /* 10 x 6 divisions */
(void)ugui_widget_waveform_add_channel(&s_plot, ugui_color_hex(0x00C8FFu),
s_buf, (uint16_t)UGUI_SCREEN_W);
ugui_widget_set_event_cb(&s_plot, NULL,
UGUI_EMASK_KEY_CONFIRM | UGUI_EMASK_KEY_UP | UGUI_EMASK_KEY_DOWN);
ugui_screen_add_widget(&s_screen, &s_plot);
Three Channels, One Plot¶
Three live traces (ECG, SpO2, respiration) share one plot, phase-locked to a single simulated acquisition clock. SpO2 gets its own set_channel_range() (0..120, not ±100) and a thicker anti-aliased set_channel_style(); the button shows/hides the respiration trace via set_channel_active() without touching the other two.

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10
};
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_plot;
static ugui_waveform_ext_t s_plot_ext;
static int16_t s_buf_ecg[UGUI_SCREEN_W];
static int16_t s_buf_spo2[UGUI_SCREEN_W];
static int16_t s_buf_resp[UGUI_SCREEN_W];
#define CH_SPO2 1u
#define BTN_H 32
#define BTN_GAP 8
#define BTN_Y (UGUI_SCREEN_H - BTN_H - BTN_GAP)
#define PLOT_X 10
#define PLOT_Y 38
#define PLOT_W (UGUI_SCREEN_W - 2 * 10)
#define PLOT_H (BTN_Y - PLOT_Y - BTN_GAP)
#endif
/* --- build it (in your screen's init) --- */
ugui_widget_waveform_init(&s_root, &s_plot, &s_plot_ext);
ugui_widget_set_area(&s_plot, PLOT_X, PLOT_Y, PLOT_W, PLOT_H);
ugui_widget_set_id (&s_plot, WDGT_ID_DEMO);
ugui_widget_waveform_set_range (&s_plot, -100, 100);
ugui_widget_waveform_set_grid (&s_plot, 8u, 4u);
ugui_widget_waveform_set_colors(&s_plot, ugui_color_hex(0x0A0A14u),
ugui_color_hex(0x1E2630u), ugui_color_hex(0x3C4450u));
(void)ugui_widget_waveform_add_channel(&s_plot, ugui_color_hex(0x00E07Au),
s_buf_ecg, (uint16_t)UGUI_SCREEN_W); /* ch0 ECG */
(void)ugui_widget_waveform_add_channel(&s_plot, ugui_color_hex(0x00C8FFu),
s_buf_spo2, (uint16_t)UGUI_SCREEN_W); /* ch1 SpO2 */
(void)ugui_widget_waveform_add_channel(&s_plot, ugui_color_hex(0xFFA640u),
s_buf_resp, (uint16_t)UGUI_SCREEN_W); /* ch2 resp */
/* SpO2 is a 0..120 positive signal — give it its own scale. */
ugui_widget_waveform_set_channel_range(&s_plot, CH_SPO2, 0, 120);
/* Make the pleth a thicker anti-aliased trace so it reads as the "premium"
* channel; ECG + resp stay on the fast 1 px Bresenham path. */
ugui_widget_waveform_set_channel_style(&s_plot, CH_SPO2, ugui_color_hex(0x00C8FFu), 2u);
ugui_widget_set_event_cb(&s_plot, NULL,
UGUI_EMASK_KEY_CONFIRM | UGUI_EMASK_KEY_UP | UGUI_EMASK_KEY_DOWN);
ugui_screen_add_widget(&s_screen, &s_plot);
SCROLL vs SWEEP Display Modes¶
The same live ECG feed as the default screen, but the button flips set_mode() between SCROLL (strip-chart: the whole trace slides left, newest at the right edge) and SWEEP (bedside-monitor style: the trace stays put while a write cursor advances with a short erase gap ahead of it) — SWEEP is cheaper to draw since no history scrolls.

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10
};
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_plot;
static ugui_waveform_ext_t s_plot_ext;
static int16_t s_buf[UGUI_SCREEN_W];
#define BTN_H 34
#define BTN_GAP 8
#define BTN_Y (UGUI_SCREEN_H - BTN_H - BTN_GAP)
#define PLOT_X 10
#define PLOT_Y 38
#define PLOT_W (UGUI_SCREEN_W - 2 * 10)
#define PLOT_H (BTN_Y - PLOT_Y - BTN_GAP)
#endif
/* --- build it (in your screen's init) --- */
ugui_widget_waveform_init(&s_root, &s_plot, &s_plot_ext);
ugui_widget_set_area(&s_plot, PLOT_X, PLOT_Y, PLOT_W, PLOT_H);
ugui_widget_set_id (&s_plot, WDGT_ID_DEMO);
(void)ugui_widget_waveform_add_channel(&s_plot, ugui_color_hex(0x00E07Au),
s_buf, (uint16_t)UGUI_SCREEN_W);
ugui_widget_set_event_cb(&s_plot, NULL,
UGUI_EMASK_KEY_CONFIRM | UGUI_EMASK_KEY_UP | UGUI_EMASK_KEY_DOWN);
ugui_screen_add_widget(&s_screen, &s_plot);
Freeze, Pan & In-Plot Labels¶
add_label() draws two labels over the trace: a static lead name ("II") and a live LIVE/FROZEN readout the screen rewrites in place. Freeze holds the display while the buffer keeps recording; while frozen, the pan buttons feed the widget KEY_LEFT/KEY_RIGHT to scroll back and forth through the captured history (a SCROLL-mode feature — the buffer here is sized to several screens of scroll-back).

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10
};
/* --- fonts: each is DEFINED in inc/fonts/<name>.h — #include that
* header in exactly one .c of your app; elsewhere just: --- */
extern const ugui_font_t lexend_14pt_2bpp;
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_plot;
static ugui_waveform_ext_t s_plot_ext;
#define PAN_SCREENS 3
static int16_t s_buf[PAN_SCREENS * UGUI_SCREEN_W];
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_text_cfg_t s_lbl_lead;
static ugui_text_cfg_t s_lbl_state;
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
#define BTN_H 34
#define BTN_GAP 8
#define BTN_Y (UGUI_SCREEN_H - BTN_H - BTN_GAP)
#define PLOT_X 10
#define PLOT_Y 38
#define PLOT_W (UGUI_SCREEN_W - 2 * 10)
#define PLOT_H (BTN_Y - PLOT_Y - BTN_GAP)
#endif
/* --- local helpers & event callbacks --- */
static void add_plot_label(ugui_text_cfg_t* cfg, const char* str, ugui_color_t color,
int16_t x, int16_t y, int16_t w, int16_t h, ugui_align_t align)
{
ugui_text_cfg_init(cfg, str, &lexend_14pt_2bpp, color, align,
(ugui_point_t){ 0, 0 }, 0u);
cfg->area = (ugui_rect_t){ x, y, w, h };
(void)ugui_widget_waveform_add_label(&s_plot, cfg);
}
/* --- build it (in your screen's init) --- */
ugui_widget_waveform_init(&s_root, &s_plot, &s_plot_ext);
ugui_widget_set_area(&s_plot, PLOT_X, PLOT_Y, PLOT_W, PLOT_H);
ugui_widget_set_id (&s_plot, WDGT_ID_DEMO);
(void)ugui_widget_waveform_add_channel(&s_plot, ugui_color_hex(0x00E07Au),
s_buf, (uint16_t)(PAN_SCREENS * UGUI_SCREEN_W));
/* Pan/history is a SCROLL feature (a sweep cursor keeps no scrollable
* history); the widget defaults to SWEEP, so switch to SCROLL here. */
ugui_widget_waveform_set_mode(&s_plot, UGUI_WAVEFORM_MODE_SCROLL, 0u);
#ifdef UGUI_ENABLE_FONT
add_plot_label(&s_lbl_lead, "II", ugui_color_hex(0x00E07Au),
6, 4, 40, 16, (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER));
add_plot_label(&s_lbl_state, "LIVE", ugui_color_hex(0x00E07Au),
PLOT_W - 76, 4, 70, 16, (ugui_align_t)(UGUI_ALIGN_RIGHT | UGUI_ALIGN_V_CENTER));
#endif
ugui_widget_set_event_cb(&s_plot, NULL,
UGUI_EMASK_KEY_CONFIRM | UGUI_EMASK_KEY_UP | UGUI_EMASK_KEY_DOWN
| UGUI_EMASK_KEY_LEFT | UGUI_EMASK_KEY_RIGHT | UGUI_EMASK_SLIDE);
ugui_screen_add_widget(&s_screen, &s_plot);
Four-Lane Patient Monitor¶
A real bedside monitor gives each parameter its own lane: FOUR independent waveform widgets (not channels in one plot) stacked and sized proportionally to the panel height, all fed from one simulated acquisition clock so the two ECG leads stay beat-aligned. Each lane runs in SWEEP mode with its own colour, scale and in-widget lead label; Freeze holds every lane at once.

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10, WDGT_ID_SLOT0, WDGT_ID_SLOT1,
WDGT_ID_SLOT2
};
/* --- fonts: each is DEFINED in inc/fonts/<name>.h — #include that
* header in exactly one .c of your app; elsewhere just: --- */
extern const ugui_font_t lexend_14pt_2bpp;
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_ecg1; static ugui_waveform_ext_t s_ecg1_ext; static int16_t s_buf_ecg1[UGUI_SCREEN_W];
static ugui_widget_t s_ecg2; static ugui_waveform_ext_t s_ecg2_ext; static int16_t s_buf_ecg2[UGUI_SCREEN_W];
static ugui_widget_t s_resp; static ugui_waveform_ext_t s_resp_ext; static int16_t s_buf_resp[UGUI_SCREEN_W];
static ugui_widget_t s_spo2; static ugui_waveform_ext_t s_spo2_ext; static int16_t s_buf_spo2[UGUI_SCREEN_W];
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_text_cfg_t s_lbl_ecg1;
static ugui_text_cfg_t s_lbl_ecg2;
static ugui_text_cfg_t s_lbl_resp;
static ugui_text_cfg_t s_lbl_spo2;
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
#define LANE_MARGIN 6
#define LANE_X LANE_MARGIN
#define LANE_W (UGUI_SCREEN_W - 2 * LANE_MARGIN)
#define ROW_H 30
#define ROW_GAP 6
#define ROW_Y (UGUI_SCREEN_H - ROW_H - ROW_GAP)
#define LANE_GAP 4
#endif
/* --- local helpers & event callbacks --- */
static void make_lane(ugui_widget_t* w, ugui_waveform_ext_t* ext, int16_t* buf,
ugui_text_cfg_t* lbl, uint8_t id, int16_t y, int16_t h,
ugui_color_t color, int16_t ymin, int16_t ymax, const char* name)
{
ugui_widget_waveform_init(&s_root, w, ext);
ugui_widget_set_area(w, LANE_X, y, LANE_W, h);
ugui_widget_set_id (w, id);
ugui_widget_waveform_set_range (w, ymin, ymax);
ugui_widget_waveform_set_grid (w, 0u, 0u); /* clean lane: trace only */
ugui_widget_waveform_set_colors(w, ugui_color_hex(0x05060Cu),
ugui_color_hex(0x141A22u), ugui_color_hex(0x222C36u));
(void)ugui_widget_waveform_add_channel(w, color, buf, (uint16_t)UGUI_SCREEN_W);
ugui_widget_waveform_set_mode(w, UGUI_WAVEFORM_MODE_SWEEP, 6u);
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(lbl, name, &lexend_14pt_2bpp, color,
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
(ugui_point_t){ 0, 0 }, 0u);
lbl->area = (ugui_rect_t){ 4, 1, 64, 14 };
(void)ugui_widget_waveform_add_label(w, lbl);
#else
(void)lbl; (void)name;
#endif
ugui_widget_set_event_cb(w, NULL, UGUI_EMASK_KEY_CONFIRM);
ugui_screen_add_widget(&s_screen, w);
}
/* --- build it (in your screen's init) --- */
{
const int16_t top = (int16_t)38;
const int16_t bot = (int16_t)(ROW_Y - LANE_GAP); /* lanes bottom */
const int16_t avail = (int16_t)(bot - top - 3 * LANE_GAP);
const int16_t unit = (int16_t)(avail / 10);
const int16_t ecg_h = (int16_t)(unit * 3);
const int16_t sml_h = (int16_t)(unit * 2);
int16_t y = top;
make_lane(&s_ecg1, &s_ecg1_ext, s_buf_ecg1, &s_lbl_ecg1, WDGT_ID_DEMO,
y, ecg_h, ugui_color_hex(0x00E07Au), -100, 100, "ECG II");
y = (int16_t)(y + ecg_h + LANE_GAP);
make_lane(&s_ecg2, &s_ecg2_ext, s_buf_ecg2, &s_lbl_ecg2, WDGT_ID_SLOT0,
y, ecg_h, ugui_color_hex(0x40E0C0u), -100, 100, "ECG V");
y = (int16_t)(y + ecg_h + LANE_GAP);
make_lane(&s_resp, &s_resp_ext, s_buf_resp, &s_lbl_resp, WDGT_ID_SLOT1,
y, sml_h, ugui_color_hex(0xFFD200u), -100, 100, "RESP");
y = (int16_t)(y + sml_h + LANE_GAP);
make_lane(&s_spo2, &s_spo2_ext, s_buf_spo2, &s_lbl_spo2, WDGT_ID_SLOT2,
y, (int16_t)(bot - y), ugui_color_hex(0x00C8FFu), 0, 120, "SpO2");
}
Medical History Trends (Dual Y-Axis)¶
Two slow-trending parameters share one plot on independent scales: set_channel_range() maps blood pressure (80..160 mmHg) and SpO2 (70..100%) each to the full plot height — one sample is pushed per second, so this shot fast-forwards a few minutes to show the trend actually forming. The Y-axis scale numbers are plain box widgets in side strips OUTSIDE the waveform widget; the time markers are in-widget labels via add_label(). A Freeze button (not pictured) holds the trend while the buffer keeps recording.

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10
};
/* --- fonts: each is DEFINED in inc/fonts/<name>.h — #include that
* header in exactly one .c of your app; elsewhere just: --- */
extern const ugui_font_t lexend_14pt_2bpp;
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_plot;
static ugui_waveform_ext_t s_plot_ext;
#define BUF_SAMPLES 512u
static int16_t s_buf_bp [BUF_SAMPLES];
static int16_t s_buf_spo2[BUF_SAMPLES];
#define CH_BP 0u
#define CH_SPO2 1u
#define BP_MIN 80
#define BP_MAX 160
#define SPO2_MIN 70
#define SPO2_MAX 100
#define BP_TICK_N 5u
static ugui_widget_t s_bp_tick[BP_TICK_N];
static ugui_box_ext_t s_bp_tick_ext[BP_TICK_N];
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_text_cfg_t s_bp_tick_txt[BP_TICK_N];
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static const int16_t k_bp_val [BP_TICK_N] = { 160, 140, 120, 100, 80 };
static const char* k_bp_str [BP_TICK_N] = {"160","140","120","100"," 80"};
#define SPO2_TICK_N 4u
static ugui_widget_t s_spo2_tick[SPO2_TICK_N];
static ugui_box_ext_t s_spo2_tick_ext[SPO2_TICK_N];
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_text_cfg_t s_spo2_tick_txt[SPO2_TICK_N];
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static const int16_t k_spo2_val [SPO2_TICK_N] = { 100, 90, 80, 70 };
static const char* k_spo2_str [SPO2_TICK_N] = {"100"," 90"," 80"," 70"};
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
static ugui_text_cfg_t s_lbl_bp; /* "BP mmHg" — top-left of plot */
static ugui_text_cfg_t s_lbl_spo2; /* "SpO2 %" — below BP label */
static ugui_text_cfg_t s_lbl_t3m; /* time marker at midpoint (≈ 3 min ago)*/
static ugui_text_cfg_t s_lbl_t1m; /* time marker at 3/4 point (≈ 1 min ago)*/
#endif
#if defined(UGUI_WIDGET_ENABLE_WAVEFORM)
#define MED_COL_BG ugui_color_hex(0x05060Cu) /* fixed dark medical bg */
#define MED_COL_GRID ugui_color_hex(0x0F1A24u)
#define MED_COL_AXIS ugui_color_hex(0x222C36u)
#define SCALE_W 30 /* px: width of left / right scale strip */
#define SCALE_LBL_H 14 /* px: height of one tick label box */
#define ROW_H 28 /* px: bottom control row */
#define ROW_GAP 6 /* px: gap above control row */
#define CARD_PAD 4 /* px: gap between header and plot */
#define PLOT_X ((int16_t)(10 + SCALE_W + 2))
#define PLOT_Y ((int16_t)(38 + CARD_PAD))
#define PLOT_W ((int16_t)(UGUI_SCREEN_W - 2 * 10 - 2 * (SCALE_W + 2)))
#define PLOT_H ((int16_t)(UGUI_SCREEN_H - PLOT_Y - ROW_H - ROW_GAP - CARD_PAD))
#define RIGHT_SCALE_X ((int16_t)(PLOT_X + PLOT_W + 2))
#endif
/* --- local helpers & event callbacks --- */
static int16_t tick_y(int16_t val, int16_t vmin, int16_t vmax)
{
return (int16_t)(PLOT_Y
+ (int32_t)PLOT_H * (vmax - val) / (vmax - vmin)
- SCALE_LBL_H / 2);
}
static void make_scale_tick(ugui_screen_t* owner, ugui_widget_t* w,
ugui_box_ext_t* ext, ugui_text_cfg_t* text_cfg,
int16_t x, int16_t y, ugui_align_t align,
ugui_color_t color, const char* str)
{
ugui_widget_box_init(&s_root, w, ext);
ugui_widget_set_area(w, x, y, (int16_t)SCALE_W, (int16_t)SCALE_LBL_H);
ugui_widget_box_set_style(w, true, 0u, 0u, MED_COL_BG, MED_COL_BG);
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(text_cfg, str, &lexend_14pt_2bpp, color,
align, (ugui_point_t){ 0, 0 }, 1u);
ugui_widget_box_set_text(w, text_cfg);
#else
(void)text_cfg; (void)align; (void)color; (void)str;
#endif
w->state.enabled = 0u; /* non-interactive; don't consume touch */
ugui_screen_add_widget(owner, w);
}
/* --- build it (in your screen's init) --- */
ugui_widget_waveform_init(&s_root, &s_plot, &s_plot_ext);
ugui_widget_set_area(&s_plot, PLOT_X, PLOT_Y, PLOT_W, PLOT_H);
ugui_widget_set_id (&s_plot, WDGT_ID_DEMO);
ugui_widget_waveform_set_grid (&s_plot, 4u, 4u);
ugui_widget_waveform_set_colors(&s_plot, MED_COL_BG, MED_COL_GRID,
MED_COL_AXIS);
/* CH0 — Blood pressure (green), anti-aliased 2 px. */
(void)ugui_widget_waveform_add_channel(&s_plot,
ugui_color_hex(0x00E07Au),
s_buf_bp, BUF_SAMPLES);
ugui_widget_waveform_set_channel_range(&s_plot, CH_BP, BP_MIN, BP_MAX);
ugui_widget_waveform_set_channel_style(&s_plot, CH_BP,
ugui_color_hex(0x00E07Au), 2u);
/* CH1 — SpO2 (cyan), anti-aliased 2 px. */
(void)ugui_widget_waveform_add_channel(&s_plot,
ugui_color_hex(0x00C8FFu),
s_buf_spo2, BUF_SAMPLES);
ugui_widget_waveform_set_channel_range(&s_plot, CH_SPO2,
SPO2_MIN, SPO2_MAX);
ugui_widget_waveform_set_channel_style(&s_plot, CH_SPO2,
ugui_color_hex(0x00C8FFu), 2u);
#ifdef UGUI_ENABLE_FONT
/* Channel labels — drawn inside the plot, top-left corner. */
ugui_text_cfg_init(&s_lbl_bp, "BP mmHg", &lexend_14pt_2bpp,
ugui_color_hex(0x00E07Au),
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
(ugui_point_t){ 0, 0 }, 0u);
s_lbl_bp.area = (ugui_rect_t){ 4, 2, 80, 14 };
(void)ugui_widget_waveform_add_label(&s_plot, &s_lbl_bp);
ugui_text_cfg_init(&s_lbl_spo2, "SpO2 %", &lexend_14pt_2bpp,
ugui_color_hex(0x00C8FFu),
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
(ugui_point_t){ 0, 0 }, 0u);
s_lbl_spo2.area = (ugui_rect_t){ 4, 18, 80, 14 };
(void)ugui_widget_waveform_add_label(&s_plot, &s_lbl_spo2);
/* Time markers — pinned to the bottom of the plot.
* In SCROLL mode at 1 Hz the mid-point (x = PLOT_W/2) holds data
* approximately PLOT_W/2 seconds old; labels are relative approximations. */
ugui_text_cfg_init(&s_lbl_t3m, "T-3m", &lexend_14pt_2bpp,
ugui_color_hex(0x3C4450u),
(ugui_align_t)(UGUI_ALIGN_H_CENTER | UGUI_ALIGN_V_CENTER),
(ugui_point_t){ 0, 0 }, 0u);
s_lbl_t3m.area = (ugui_rect_t){
(int16_t)(PLOT_W / 2 - 20),
(int16_t)(PLOT_H - 16),
40, 14
};
(void)ugui_widget_waveform_add_label(&s_plot, &s_lbl_t3m);
ugui_text_cfg_init(&s_lbl_t1m, "T-1m", &lexend_14pt_2bpp,
ugui_color_hex(0x3C4450u),
(ugui_align_t)(UGUI_ALIGN_H_CENTER | UGUI_ALIGN_V_CENTER),
(ugui_point_t){ 0, 0 }, 0u);
s_lbl_t1m.area = (ugui_rect_t){
(int16_t)(PLOT_W * 3 / 4 - 20),
(int16_t)(PLOT_H - 16),
40, 14
};
(void)ugui_widget_waveform_add_label(&s_plot, &s_lbl_t1m);
#endif /* UGUI_ENABLE_FONT */
ugui_widget_set_event_cb(&s_plot, NULL,
UGUI_EMASK_KEY_CONFIRM |
UGUI_EMASK_KEY_UP | UGUI_EMASK_KEY_DOWN);
ugui_screen_add_widget(&s_screen, &s_plot);
/* ================================================================
* Left scale — BP ticks (right-aligned, flush to waveform left edge)
* ================================================================ */
for (uint8_t i = 0u; i < BP_TICK_N; i++) {
make_scale_tick(&s_screen,
&s_bp_tick[i], &s_bp_tick_ext[i],
#ifdef UGUI_ENABLE_FONT
&s_bp_tick_txt[i],
#else
NULL,
#endif
(int16_t)10,
tick_y(k_bp_val[i], BP_MIN, BP_MAX),
(ugui_align_t)(UGUI_ALIGN_RIGHT | UGUI_ALIGN_V_CENTER),
ugui_color_hex(0x00E07Au),
k_bp_str[i]);
}
/* ================================================================
* Right scale — SpO2 ticks (left-aligned, flush to waveform right edge)
* ================================================================ */
for (uint8_t i = 0u; i < SPO2_TICK_N; i++) {
make_scale_tick(&s_screen,
&s_spo2_tick[i], &s_spo2_tick_ext[i],
#ifdef UGUI_ENABLE_FONT
&s_spo2_tick_txt[i],
#else
NULL,
#endif
RIGHT_SCALE_X,
tick_y(k_spo2_val[i], SPO2_MIN, SPO2_MAX),
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
ugui_color_hex(0x00C8FFu),
k_spo2_str[i]);
}
From this widget to a working app
Everything above is the widget-specific code. The scaffolding it drops into — storage, HAL, main loop, display-mode bring-up — is the same for every app: copy it from the Application Skeleton.