commit 829c1490d193674ec3ebfd2ae385f33cbc3818ee Author: Eduardo Silva Date: Sun Oct 19 22:55:56 2025 +0100 first commit diff --git a/library.json b/library.json new file mode 100644 index 0000000..ee6ef3f --- /dev/null +++ b/library.json @@ -0,0 +1,24 @@ +{ + "name": "eslabsCurses", + "version": "0.1.1", + "description": "Simple UI for creating interfaces with my ESP32-8048S043", + "keywords": "ESP32-8048S043, ESP32, curses", + "repository": + { + "type": "git", + "url": "https://github.com/ESilva15/eslabsCurses" + }, + "authors": + [ + { + "name": "Eduardo Silva", + "maintainer": true + } + ], + "dependencies": { + "Adafruit_GFX_Library": "~1.11.3", + "Adafruit_BusIO": "1.16.1" + }, + "frameworks": "*", + "platforms": "*" +} diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..e69de29 diff --git a/src/UIBar.cpp b/src/UIBar.cpp new file mode 100644 index 0000000..a28fb58 --- /dev/null +++ b/src/UIBar.cpp @@ -0,0 +1,104 @@ +#include "UIBar.h" +#include "UIDrawing.h" + +UIBar::UIBar(Arduino_GFX *d, UIDimensions dims, UIDecorations *decor, + char *title) + : UIElement(d, dims, decor, title) {} + +void UIBar::Update(char *v) { + unsigned long time = millis(); + if ((time - this->lastUpdate) <= this->refreshRate) { + return; + } + + // Conver the string to an integer + uint32_t newVal = atoi(v); + + if (this->value == newVal) { + // If the value hasn't changed we do not need to re-render + return; + } + + this->DrawBar(&this->value, &newVal); + this->value = newVal; +} + +void UIBar::DrawBar(uint32_t *oldValue, uint32_t *newValue) { + // We can help identifying cur RPM visually by using different + // colors on the bar, for example, at a given distance of the ideal shift + // we can change the color gradually + + // If the new value is larger than the old one + // we just paint a new rectangle + // If the new value is smaller than the old one + // we have to delete a chunk of the old rectangle + int16_t startX = 0; + int16_t len = 0; + uint16_t colour = WHITE; + + if (*newValue > *oldValue) { // Increment the bar + int32_t newV = + (*newValue * (uint32_t)(this->dims.width - 4)) / (this->range * 1000); + int32_t oldV = + (this->value * (uint32_t)(this->dims.width - 4)) / (this->range * 1000); + + startX = oldV; + len = newV - oldV; + + } else { // Decrement the bar + int32_t newV = + (*newValue * (uint32_t)(this->dims.width - 4)) / (this->range * 1000); + int32_t oldV = + (this->value * (uint32_t)(this->dims.width - 4)) / (this->range * 1000); + + startX = newV; + len = oldV - newV; + colour = BLACK; + } + this->display->fillRect(this->dims.x + 2 + startX, this->dims.y + 2, len, + this->dims.height - 4, colour); + + this->value = *newValue; +} + +void UIBar::renderBlank() { + this->display->setCursor(this->dims.x, this->dims.y); + this->display->print("!RANGE"); +} + +void UIBar::Box() { + // If the range was not set, we cannot draw our ruler + if (this->range <= 0) { + this->renderBlank(); + return; + } + + // Bounding box for the bar + this->display->drawRect(this->dims.x, this->dims.y, this->dims.width, + this->dims.height, RED); + + // Now we can draw the scale (this should be easy to modify if needed) + // dividirs will be the range of the tach - 8 = 8000rpm + + // Calculate step size as a float for accurate positioning + float step = (float)this->dims.width / this->range; + for (int k = 0; k <= this->range; k++) { + uint16_t x = round(k * step) + this->dims.x; + + // To ensure it doesn't go beyond the bounding box + if (x >= this->dims.x + this->dims.width) { + x = this->dims.x + this->dims.width - 1; + } + + this->display->setTextColor(RED); + this->display->drawFastVLine(x, this->dims.y + this->dims.height, 7, RED); + + char legend[5]; + sprintf(legend, "%d", k); + this->display->setCursor(x - (CHR_WIDTH(this->decor->textSize) / 2), + this->dims.y + this->dims.height + 7 + 2); + this->display->setTextColor(WHITE); + this->display->setTextSize(this->decor->textSize); + this->display->print(legend); + } +} diff --git a/src/UIBar.h b/src/UIBar.h new file mode 100644 index 0000000..cc17014 --- /dev/null +++ b/src/UIBar.h @@ -0,0 +1,20 @@ +#ifndef _UI_BAR +#define _UI_BAR + +#include "UIComponent.h" + +struct UIBar : UIElement { + // This might be the only when where its more efficient to have the data + // as integers instead + uint32_t value = 0; // current tach value + uint8_t range = 0; // define the tach range, ie: 8 for 8000rpm + + UIBar(Arduino_GFX *d, UIDimensions dims, UIDecorations *decor, char *title); + + void Update(char *val); + void DrawBar(uint32_t *oldValue, uint32_t *newValue); + void renderBlank(); + void Box(); +}; + +#endif diff --git a/src/UIComponent.cpp b/src/UIComponent.cpp new file mode 100644 index 0000000..0a60bd7 --- /dev/null +++ b/src/UIComponent.cpp @@ -0,0 +1,108 @@ +#include "UIComponent.h" +// #include "u8g2.h" +#include "Arduino.h" +#include "HardwareSerial.h" +#include "UIDrawing.h" +// #include +#include + +#define DEBUG 1 + +UIElement::UIElement(Arduino_GFX *d, UIDimensions dims, UIDecorations *decor, + char *title) + : display(d), dims(dims), decor(decor), title(title) {} + +UIDimensions::UIDimensions(uint16_t x, uint16_t y, uint16_t w, uint16_t h) + : x(x), y(y), width(w), height(h) {} + +int16_t UIElement::getContentAreaX0() { + if (this->decor->hasBorder) { + return this->dims.x + DEFAULT_BORDER_THICKNESS + DEFAULT_MARGIN; + } + + return this->dims.x; +} + +int16_t UIElement::getContentAreaY0() { + if (this->decor->hasBorder) { + return this->dims.y + this->getTitleAreaHeight() + + DEFAULT_TITLE_CONTENT_SPACING; + } + + return this->dims.y; +} + +uint16_t UIElement::getContentAreaHeight() { + return this->dims.height - (DEFAULT_MARGIN * 2) - DEFAULT_BORDER_THICKNESS - + this->getTitleAreaHeight(); +} + +uint16_t UIElement::getContentAreaWidth() { + return this->dims.width - ((DEFAULT_BORDER_THICKNESS + DEFAULT_MARGIN) * 2); +} + +int16_t UIElement::getTitleAreaX0() { + return this->dims.x + DEFAULT_BORDER_THICKNESS + DEFAULT_MARGIN; +} + +int16_t UIElement::getTitleAreaY0() { return this->dims.y; } + +uint16_t UIElement::getTitleAreaHeight() { + int16_t x = 0, y = 0; + uint16_t w = 0, h = 0; + + this->display->setTextSize(this->decor->titleSize); + this->display->getTextBounds((const char *)this->title, 0, 0, &x, &y, &w, &h); + + return h; +} + +uint16_t UIElement::getTitleAreaWidth() { + int16_t x = 0, y = 0; + uint16_t w = 0, h = 0; + + this->display->setTextSize(this->decor->titleSize); + this->display->getTextBounds((const char *)this->title, 0, 0, &x, &y, &w, &h); + + return w; +} + +void UIElement::replaceString(char *oldVal, char *newVal) { + size_t newValLen = strlen(newVal); + size_t oldValLen = strlen(oldVal); + + // It crashs somewhere around here + int16_t x0 = this->getContentAreaX0(); + int16_t y0 = this->getContentAreaY0(); // <- Crashes here + // Before here + + this->display->setTextSize(this->decor->textSize); + + // If the new value is shorter, delete the extra in advance + if (oldValLen > newValLen) { + // If the old value is longer than the new value, we have to delete the + // extra of the old value, which is whatever starts at the new value len + for (size_t start = newValLen; start < oldValLen; start++) { + this->display->setCursor(x0 + start * (CHR_WIDTH(this->decor->textSize)), + y0); + this->display->setTextColor(this->decor->bgColor); + this->display->print(oldVal[start]); + } + } + + for (size_t k = 0; k < newValLen; k++) { + if (k <= oldValLen) { + if (oldVal[k] != newVal[k]) { + this->display->setCursor(x0 + k * (CHR_WIDTH(this->decor->textSize)), + y0 + 0); + this->display->setTextColor(this->decor->bgColor); + this->display->print(oldVal[k]); + } + } + + this->display->setCursor(x0 + k * (CHR_WIDTH(this->decor->textSize)), + y0 + 0); + this->display->setTextColor(this->decor->fgColor); + this->display->print(newVal[k]); + } +} diff --git a/src/UIComponent.h b/src/UIComponent.h new file mode 100644 index 0000000..5be5b2d --- /dev/null +++ b/src/UIComponent.h @@ -0,0 +1,55 @@ +#ifndef __UI_COMPONENT +#define __UI_COMPONENT + +#include "Arduino_GFX.h" +#include "UIDecorations.h" +#include + +enum ComponentType { + STRING, + TABLE, +}; + +struct UIDimensions { + uint16_t x = 0, y = 0; + uint16_t width = 0, height = 0; + + UIDimensions(uint16_t x, uint16_t y, uint16_t w, uint16_t h); +}; + +struct UIElement { + Arduino_GFX *display; + UIDimensions dims; + UIDecorations *decor; + char *title; + uint64_t lastUpdate = 0; + uint16_t refreshRate = 0; + ComponentType type; + + UIElement(Arduino_GFX *d, UIDimensions dims, UIDecorations *decor, + char *Title); + + // Getters + int16_t getContentAreaX0(); + int16_t getContentAreaY0(); + uint16_t getContentAreaHeight(); + uint16_t getContentAreaWidth(); + int16_t getTitleAreaX0(); + int16_t getTitleAreaY0(); + uint16_t getTitleAreaHeight(); + uint16_t getTitleAreaWidth(); + + // Positioning + void horizontalCenter(UIElement *reference); + void verticalCenter(UIElement *reference); + void placeBelow(UIElement *reference); + void placeRight(UIElement *reference); + void placeLeft(UIElement *reference); + + // Drawing + void drawBox(); + void noBox(); + void replaceString(char *oldVal, char *newVal); +}; + +#endif diff --git a/src/UIDecorations.cpp b/src/UIDecorations.cpp new file mode 100644 index 0000000..fb30dc8 --- /dev/null +++ b/src/UIDecorations.cpp @@ -0,0 +1,6 @@ +#include "UIDecorations.h" + +UIDecorations::UIDecorations() {} +UIDecorations::UIDecorations(uint16_t bg, uint16_t fg, uint16_t title, + uint16_t border, uint8_t titleSize, + uint8_t textSize) {} diff --git a/src/UIDecorations.h b/src/UIDecorations.h new file mode 100644 index 0000000..9278704 --- /dev/null +++ b/src/UIDecorations.h @@ -0,0 +1,18 @@ +#ifndef __UI_DECORATIONS +#define __UI_DECORATIONS + +#include "UIDrawing.h" +#include + +struct UIDecorations { + bool hasBorder = true; + uint16_t bgColor = MAIN_BG_COLOR, fgColor = MAIN_FG_COLOR; + uint16_t titleColor = MAIN_FG_COLOR, borderColor = RED; + uint8_t titleSize = 2, textSize = 4; + + UIDecorations(); + UIDecorations(uint16_t bg, uint16_t fg, uint16_t title, uint16_t border, + uint8_t titleSize, uint8_t textSize); +}; + +#endif diff --git a/src/UIDrawing.cpp b/src/UIDrawing.cpp new file mode 100644 index 0000000..66e5b6a --- /dev/null +++ b/src/UIDrawing.cpp @@ -0,0 +1,107 @@ +#include "UIDrawing.h" +#include "Arduino_GFX.h" +#include "HardwareSerial.h" +#include "UIComponent.h" +#include + +/* TODO: + * On the placement functions, change 5 to a variable or something + */ + +// #define DEBUG + +uint16_t calculateHeight(int16_t titleSize, int16_t textSize, int16_t nLines) { + return CHR_HEIGHT(titleSize) + CHR_HEIGHT(textSize) + + DEFAULT_TITLE_CONTENT_SPACING + + (2 * (DEFAULT_BORDER_THICKNESS + DEFAULT_MARGIN)); +} + +uint16_t calculateWidth(int16_t textSize, int16_t nChars) { + return CHR_WIDTH(textSize) * nChars + + (2 * (DEFAULT_MARGIN + DEFAULT_BORDER_THICKNESS)); +} + +void UIElement::horizontalCenter(UIElement *ref) { + uint16_t middlePoint = 0; + + if (ref != nullptr) { + Serial2.println("We are here indeed!"); + middlePoint = ref->dims.x + (ref->dims.width / 2); + } else { + middlePoint = this->display->width() / 2; + } + + this->dims.x = middlePoint - (this->dims.width / 2); +} + +// We can add a mode of alignment here, center, left, right, whatever +void UIElement::placeBelow(UIElement *ref) { + // For now the default alignment will be left until I need some other + this->dims.x = ref->dims.x; + this->dims.y = ref->dims.y + ref->dims.height + 5; +} + +void UIElement::placeRight(UIElement *ref) { + this->dims.x = ref->dims.x + ref->dims.width + 5; +} + +void UIElement::placeLeft(UIElement *ref) { + this->dims.x = ref->dims.x - this->dims.width - 5; +} + +void UIElement::drawBox() { + // This is the border rectangle + if (this->decor->hasBorder) { + for (int k = 0; k < DEFAULT_BORDER_THICKNESS; k++) { + this->display->drawRect( + this->dims.x + k, this->dims.y + k, this->dims.width - (k * 2), + this->dims.height - (k * 2), this->decor->borderColor); + } + } + + // For debugging purpouses + // title area + int16_t x = 0, y = 0; + uint16_t w = 0, h = 0; + + // U8G2 u8g2; // No display pin setup needed since we're only using font data + // u8g2.setFont(u8g2_font_9x18_tf); + // int ascent = u8g2.getAscent(); + + h = this->getTitleAreaHeight(); + uint16_t titleAreaWidth = this->getTitleAreaWidth(); + int16_t titleTopLeftX = this->getTitleAreaX0(); +#ifdef DEBUG + // Title area bounds + this->display->drawRect(titleTopLeftX, this->getTitleAreaY0(), titleAreaWidth, + h, GREEN); +#endif + + // content area + x = this->getContentAreaX0(); + y = this->getContentAreaY0(); + w = this->getContentAreaWidth(); + h = this->getContentAreaHeight(); + +#ifdef DEBUG + // Content area bounds + this->display->drawRect(x, y, w, h, BLUE); +#endif + + if (this->decor->hasBorder) { + // Remove the border line behind the title + this->display->fillRect(titleTopLeftX - DEFAULT_MARGIN, this->dims.y, + titleAreaWidth + DEFAULT_MARGIN, + DEFAULT_BORDER_THICKNESS, MAIN_BG_COLOR); + + // Render the title + this->display->setCursor(titleTopLeftX, this->dims.y); + this->display->print(this->title); + } + +#ifdef DEBUG + // Element bounds + this->display->drawRect(this->dims.x, this->dims.y, this->dims.width, + this->dims.height, PURPLE); +#endif +} diff --git a/src/UIDrawing.h b/src/UIDrawing.h new file mode 100644 index 0000000..0650435 --- /dev/null +++ b/src/UIDrawing.h @@ -0,0 +1,25 @@ +#ifndef __CURSES_UI_VISUALS +#define __CURSES_UI_VISUALS + +#include + +#define RIGHT 0 +#define INVERTED 1 +#define LEFT 2 +#define NORMAL 3 + +#define DEFAULT_BORDER_THICKNESS 3 +#define DEFAULT_TITLE_CONTENT_SPACING 3 +#define DEFAULT_MARGIN 3 + +#define MAIN_BG_COLOR RGB565(20, 10, 10) +#define MAIN_FG_COLOR RGB565(255, 255, 255) + +#define CHR_WIDTH(size) (6 * size) +#define CHR_HEIGHT(size) (8 * size) +#define CHR_SPACE(size) (size) + +uint16_t calculateHeight(int16_t titleSize, int16_t textSize, int16_t nLines); +uint16_t calculateWidth(int16_t textSize, int16_t nChars); + +#endif diff --git a/src/UIString.cpp b/src/UIString.cpp new file mode 100644 index 0000000..e8f73e4 --- /dev/null +++ b/src/UIString.cpp @@ -0,0 +1,48 @@ +#include "UIString.h" +#include "HardwareSerial.h" +#include "UIComponent.h" +#include + +UIString::UIString() : UIElement(nullptr, {0, 0, 0, 0}, nullptr, (char *)"") { + this->type = STRING; + memset(this->value, 0, this->bufferSize); +} + +UIString::UIString(Arduino_GFX *d, UIDimensions dims, UIDecorations *decor, + char *title) + : UIElement(d, dims, decor, title) { + this->type = STRING; + memset(this->value, 0, this->bufferSize); +} + +void UIString::Update(const char *v) { + uint64_t time = millis(); + if ((time - this->lastUpdate) <= this->refreshRate) { + return; + } + + // If the value is larger than our buffer - should add a way of knowing we + // got an error here + if (strlen(v) >= this->bufferSize) { + return; + } + + // If the value hasn't changed we do not need to re-render + if (strcmp(this->value, v) == 0) { + return; + } + + char newVal[this->bufferSize]; + memset(newVal, 0, this->bufferSize); + // strncpy(newVal, v, this->bufferSize - 1); + sprintf(newVal, "%s", v); + + char oldVal[this->bufferSize]; + memset(oldVal, 0, this->bufferSize); + // strncpy(oldVal, v, this->bufferSize - 1); + sprintf(oldVal, "%s", this->value); + + // Figure out the x0 and y0 for the text + replaceString(oldVal, newVal); + strncpy(this->value, v, this->bufferSize); +} diff --git a/src/UIString.h b/src/UIString.h new file mode 100644 index 0000000..883664e --- /dev/null +++ b/src/UIString.h @@ -0,0 +1,17 @@ +#ifndef __UI_STRING +#define __UI_STRING + +#include "UIComponent.h" + +// Representation of single line strings +struct UIString : UIElement { + static const size_t bufferSize = 64; + char value[bufferSize]; + + UIString(); + UIString(Arduino_GFX *d, UIDimensions dims, UIDecorations *decor, + char *title); + void Update(const char *value); +}; + +#endif diff --git a/src/UITable.cpp b/src/UITable.cpp new file mode 100644 index 0000000..8c4ab22 --- /dev/null +++ b/src/UITable.cpp @@ -0,0 +1,82 @@ +#include "UITable.h" +#include "HardwareSerial.h" +#include "UIDecorations.h" +#include "UIDrawing.h" +#include + +#define DEBUG + +UITable::UITable(Arduino_GFX *gfx, UIDimensions dims, UIDecorations *decor, + int nRows, int nCols, int *colWidths, char *title) + : UIElement(gfx, dims, decor, title) { + this->type = TABLE; + this->nRows = nRows; + this->nColumns = nCols; + this->tableData = new UIString *[this->nRows * this->nColumns]; + this->colWidths = colWidths; + + // Default decoration for every cell + this->decor->textSize = 2; + + for (int k = 0; k < ROWS * COLUMNS; k++) { + UIDecorations *cellDecor = new UIDecorations(); + cellDecor->textSize = this->decor->textSize; + + UIDimensions dims = {0, 0, 0, 0}; + + this->tableData[k] = new UIString(display, dims, cellDecor, (char *)""); + } + + int cellH = CHR_HEIGHT(this->decor->textSize) + CELL_MARGIN; + + // Initialize the table dimensions + //// Calculate the height + this->dims.height = ROWS * cellH + this->getTitleAreaHeight() + + DEFAULT_MARGIN + DEFAULT_BORDER_THICKNESS; + + //// Calculate the width + for (int c = 0; c < this->nColumns; c++) { + this->dims.width += + (CHR_WIDTH(this->decor->textSize) + CELL_MARGIN) * this->colWidths[c]; + } + this->dims.width += (DEFAULT_BORDER_THICKNESS + DEFAULT_MARGIN) * 2; +} + +uint16_t UITable::getContentAreaHeight() { + return this->dims.height - (DEFAULT_MARGIN * 2) - DEFAULT_BORDER_THICKNESS - + this->getTitleAreaHeight(); +} + +/* + * This was hardcoded for the relative and had a warning for that but + * I recon its dynamic enough for now + */ +void UITable::setup() { + int cellH = CHR_HEIGHT(this->decor->textSize) + CELL_MARGIN; + + // Initialize the cells + uint16_t yOffset = this->getContentAreaY0(); + for (int r = 0; r < ROWS; r++) { + int16_t xOffset = this->getContentAreaX0(); + for (int c = 0; c < COLUMNS; c++) { + this->tableData[r * COLUMNS + c]->refreshRate = this->refreshRate; + this->tableData[r * COLUMNS + c]->decor->textSize = this->decor->textSize; + this->tableData[r * COLUMNS + c]->decor->hasBorder = false; + this->tableData[r * COLUMNS + c]->dims.x = xOffset + CELL_MARGIN; + this->tableData[r * COLUMNS + c]->dims.y = (cellH * r) + yOffset; + this->tableData[r * COLUMNS + c]->dims.height = cellH; + this->tableData[r * COLUMNS + c]->dims.width = + this->colWidths[c] * (CHR_WIDTH(this->decor->textSize) + CELL_MARGIN); + memset(this->tableData[r * COLUMNS + c]->value, 0, + this->tableData[r * COLUMNS + c]->bufferSize); + + xOffset += + this->colWidths[c] * (CHR_WIDTH(this->decor->textSize) + CELL_MARGIN); + +#ifdef DEBUG + this->tableData[r * COLUMNS + c]->drawBox(); + this->tableData[r * COLUMNS + c]->Update("---"); +#endif + } + } +} diff --git a/src/UITable.h b/src/UITable.h new file mode 100644 index 0000000..8098358 --- /dev/null +++ b/src/UITable.h @@ -0,0 +1,31 @@ +#ifndef __UI_TABLE +#define __UI_TABLE + +#include "Arduino_GFX.h" +#include "UIComponent.h" +#include "UIString.h" + +#define COLUMNS 3 +#define ROWS 5 +#define CELL_MARGIN 2 + +/* Very shitty table element, only works for the relative as of now + */ +struct UITable : UIElement { + // will hold rows * columns UIStrings to fill the table + int nRows = 0; + int nColumns = 0; + int *colWidths; + UIString **tableData; + + UITable(Arduino_GFX *gfx, UIDimensions dims, UIDecorations *decor, int nR, + int nC, int *cWidth, char *t); + + // void Update(StandingLine standings[5]); + void setup(); + + // Getters + uint16_t getContentAreaHeight(); +}; + +#endif