Skip to content

Commit

Permalink
Merge pull request #24 from DCC-EX:row-formatting
Browse files Browse the repository at this point in the history
Row-formatting
  • Loading branch information
peteGSX authored Jan 13, 2025
2 parents 3843d24 + e5d6bf7 commit cf3f1bf
Show file tree
Hide file tree
Showing 11 changed files with 942 additions and 45 deletions.
212 changes: 212 additions & 0 deletions DisplayInterface.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* © 2025 Peter Cole
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this code. If not, see <https://www.gnu.org/licenses/>.
*/

#include "DisplayInterface.h"
#include <iomanip>

void DisplayInterface::setNext(DisplayInterface *display) { _next = display; }

DisplayInterface *DisplayInterface::getNext() { return _next; }

void DisplayInterface::setLogger(Logger *logger) { _logger = logger; }

void DisplayInterface::setId(uint8_t displayId) { _displayId = displayId; }

uint8_t DisplayInterface::getId() { return _displayId; }

void DisplayInterface::setScreenId(int screenId) {
_needsRedraw = true;
_screenId = screenId;
}

int DisplayInterface::getScreenId() { return _screenId; }

int DisplayInterface::getCSPin() { return _csPin; }

void DisplayInterface::setNeedsRedraw(bool redraw) { _needsRedraw = redraw; }

bool DisplayInterface::needsRedraw() { return _needsRedraw; }

/**
* @brief Static method to enable calling back to a derived class with a formatted row
* @details Valid uses of modifiers:
* "`_`This is an underlined row"
* "`-`This is a horizontal line, text will be ignored"
* "`~`This row will always ticker when implemented"
* "`!`This row will never ticker when implemented"
* "`#FF0000`This row has red text"
* "`_``~`This row is underlined, will always ticker, and `#FF0000`this bit of text is red"
* "`!``_`This row is underlined, and will never ticker"
* "`#FF0000`This row starts red, then goes `#FFFFFF`white"
* @param row Row to display
* @param text Text containing formatting
*/
void DisplayInterface::formatRow(int rowId, const char *text) {
/**
* @brief stateMachine enum allows us to iterate through each char of text and examine it byte by byte for modifiers.
*/
enum stateMachine : byte { FIND_MODSTART, FIND_MODIFIER, FIND_COLOURSTART, FIND_COLOUR };

size_t textLength = strlen(text) + 1; /** Length of the provided text, we can't return anything longer than this */
size_t textStart = 0; /** Starting index of text we need to return, enables subtracting modifiers */
size_t copyLength = 0; /** Size of text to copy into textOnly later */
int column = 0; /** Default start at column 0, needs to be updated if appending */
bool append = false; /** Default is a new row, update if colours are embedded */
char *textOnly = new char[textLength]; /** Text only minus any modifiers, no bigger than provided size */
char check; /** Holds each char for checking */
stateMachine state = FIND_MODSTART; /** Start by looking for the first backtick */
RowAttributes attributes = {false, false, false,
false, false, 0xFFFF}; /** Set all attributes false to start with, and white text */

// Iterate through the provided text to look for leading modifiers
for (size_t i = 0; i < textLength; i++) {
check = text[i];
switch (state) {
case FIND_MODSTART: { // If first backtick, look for a modifier next, skip if using colour modifier
if (check == '`' && text[i + 1] != '#') {
state = FIND_MODIFIER;
break;
}
}
case FIND_MODIFIER: {
if (_isModifier(check) && text[i + 1] == '`' &&
(i + 1) <= textLength) { // If modifier is valid and next char is backtick, set it
attributes = _setAttribute(attributes, check);
i++;
state = FIND_MODSTART; // There may be more modifiers so look again
textStart = i + 1; // Set the start of our text to the next char after the backtick
} else {
state = FIND_COLOURSTART; // Now we've finished with modifiers, need to look for colours
}
break;
}
case FIND_COLOURSTART: {
if (check == '`') {
state = FIND_COLOUR;
break;
}
}
case FIND_COLOUR: {
if (check == '#' && text[i + 7] == '`' && (i + 7) <= textLength) { // Look for valid colour size of #FFFFFF
char *rgb = new char[7];
strncpy(rgb, text + i + 1, 6); // Extract the RGB colour string
rgb[6] = '\0';
if (_isRGB(rgb)) { // If colour is valid, we need to convert to RGB565
uint16_t rgb565 = _convertRGBtoRGB565(rgb);
if (i > (textStart + 1)) { // If the colour is found after the start of our text...
// Calculate text length and copy the subtext so far
// Send that off with:
// display->displayFormattedRow(rowId, column, attributes, textSoFar, append);
// Set append flag because everything after that is appended
copyLength = i - textStart - 1;
strncpy(textOnly, text + textStart, copyLength);
textOnly[copyLength] = '\0';
displayFormattedRow(rowId, column, attributes, textOnly, append);
append = true;
column += copyLength;
}
textStart = i + 8;
attributes = _setAttribute(attributes, check, rgb565); // Set the colour and flag it
state = FIND_COLOURSTART;
}
delete[] rgb;
}
break;
}
}
}
// Make sure our attributes are sane according to the rules
attributes = DisplayInterface::_sanitiseAttributes(attributes);
// If we've set a horizontal line, we don't return text, just null terminator
if (attributes.isLine) {
column = 0;
textOnly[0] = '\0';
} else {
// Otherwise copy the appropriate chars to returnedText ready to call our method
copyLength = textLength - textStart - 1;
strncpy(textOnly, text + textStart, copyLength);
textOnly[copyLength] = '\0';
}
displayFormattedRow(rowId, column, attributes, textOnly, append);
delete[] textOnly;
}

