443 lines
17 KiB
C
443 lines
17 KiB
C
/* 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 <stdio.h>
|
|
#include <limits.h>
|
|
#include <sgl_theme.h>
|
|
#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);
|
|
}
|