diff --git a/.github/workflows/arduino-lint.yml b/.github/workflows/arduino-lint.yml index b689435..87f6b18 100644 --- a/.github/workflows/arduino-lint.yml +++ b/.github/workflows/arduino-lint.yml @@ -6,5 +6,5 @@ jobs: - uses: actions/checkout@v2 - uses: arduino/arduino-lint-action@v1 with: - # library-manager: update + library-manager: update compliance: strict \ No newline at end of file diff --git a/API.md b/API.md new file mode 100644 index 0000000..bfe9dfd --- /dev/null +++ b/API.md @@ -0,0 +1,88 @@ +# LCDGraph API +## Table of Contents +- [Including](#including) +- [Constructor](#constructor) +- [Attributes](#attributes) +- [Methods](#methods) + - [`void begin(LiquidCrystal *lcd)`](#void-beginliquidcrystal-lcd) + - [`void add(DataFormat value)`](#void-adddataformat-value) + - [`void clear()`](#void-clear) + - [`void setRegisters()`](#void-setregisters) + - [`void display(uint8_t x, uint8_t y)`](#void-displayuint8_t-x-uint8_t-y) + - [`void autoRescale(bool force0 = false, bool allowSmallerRange = true)`](#void-autorescalebool-force0--false-bool-allowsmallerrange--true) + - [`uint8_t length()`](#uint8_t-length) + - [`void end()`](#void-end) + +# Including +Add these lines to the top of your main project file: +```c++ +// Libraries to include +#include +#include +``` + + +# Constructor +```c++ +LCDGraph(uint8_t width, uint8_t height, uint8_t firstRegister) +``` +Initialises the class. +Currently, the height must be 1 as vertical tiling is not yet implemented. +- `DataFormat` is a template and is the data format that will be expected when calling `add` and setting and reading `yMax` and `yMin`. This can be changed to a smaller data type such as a `byte` to save memory if needed or a larger one such as an `unsigned long` or `float` if larger or floating point numbers need to be displayed. Because each data type will require its own copy of the class, it is recommended that if possible, only a single data type is used accross all instances to save program memory. +- `width` is the width in characters. +- `height` is the height in characters on the display, but should be 1 currently. +- `firstRegister` is the first register to use in the display. As there are only 8, they may need to be shared around. + +For example, using 2 4 character wide graphs would be: +```c++ +LCDGraph graph1(4, 1, 0); +LCDGraph graph2(4, 1, 4); +``` + +An 8 char wide graph must start at register 0 in the display, a 7 char wide at 0 or 1, ... + +# Attributes +| Name | Data Type | Comments | Default | +| ------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-----: | +| `yMin` | `DataFormat` (specified in constructor) | The current minimum of the Y axis. Can be written to to set it manually | 0 | +| `yMax` | `DataFormat` (specified in constructor) | The current maximum of the Y axis. Can be written to to set it manually | 255 | +| `filled` | `bool` | Whether everything under the line will be filled in | `true` | +| `showXAxis` | `bool` | Whether to draw the X axis as a solid line. | `true` | +| `showYAxis` | `bool` | Whether to draw the Y axis as a solid line. | `true` | +| `intercepts` | `bool` | If `true`, makes any x and y axis intercepts display as an off pixel when axis are displayed so that points on axis do not disappear. Can be a bit misleading if the x axis is on the top or bottom of the display. | `false` | + + +# Methods +## `void begin(LiquidCrystal *lcd)` +Gives the library the pointer to the lcd object to use. +For example: +```c++ +// Set up the lcd +lcd.begin(20, 4); +graph.begin(&lcd); +``` + +## `void add(DataFormat value)` +Adds a data point to the internal graph circular buffer. +Chucks out the earlist data point if the buffer is full. + +## `void clear()` +Removes all data from the circular buffer + +## `void setRegisters()` +Generates the custom characters from the data currently in the circular buffer and sends it to the custom character registers in the lcd. If any of the special characters or this graph are already on the display, they will be updated. Otherwise, `display(uint8_t x, uint8_t y);` needs to be called to draw the graph from the registers. + +## `void display(uint8_t x, uint8_t y)` +Displays the graph in the correct location on the display. setRegisters needs to be called beforehand to display the latest data. In all displays I have used, `display` only needs to be called once if the graph is going to be continually be updated in the same location. + +## `void autoRescale(bool force0 = false, bool allowSmallerRange = true)` +Rescales the graph to fit all data. +- `force0` will make sure that 0 is included as either the minimum or maximum if true. 0 will also be included as a limit if there is only a single value. +- `allowSmallerRange` will allow `yMin` to increase and `yMax` to decrease to fit all current data. If false, the range can only expand (`yMin` decrease and `yMax` increase). + +## `uint8_t length()` +Returns the number of points in the circular buffer. + +## `void end()` +Deallocates the internal circular buffer from memory. +It is preferable to only create the instance and use it as global (with `clear()` if necessary) for the entire time the program is running to avoid memory fragmentation. \ No newline at end of file diff --git a/LCDGraph.cpp b/LCDGraph.cpp new file mode 100644 index 0000000..34a7af9 --- /dev/null +++ b/LCDGraph.cpp @@ -0,0 +1,145 @@ +/** + * LCDGraph.cpp - Arduino library for drawing graphs on alphanumeric LCDs using custom chars. + * Jotham Gates, 27/11/2020 + */ + +#include "Arduino.h" +#include "LCDGraph.h" +#include + +LCDGraph::LCDGraph(uint8_t width, uint8_t height, uint8_t firstRegister) { + _width = width; + _height = height; + _firstRegister = firstRegister; +} + +void LCDGraph::begin(LiquidCrystal *lcd) { + _lcd = lcd; +} + +/** Adds a data point to the graph circular buffer. + * Chucks out the earlist data point if the buffer is full. + */ +void LCDGraph::add(LCDGRAPH_DATA_TYPE value) { + // Calculate the position for the new data point + uint8_t position = _start + length; + if(position >= LCDGRAPH_MAX_DATA) { + position -= LCDGRAPH_MAX_DATA; + } + + // Add the new data point + _data[position] = value; + + // Either increase the length if not full or update the address of the first element to be the next as we will have just overwritten the first element if the buffer is at capacity. + if(length < LCDGRAPH_MAX_DATA) { + length++; + } else { + _start++; + if(_start == LCDGRAPH_MAX_DATA) { // Wrap around if needed + _start = 0; + } + } +} +void LCDGraph::clear() { + length = 0; +} + +void LCDGraph::setRegisters() { + uint8_t accessed = 0; + // For each character to draw + for(uint8_t block = 0; block < _width; block++) { + // Get the data from the stored array and map it to the available pixel height. + uint8_t mappedData[LCDGRAPH_CHAR_WIDTH]; + + // Clear mappedData: + for(uint8_t i = 0; i < LCDGRAPH_CHAR_WIDTH; i++) { + mappedData[i] = 0; + } + + uint8_t printMask = 0x00; // If a bit is 0, do not print anything in its column. + uint8_t startPos = block * LCDGRAPH_CHAR_WIDTH; + + // Load data if available and map it to fit + for(uint8_t i = 0; i < LCDGRAPH_CHAR_WIDTH && accessed < length; i++, accessed++) { + mappedData[i] = map(_atPosition(i + startPos), yMax, yMin, 0, LCDGRAPH_CHAR_HEIGHT-1); + printMask |= 1 << i; // Set the value as printable + } + + // Generate the custom character for that block + uint8_t xAxis = map(0, yMax, yMin, 0, LCDGRAPH_CHAR_HEIGHT-1); + + uint8_t character[LCDGRAPH_CHAR_HEIGHT]; + for(uint8_t row = 0; row < LCDGRAPH_CHAR_HEIGHT; row++) { + character[row] = 0; // Clear the row in case there is something left in memory + // Draw the x axis if needed + if(axis && (row == xAxis)) { + character[row] = 0xff; // Use exclusive or so that any point on the x axis is now light to stand out. + } + // For each column, check if a point should be drawn in the row. + for(uint8_t col = 0; col < LCDGRAPH_CHAR_WIDTH; col++) { + // Draw the y axis if needed + if(axis && !col && !block) { + character[row] |= 1 << (LCDGRAPH_CHAR_WIDTH -1); + } + + // Draw the point on the graph or point under it if shading + if((printMask >> col) & 1) { // If there should be a point drawn on this column + if(mappedData[col] == row) { // An actual point + character[row] ^= 1 << (LCDGRAPH_CHAR_WIDTH -1 -col); // XOR so that any point on an axis appears white. + } else if(filled && mappedData[col] < row) { // The shaded area below + character[row] |= 1 << (LCDGRAPH_CHAR_WIDTH -1 -col); // Normal OR so the axis is not inverted + } + } + } + } + + _lcd->createChar(_firstRegister + block, character); // Send the character to the display. + } +} + +/* Draws the custom characters to the display */ +void LCDGraph::display(uint8_t x, uint8_t y) { + _lcd->setCursor(x, y); + for(uint8_t i = _firstRegister; i < _width + _firstRegister; i++) { + _lcd->write(uint8_t(i)); + } +} + +/** Rescales the graph to fit all data. If force0 is set to true, will make + * sure that 0 is included as either the minimum or maximum. + */ +void LCDGraph::autoRescale(bool force0) { + // Find the minimum and maximum + LCDGRAPH_DATA_TYPE value = _atPosition(0); + yMin = value; + yMax = value; + for(uint8_t i = 1; i < length; i++) { + LCDGRAPH_DATA_TYPE value = _atPosition(i); + if(value < yMin) { + yMin = value; + } + if(value > yMax) { + yMax = value; + } + } + + // Make sure the x axis is displayed if required. + if(force0 || yMin == yMax) { // yMin == yMax is for if there is only 1 point, include a 0 as another point. + if(yMin > 0) { + yMin = 0; + } + if(yMax < 0) { + yMax = 0; + } + } +} + +/* Translates the position in order into the physical address and returns the value at the address. */ +inline LCDGRAPH_DATA_TYPE LCDGraph::_atPosition(uint8_t position) { + position += _start; + if(position >= LCDGRAPH_MAX_DATA) { + position -= LCDGRAPH_MAX_DATA; + } + + return _data[position]; +} \ No newline at end of file diff --git a/LCDGraph.h b/LCDGraph.h new file mode 100644 index 0000000..495b5bc --- /dev/null +++ b/LCDGraph.h @@ -0,0 +1,220 @@ +/** + * LCDGraph.h - Arduino library for drawing graphs on alphanumeric LCDs using custom chars. + * Jotham Gates + * Created 27/11/2020 + * Last Modified 07/02/2021 + */ + +// TODO: Make this work with something other than a 8 by 1 graph. +// TODO: Make the data work with templates. - Everything put in the header file for this purpose +#ifndef LCDGraph_h +#define LCDGraph_h + +#include "Arduino.h" +#include + +#define LCDGRAPH_MAX_CHARS 8 +#define LCDGRAPH_CHAR_WIDTH 5 +#define LCDGRAPH_CHAR_HEIGHT 8 // Could be 10 on some LCDs +#define DataFormat int16_t +template +class LCDGraph { + public: + /** Initialises the class. + * Currently, the height must be 1 as vertical tiling is not yet implemented. + * @param width is the width in characters. + * @param height is the height in characters on the display, but should be 1 currently. + * @param firstRegister is the first register to use in the display. As there are only 8, they may need to be shared around. + * For example, using 2 4 character wide graphs would be: + * LCDGraph graph1(4, 1, 0); + * LCDGraph graph2(4, 1, 4); + * An 8 char wide graph must start at 0, a 7 char wide at 0 or 1, ... + */ + LCDGraph(uint8_t width, uint8_t height, uint8_t firstRegister) { + _width = width; + _height = height; + _firstRegister = firstRegister; + _data = (DataFormat *)malloc(width * LCDGRAPH_CHAR_WIDTH * sizeof(DataFormat)); + } + + /** Sets the pointer to the lcd object */ + void begin(LiquidCrystal *lcd) { + _lcd = lcd; + } + + /** Adds a data point to the graph circular buffer. + * Chucks out the earlist data point if the buffer is full. + */ + void add(DataFormat value) { + // Calculate the position for the new data point + uint8_t position = _start + length; + uint8_t dataSize = _width * LCDGRAPH_CHAR_WIDTH; + if(position >= dataSize) { + position -= dataSize; + } + + // Add the new data point + _data[position] = value; + // Either increase the length if not full or update the address of the first element to be the next as we will have just overwritten the first element if the buffer is at capacity. + if(length < dataSize) { + length++; + } else { + _start++; + if(_start == dataSize) { // Wrap around if needed + _start = 0; + } + } + } + + /** Removes all data from the circular buffer */ + void clear() { + length = 0; + } + + /** Generates the custom characters from the data currently in the circular buffer and sends it to the custom character registers in the lcd. + * If any of the special characters or this graph are already on the display, they will be updated. + * Otherwise, display(uint8_t x, uint8_t y); needs to be called to draw the graph from the registers. + */ + void setRegisters() { + uint8_t accessed = 0; + // For each character to draw + for(uint8_t block = 0; block < _width; block++) { + // Get the data from the stored array and map it to the available pixel height. + uint8_t mappedData[LCDGRAPH_CHAR_WIDTH]; + + // Clear mappedData: + for(uint8_t i = 0; i < LCDGRAPH_CHAR_WIDTH; i++) { + mappedData[i] = 0; + } + + uint8_t printMask = 0x00; // If a bit is 0, do not print anything in its column. + uint8_t startPos = block * LCDGRAPH_CHAR_WIDTH; + + // Load data if available and map it to fit + for(uint8_t i = 0; i < LCDGRAPH_CHAR_WIDTH && accessed < length; i++, accessed++) { + mappedData[i] = map(_atPosition(i + startPos), yMax, yMin, 0, LCDGRAPH_CHAR_HEIGHT-1); + printMask |= 1 << i; // Set the value as printable + } + + // Generate the custom character for that block + uint8_t xAxis = map(0, yMax, yMin, 0, LCDGRAPH_CHAR_HEIGHT-1); + + uint8_t character[LCDGRAPH_CHAR_HEIGHT]; + for(uint8_t row = 0; row < LCDGRAPH_CHAR_HEIGHT; row++) { + character[row] = 0; // Clear the row in case there is something left in memory + // Draw the x axis if needed + if(showXAxis && (row == xAxis)) { + character[row] = 0xff; // Use exclusive or so that any point on the x axis is now light to stand out. + } + // For each column, check if a point should be drawn in the row. + for(uint8_t col = 0; col < LCDGRAPH_CHAR_WIDTH; col++) { + // Draw the y axis if needed + if(showYAxis && !col && !block) { + character[row] |= 1 << (LCDGRAPH_CHAR_WIDTH -1); + } + + // Draw the point on the graph or point under it if shading + if((printMask >> col) & 1) { // If there should be a point drawn on this column + uint8_t point = 1 << (LCDGRAPH_CHAR_WIDTH -1 -col); + if(mappedData[col] == row) { // An actual point + // Either XOR or OR to display or not display axis intercepts as the pixels inverted + if(intercepts) { + character[row] ^= point; // XOR so that any point on an axis appears white. + } else { + character[row] |= point; // A point on the axis will be hidden + } + } else if(filled && ((row > mappedData[col] && row < xAxis) || (row < mappedData[col] && row > xAxis))) { // The shaded area below + character[row] |= point; // Normal OR so the axis is not inverted + } + } + } + } + _lcd->createChar(_firstRegister + block, character); // Send the character to the display. + } + } + + /** Displays the graph in the correct location on the display. + * setRegisters needs to be called beforehand to display the latest data. + * In all displays I have used, display() only needs to be called once if the graph is going to be continually be updated in the same location + */ + void display(uint8_t x, uint8_t y) { + _lcd->setCursor(x, y); + for(uint8_t i = _firstRegister; i < _width + _firstRegister; i++) { + _lcd->write(uint8_t(i)); + } + } + + + /** Rescales the graph to fit all data. + * @param force0 will make sure that 0 is included as either the minimum or maximum if true. + * 0 will also be included as a limit if there is only a single value. + * @param allowSmallerRange will allow yMin to increase and yMax to decrease to fit all + * current data. If false, the range can only expand (yMin decrease and yMax increase). + */ + // TODO: Padding instead of including 0 + void autoRescale(bool force0 = false, bool allowSmallerRange = true) { + // Find the minimum and maximum + if(allowSmallerRange) { + DataFormat value = _atPosition(0); + yMin = value; + yMax = value; + } + for(uint8_t i = 0; i < length; i++) { + DataFormat value = _atPosition(i); + if(value < yMin) { + yMin = value; + } + if(value > yMax) { + yMax = value; + } + } + + // Make sure the x axis is displayed if required. + if(force0 || yMin == yMax) { // yMin == yMax is for if there is only 1 point, include a 0 as another point. + if(yMin > 0) { + yMin = 0; + } + if(yMax < 0) { + yMax = 0; + } + } + } + + /** Deallocates the buffer from memory. + * It is preferable to only create the instance and use it (with clear() if necessary) for the entire time the program is running. + */ + void end() { + free(_data); + } + + uint8_t length = 0; // Number of data points being shown + DataFormat yMin = 0; + DataFormat yMax = 255; + bool filled = true; // TODO: Make filled to the x axis rather than bottom of screen. + bool showXAxis = true; // Show x axis + bool showYAxis = true; + bool intercepts = false; // Make any x and y axis intercepts display as an off pixel when axis are displayed + + private: + /** Translates the position in order into the physical address and returns the value at the address. */ + DataFormat _atPosition(uint8_t position) { + position += _start; + uint8_t dataSize = _width * LCDGRAPH_CHAR_WIDTH; + if(position >= dataSize) { + position -= dataSize; + } + + return _data[position]; + } + + LiquidCrystal *_lcd; + DataFormat *_data; + uint8_t _start = 0; + uint8_t _width = 8; + uint8_t _height = 1; + + uint8_t _firstRegister = 0; + +}; + +#endif \ No newline at end of file diff --git a/Pictures/InUse.jpg b/Pictures/InUse.jpg new file mode 100644 index 0000000..67d8fe6 Binary files /dev/null and b/Pictures/InUse.jpg differ diff --git a/Pictures/Mockup.svg b/Pictures/Mockup.svg new file mode 100644 index 0000000..4df5cd3 --- /dev/null +++ b/Pictures/Mockup.svg @@ -0,0 +1,20322 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index a5a520e..cc52349 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# LCDGraph ![Arduino Lint Actions Status](https://github.com/jgOhYeah/LCDGraph/actions/workflows/arduino-lint.yml/badge.svg) +# LCDGraph ![Arduino Lint Actions Status](https://github.com/jgOhYeah/LCDGraph/actions/workflows/arduino-lint.yml/badge.svg) An Arduino library for drawing line graphs on alphanumeric displays using custom characters @@ -8,9 +8,7 @@ This library uses custom characters to draw simple line graphs on Hitachi HD4478 The x axis is always the data point while the y axis can be scaled as required, even after data has been entered. -## Table of Contents -- [LCDGraph](#lcdgraph) - - [Table of Contents](#table-of-contents) +## Table of Contents - [Connections](#connections) - [Getting started](#getting-started) - [Installing the library](#installing-the-library) diff --git a/library.properties b/library.properties index 2767c67..964d19a 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=LCDGraph -version=1.0.0 +version=1.1.0 author=Jotham Gates maintainer=Jotham Gates sentence=An Arduino library for drawing line graphs on alphanumeric displays using custom characters.