RowAttributes DisplayInterface::_sanitiseAttributes(RowAttributes attributes) {
if (attributes.isLine) {
attributes.isUnderlined = false;
attributes.alwaysTicker = false;
attributes.neverTicker = false;
}
if (attributes.alwaysTicker && attributes.neverTicker) {
attributes.alwaysTicker = false;
attributes.neverTicker = false;
}
return attributes;
}

bool DisplayInterface::_isModifier(char check) {
// Note while # is the modifier for colour, it is not a single char modifier and is dealt with separately
if (check == '_' || check == '-' || check == '~' || check == '!') {
return true;
} else {
return false;
}
}

RowAttributes DisplayInterface::_setAttribute(RowAttributes attributes, char modifier, uint16_t colour) {
switch (modifier) {
case '#':
attributes.colourSet = true;
attributes.textColour = colour;
break;
case '_':
attributes.isUnderlined = true;
break;
case '-':
attributes.isLine = true;
break;
case '~':
attributes.alwaysTicker = true;
break;
case '!':
attributes.neverTicker = true;
break;
default:
break;
}
return attributes;
}

bool DisplayInterface::_isRGB(const char *colour) {
bool isRGB = true;
for (size_t i = 0; i < 6; i++) {
// Iterate through the char array, valid chars are 0-9 and A-f
if ((colour[i] < '0' || colour[i] > '9') && (colour[i] < 'A' || colour[i] > 'F')) {
isRGB = false;
}
}
return isRGB;
}

uint16_t DisplayInterface::_convertRGBtoRGB565(const char *colour) {
uint16_t rgb565 = 0;
uint8_t r = strtol(colour, NULL, 16) >> 16;
uint8_t g = (strtol(colour, NULL, 16) >> 8) & 0xFF;
uint8_t b = strtol(colour, NULL, 16) & 0xFF;
rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
return rgb565;
}
89 changes: 76 additions & 13 deletions DisplayInterface.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/*
* © 2025 Peter Cole
* © 2024 Peter Cole
*
* This is free software: you can redistribute it and/or modify
Expand All @@ -21,6 +22,16 @@
#include "Logger.h"
#include "Screen.h"

/// @brief Structure for row attributes
struct RowAttributes {
bool colourSet; /** This row has had a custom colour set, use textColour, set with '#' */
bool isUnderlined; /** This row needs to be underlined, set with '_' */
bool isLine; /** This row is a horizontal line, set with '-' */
bool alwaysTicker; /** This row should always scroll horizontally, set with '~' */
bool neverTicker; /** This row should never scroll horizontally, set with '!' */
uint16_t textColour; /** 16bit colour for text */
};

