/* source/widgets/sgl_scope.c * * MIT License * * Copyright(c) 2023-present All contributors of SGL * Document reference link: https://sgl-docs.readthedocs.io * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include "sgl_scope.h" // Draw a dashed line using Bresenham's algorithm with dash pattern static void draw_dashed_line(sgl_surf_t *surf, sgl_area_t *area, int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t gap, sgl_color_t color) { int16_t dx = sgl_abs(x1 - x0); int16_t dy = sgl_abs(y1 - y0); int16_t sx = (x0 < x1) ? 1 : -1; int16_t sy = (y0 < y1) ? 1 : -1; int16_t err = dx - dy; int16_t e2; int16_t dash_len = 0; sgl_area_t clip_area = { .x1 = surf->x1, .y1 = surf->y1, .x2 = surf->x2, .y2 = surf->y2 }; sgl_area_selfclip(&clip_area, area); while (1) { // Draw dash segment if (dash_len < gap) { // Check if point is within clipping area if (x0 >= clip_area.x1 && x0 <= clip_area.x2 && y0 >= clip_area.y1 && y0 <= clip_area.y2) { sgl_color_t *buf = sgl_surf_get_buf(surf, x0 - surf->x1, y0 - surf->y1); *buf = color; } dash_len++; } else if (dash_len < 2 * gap) { // Skip drawing (gap segment) dash_len++; } else { // Reset dash counter dash_len = 0; } if (x0 == x1 && y0 == y1) break; e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } // Custom line drawing function supporting variable line width static void custom_draw_line(sgl_surf_t *surf, sgl_area_t *area, sgl_pos_t start, sgl_pos_t end, sgl_color_t color, int16_t width) { int16_t x0 = start.x; int16_t y0 = start.y; int16_t x1 = end.x; int16_t y1 = end.y; // Handle invalid line width (zero or negative) if (width <= 0) return; // For line width = 1, use standard Bresenham algorithm if (width == 1) { int16_t dx = sgl_abs(x1 - x0); int16_t dy = sgl_abs(y1 - y0); int16_t sx = (x0 < x1) ? 1 : -1; int16_t sy = (y0 < y1) ? 1 : -1; int16_t err = dx - dy; int16_t e2; sgl_area_t clip_area = { .x1 = surf->x1, .y1 = surf->y1, .x2 = surf->x2, .y2 = surf->y2 }; while (1) { // Check if point is within clipping area if (x0 >= clip_area.x1 && x0 <= clip_area.x2 && y0 >= clip_area.y1 && y0 <= clip_area.y2) { sgl_color_t *buf = sgl_surf_get_buf(surf, x0 - surf->x1, y0 - surf->y1); *buf = color; } if (x0 == x1 && y0 == y1) break; e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } return; } // For line width > 1, draw main line plus perpendicular offsets to simulate thickness int16_t dx = sgl_abs(x1 - x0); int16_t dy = sgl_abs(y1 - y0); int16_t sx = (x0 < x1) ? 1 : -1; int16_t sy = (y0 < y1) ? 1 : -1; int16_t err = dx - dy; int16_t e2; sgl_area_t clip_area = { .x1 = surf->x1, .y1 = surf->y1, .x2 = surf->x2, .y2 = surf->y2 }; sgl_area_selfclip(&clip_area, area); // Compute half-width for symmetric thickening int16_t half_width = width / 2; while (1) { // Draw current pixel and its perpendicular neighbors to form line thickness for (int16_t w = -half_width; w <= half_width; w++) { int16_t px, py; // Determine offset direction based on dominant axis if (dx > dy) { // Dominant X-axis direction px = x0; py = y0 + w; } else { // Dominant Y-axis direction px = x0 + w; py = y0; } // Check if point is within clipping area if (px >= clip_area.x1 && px <= clip_area.x2 && py >= clip_area.y1 && py <= clip_area.y2) { sgl_color_t *buf = sgl_surf_get_buf(surf, px - surf->x1, py - surf->y1); *buf = color; } } if (x0 == x1 && y0 == y1) break; e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } // Oscilloscope drawing callback function static void scope_construct_cb(sgl_surf_t *surf, sgl_obj_t* obj, sgl_event_t *evt) { sgl_scope_t *scope = (sgl_scope_t*)obj; if(evt->type == SGL_EVENT_DRAW_MAIN) { // Skip drawing if object is completely outside screen bounds if (obj->area.x2 < surf->x1 || obj->area.x1 > surf->x2 || obj->area.y2 < surf->y1 || obj->area.y1 > surf->y2) { return; // Object is fully off-screen; no need to draw } // Draw background sgl_draw_rect_t bg_rect = { .color = scope->bg_color, .alpha = scope->alpha, .radius = 0, .border = scope->border_width, }; sgl_draw_rect(surf, &obj->area, &obj->coords, &bg_rect); // Compute waveform display parameters int16_t display_min = scope->min_value; int16_t display_max = scope->max_value; int16_t actual_min = display_min; // Actual min/max of waveform data (for labels) int16_t actual_max = display_max; // Actual max of waveform data (for labels) // If auto-scaling is enabled, recalculate min/max from current buffer data if (scope->auto_scale) { // Recalculate running_min and running_max based on current data in buffer if (scope->display_count > 0) { display_min = scope->data_buffer[0]; display_max = scope->data_buffer[0]; // Iterate through all data points in buffer to find min/max uint32_t end_index = (scope->display_count < scope->data_len) ? scope->display_count : scope->data_len; for (uint32_t i = 0; i < end_index; i++) { int16_t val = scope->data_buffer[i]; if (val < display_min) display_min = val; if (val > display_max) display_max = val; } // Save actual data min/max for label display actual_min = display_min; actual_max = display_max; // Update running values for next frame scope->running_min = display_min; scope->running_max = display_max; } // Add margin to prevent waveform from touching borders int32_t margin = (int32_t)(display_max - display_min) / 10; if (margin == 0) margin = 1; display_min = (display_min > INT16_MIN + margin) ? display_min - margin : INT16_MIN; display_max = (display_max < INT16_MAX - margin) ? display_max + margin : INT16_MAX; } // Avoid division by zero if min equals max if (display_min == display_max) { if (display_max < INT16_MAX) { display_max++; } else { display_min--; } } // Draw grid lines int16_t width = obj->coords.x2 - obj->coords.x1; int16_t height = obj->coords.y2 - obj->coords.y1; int16_t x_center = (obj->coords.x1 + obj->coords.x2) / 2; int16_t y_center = obj->coords.y1 + (int32_t)(height * (display_max - (display_min + display_max) / 2)) / (display_max - display_min); // Draw horizontal center line (midpoint of display range) if (scope->grid_style) { // Draw dashed line draw_dashed_line(surf, &obj->area, obj->coords.x1, y_center, obj->coords.x2, y_center, scope->grid_style, scope->grid_color); } else { // Draw solid line sgl_draw_fill_hline(surf, &obj->area, y_center, obj->coords.x1, obj->coords.x2, 1, scope->grid_color, scope->alpha); } // Draw vertical center line if (scope->grid_style) { // Draw dashed line draw_dashed_line(surf, &obj->area, x_center, obj->coords.y1, x_center, obj->coords.y2, scope->grid_style, scope->grid_color); } else { // Draw solid line sgl_draw_fill_vline(surf, &obj->area, x_center, obj->coords.y1, obj->coords.y2, 1, scope->grid_color, scope->alpha); } // Draw vertical grid lines (10 divisions) for (int i = 1; i < 10; i++) { int16_t x_pos = obj->coords.x1 + (width * i / 10); if (scope->grid_style) { // Draw dashed line draw_dashed_line(surf, &obj->area, x_pos, obj->coords.y1, x_pos, obj->coords.y2, scope->grid_style, scope->grid_color); } else { // Draw solid line sgl_draw_fill_vline(surf, &obj->area, x_pos, obj->coords.y1, obj->coords.y2, 1, scope->grid_color, scope->alpha); } } // Draw horizontal grid lines (10 divisions) for (int i = 1; i < 10; i++) { int16_t y_pos = obj->coords.y1 + (height * i / 10); if (scope->grid_style) { // Draw dashed line draw_dashed_line(surf, &obj->area, obj->coords.x1, y_pos, obj->coords.x2, y_pos, scope->grid_style, scope->grid_color); } else { // Draw solid line sgl_draw_fill_hline(surf, &obj->area, y_pos, obj->coords.x1, obj->coords.x2, 1, scope->grid_color, scope->alpha); } } // Draw waveform data if (scope->display_count > 1) { sgl_pos_t start, end; // Determine number of points to display uint32_t display_points = scope->max_display_points > 0 ? scope->max_display_points : scope->data_len; if (display_points > scope->data_len) display_points = scope->data_len; // Number of actual data points to render uint32_t data_points = scope->display_count < display_points ? scope->display_count : display_points; // Compute index of the most recent data point (rightmost on screen) uint32_t last_index = (scope->current_index == 0) ? scope->data_len - 1 : scope->current_index - 1; int16_t last_value = scope->data_buffer[last_index]; // Clamp value to display range if (last_value < display_min) last_value = display_min; if (last_value > display_max) last_value = display_max; start.x = obj->coords.x2; // Rightmost X position start.y = obj->coords.y2 - ((int32_t)(last_value - display_min) * height) / (display_max - display_min); // Draw waveform from right to left for (uint32_t i = 1; i < data_points; i++) { //int index = (scope->current_index >= i) ? scope->current_index - i : scope->data_len - (i - scope->current_index); uint32_t prev_index = (scope->current_index >= i + 1) ? scope->current_index - (i + 1) : scope->data_len - (i + 1 - scope->current_index); int16_t current_value = scope->data_buffer[prev_index]; // Clamp value to display range current_value = sgl_clamp(current_value, display_min, display_max); end.x = obj->coords.x2 - (i * width / (data_points - 1)); // Move leftward end.y = obj->coords.y2 - ((int32_t)(current_value - display_min) * height) / (display_max - display_min); custom_draw_line(surf, &obj->area, start, end, scope->waveform_color, scope->line_width); start = end; } } // Draw Y-axis labels if enabled and font is set if (scope->show_y_labels && scope->y_label_font) { char label_text[16]; sgl_area_t text_area = { .x1 = obj->coords.x1 + 2, .y1 = obj->coords.y1, .x2 = obj->coords.x1 + 50, .y2 = obj->coords.y2 }; sgl_area_selfclip(&text_area, &obj->area); // Display actual maximum value of waveform data sprintf(label_text, "%d", actual_max); sgl_draw_string(surf, &text_area, obj->coords.x1 + 2, obj->coords.y1 + 2, label_text, scope->y_label_color, scope->alpha, scope->y_label_font); // Display actual minimum value of waveform data sprintf(label_text, "%d", actual_min); sgl_draw_string(surf, &text_area, obj->coords.x1 + 2, obj->coords.y2 - scope->y_label_font->font_height - 2, label_text, scope->y_label_color, scope->alpha, scope->y_label_font); // Display mid-range value of actual waveform int16_t mid_value = (actual_max + actual_min) / 2; sprintf(label_text, "%d", mid_value); sprintf(label_text, "%u", mid_value); sgl_draw_string(surf, &text_area, obj->coords.x1 + 2, y_center - scope->y_label_font->font_height/2, label_text, scope->y_label_color, scope->alpha, scope->y_label_font); } } } // Create an oscilloscope object sgl_obj_t* sgl_scope_create(sgl_obj_t* parent) { sgl_scope_t *scope = sgl_malloc(sizeof(sgl_scope_t)); if(scope == NULL) { return NULL; } memset(scope, 0, sizeof(sgl_scope_t)); sgl_obj_t *obj = &scope->obj; sgl_obj_init(obj, parent); obj->construct_fn = scope_construct_cb; sgl_obj_set_border_width(obj, SGL_THEME_BORDER_WIDTH); // Initialize default parameters scope->waveform_color = sgl_rgb(0, 255, 0); // Green waveform scope->bg_color = sgl_rgb(0, 0, 0); // Black background scope->grid_color = sgl_rgb(50, 50, 50); // Gray grid lines scope->border_width = 0; // border width is 0 scope->border_color = sgl_rgb(150, 150, 150); // Light gray outer border scope->min_value = 0; scope->max_value = 0xFFFF; scope->running_min = INT16_MAX; // Initialize runtime minimum to max int16_t value scope->running_max = INT16_MIN; // Initialize runtime maximum to min int16_t value scope->auto_scale = 1; // Enable auto-scaling by default scope->line_width = 2; // Default line thickness scope->max_display_points = 0; // Display all points by default scope->show_y_labels = 0; // Hide Y-axis labels by default scope->alpha = SGL_ALPHA_MAX; // Fully opaque by default scope->grid_style = 0; // Solid grid lines by default scope->y_label_font = NULL; // No font by default scope->y_label_color = sgl_rgb(255, 255, 255); // White label color scope->display_count = 0; // No data initially return obj; } /** * @brief Append a new data point to the oscilloscope * @param obj The oscilloscope object * @param value The new data point * @note This function appends a new data point to the oscilloscope. * If the oscilloscope is configured to auto-scale, the function updates the running minimum and maximum values. * The function also updates the display count and marks the oscilloscope object as dirty. */ void sgl_scope_append_data(sgl_obj_t* obj, int16_t value) { sgl_scope_t *scope = (sgl_scope_t*)obj; // Simply append the data point to the buffer // Min/max will be recalculated during drawing if auto-scale is enabled scope->data_buffer[scope->current_index] = value; if (sgl_is_pow2(scope->data_len)) { scope->current_index = (scope->current_index + 1) & (scope->data_len - 1); } else { scope->current_index = (scope->current_index + 1) % scope->data_len; } // update display count if (scope->display_count < scope->data_len) { scope->display_count++; } sgl_obj_set_dirty(obj); }