Skip to content

Commit

Permalink
Cleaned up and made more object orientated like
Browse files Browse the repository at this point in the history
  • Loading branch information
Jotham Gates committed Aug 19, 2021
1 parent 035d81c commit 966f9d2
Show file tree
Hide file tree
Showing 8 changed files with 20,779 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/arduino-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ jobs:
- uses: actions/checkout@v2
- uses: arduino/arduino-lint-action@v1
with:
# library-manager: update
library-manager: update
compliance: strict
88 changes: 88 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# LCDGraph API <!-- omit in toc -->
## Table of Contents <!-- omit in toc -->
- [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 <LiquidCrystal.h>
#include <LCDGraph.h>
```


# Constructor
```c++
LCDGraph<DataFormat>(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<int> graph1(4, 1, 0);
LCDGraph<int> 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.
145 changes: 145 additions & 0 deletions LCDGraph.cpp
Original file line number Diff line number Diff line change
@@ -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 <LiquidCrystal.h>

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];
}
Loading

0 comments on commit 966f9d2

Please sign in to comment.