/// @brief Class to abstract away all physical display implementation to enable multiple display types
class DisplayInterface {
public:
Expand All @@ -38,48 +49,59 @@ class DisplayInterface {
/// @param version EX-Display version
virtual void displayStartupInfo(const char *version) = 0;

/// @brief Display a row using formatting modifiers
/// @param row Row ID to display
/// @param column Column at which to display text (not pixels)
/// @param attributes RowAttributes structure containing modifier details
/// @param text Text to display
/// @param append Flag if this is appending to an existing row and should not clear the row first
virtual void displayFormattedRow(uint8_t row, uint8_t column, RowAttributes attributes, const char *text,
bool append) = 0;

/// @brief Set the next DisplayInterface derived instance in the list
/// @param display Pointer to the next instance
void setNext(DisplayInterface *display) { _next = display; }
void setNext(DisplayInterface *display);

/// @brief Get the next DisplayInterface derived instance in the list
/// @return Pointer to the next instance
DisplayInterface *getNext() { return _next; }
DisplayInterface *getNext();

/// @brief Set the logger instance to use for diagnostic logging
/// @param logger Pointer to the Logger instance to use
void setLogger(Logger *logger) { _logger = logger; }
void setLogger(Logger *logger);

/// @brief Set the ID for this display instance
/// @param displayId ID of this display
void setId(uint8_t displayId) { _displayId = displayId; }
void setId(uint8_t displayId);

/// @brief Get the ID of this display instance
/// @return ID of this display
uint8_t getId() { return _displayId; }
uint8_t getId();

/// @brief Set the Screen ID this display is currently displaying
/// @param screenId Screen ID
void setScreenId(int screenId) {
_needsRedraw = true;
_screenId = screenId;
}
void setScreenId(int screenId);

/// @brief Get the Screen ID this display is currently displaing
/// @return Screen ID
int getScreenId() { return _screenId; }
int getScreenId();

/// @brief Get the defined CS pin for this display to see if it needs manual SPI switching
/// @return Pin number of the SPI CS pin for this display (default -1 for no switching)
int getCSPin() { return _csPin; }
int getCSPin();

/// @brief Set the flag for whether this display needs redrawing or not - individual row updates are not affected
/// @param redraw true if entire redraw, otherwise false
void setNeedsRedraw(bool redraw) { _needsRedraw = redraw; }
void setNeedsRedraw(bool redraw);

/// @brief Test if this display needs an entire redraw
/// @return true|false
bool needsRedraw() { return _needsRedraw; }
bool needsRedraw();

/// @brief Static method to enable calling back to a derived class with a formatted row
/// @param row Row to display
/// @param text Text containing formatting
void formatRow(int row, const char *text);

/// @brief Destructor for a DisplayInterface
virtual ~DisplayInterface() = default;
Expand Down Expand Up @@ -114,6 +136,47 @@ class DisplayInterface {
int _csPin = -1;
/// @brief Flag that this display needs redrawing - needed for switching between screens
bool _needsRedraw = true;

/**
* @brief Sanitise the provided struct of RowAttributes
* @details If isLine is set, all other attributes except colour are overridden to false. If both alwaysTicker and
* neverTicker are set, both will be set to false as they conflict.
* @param attributes RowAttributes struct to sanitise
* @return RowAttributes Sanitised struct according to the precedence rules
*/
RowAttributes _sanitiseAttributes(RowAttributes attributes);

/**
* @brief Validates the provided char is a valid modifier
* @param check Contains the char to be validated
* @return true If modifier is valid (_, -, ~, !, #)
* @return false If modifier is invalid
*/
bool _isModifier(char check);

/**
* @brief Update the provided RowAttributes struct according to the provided modifier
* @param attributes RowAttribute to be updated
* @param modifier Modifier to apply
* @param colour If applied modifier is colour, set this 16bit colour, otherwise this is ignored
* @return RowAttributes The updated struct
*/
RowAttributes _setAttribute(RowAttributes attributes, char modifier, uint16_t colour = 0xFFFF);

/**
* @brief Check if the provided string constant translates to a valid RGB colour code
* @param colour String constant containing RGB colour codes to check
* @return true If valid - #000000 to #FFFFFF
* @return false If invalid
*/
bool _isRGB(const char *colour);

/**
* @brief Convert the provided RGB colour code string constant to a uint16_t RGB565 colour
* @param colour String constant containing the RGB colour code
* @return uint16_t RGB565 colour code
*/
uint16_t _convertRGBtoRGB565(const char *colour);
};

#endif // DISPLAYINTERFACE_H
Loading

0 comments on commit cf3f1bf

Please sign in to comment.