diff --git a/SparkLine.h b/SparkLine.h new file mode 100644 index 0000000..444beaa --- /dev/null +++ b/SparkLine.h @@ -0,0 +1,150 @@ +// Adaped version of ESParklines to sidestep random() issue in FixedPointsArduino library +// +// ESParklines – I <3 Sparklines! +// Sparklines for ESP8266/ESP32/Arduino Displays +// +// Copyright (c) 2020 karl@pitrich.com +// +// 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. +// + +#pragma once + +#include +#include +#include +//#include +//#include + +template ::value>::type* = nullptr> +class SparkLine +{ + using drawLineFunction = std::function; + using Point = struct { uint16_t x; uint16_t y; }; + using num_t = double;//float;//SFixed<15, 16>; + + T* container; + const size_t capacity; + size_t elements; + drawLineFunction drawLine; + + T findMin() const { + T lo = container[0]; + for (size_t i = 0; i < elements; i++) { + if (container[i] < lo) lo = container[i]; + } + return lo; + } + + T findMax() const { + T hi = 0; + for (size_t i = 0; i < elements; i++) { + if (container[i] > hi) hi = container[i]; + } + return hi; + } + +public: + SparkLine(size_t _size, drawLineFunction _dlf) + : container(new T[_size]), + capacity(_size), + drawLine(_dlf) + { + reset(); + } + + virtual ~SparkLine() + { + delete[] container; + } + + void reset() + { + memset(container, 0, capacity * sizeof(T)); + elements = 0; + } + + void add(T value) + { + if (elements < capacity) { + container[elements] = value; + elements++; + return; + } + + memmove(container, &container[1], (capacity - 1) * sizeof(T)); + container[capacity - 1] = value; + } + + + /** + * Draw Sparkline using passed drawLine function + * + * @param x left-most poont where to start the sparkline + * @param y bottom point of the sparkline – to match font redering + * @param maxWidth max width of the sparkline + * @param maxHeight max height of the sparkline + * @param lineWidth width of the stroke for the line to be drawn + */ + void draw(uint16_t x, uint16_t y, + uint16_t maxWidth, uint16_t maxHeight, + uint16_t lineWidth = 1) const + { + if (elements < 2) { + return; + } + + T lo = this->findMin(); + T hi = this->findMax(); + num_t slope = 1.0f * (maxHeight - lineWidth) / (hi - lo); + + Point lastPoint = { 0, 0 }; + num_t segment = 0.0f; + + size_t maxSegments = elements; + if (maxSegments > maxWidth) { + maxSegments = maxWidth; + } + + num_t pixelPerSegment = 1.0f; + if (elements <= maxWidth) { + pixelPerSegment = 1.0f * maxWidth / (elements - 1); + } + + for (size_t i = 0; i < maxSegments; i++) { + T value = container[i]; + num_t scaledValue = maxHeight - ((value - lo) * slope); + scaledValue -= lineWidth / 2.0f; + scaledValue += y - maxHeight; + + Point pt { + .x = static_cast(x + segment), + .y = static_cast(scaledValue) + }; + + if (segment > 0) { + drawLine(lastPoint.x, lastPoint.y, pt.x, pt.y); + } + + lastPoint = pt; + segment += pixelPerSegment; + } + } +}; + diff --git a/operame.ino b/operame.ino index d755952..f59673e 100644 --- a/operame.ino +++ b/operame.ino @@ -9,6 +9,7 @@ #include #include #include +#include #define LANGUAGE "nl" OperameLanguage::Texts T; @@ -41,6 +42,8 @@ bool add_units; bool wifi_enabled; bool mqtt_enabled; int max_failures; +bool sparkline_enable; +int sparkline_bufferlenght; void retain(const String& topic, const String& message) { Serial.printf("%s %s\n", topic.c_str(), message.c_str()); @@ -54,6 +57,10 @@ void clear_sprite(int bg = TFT_BLACK) { } } +SparkLine display_sparkline(766, [&](const uint16_t x0, const uint16_t y0, const uint16_t x1, const uint16_t y1) { + sprite.drawLine(x0, y0, x1, y1, TFT_PURPLE); +}); + void display_big(const String& text, int fg = TFT_WHITE, int bg = TFT_BLACK) { clear_sprite(bg); sprite.setTextSize(1); @@ -68,6 +75,10 @@ void display_big(const String& text, int fg = TFT_WHITE, int bg = TFT_BLACK) { sprite.setTextColor(fg, bg); sprite.drawString(text, display.width()/2, display.height()/2); + if (sparkline_enable) { + display_sparkline.draw( 10, display.height()/4*3, display.width()-20, display.height()/2-5); + }; + sprite.pushSprite(0, 0); } @@ -140,6 +151,7 @@ void ppm_demo() { int buttoncounter = 0; for (int p = 400; p < 1200; p++) { display_ppm(p); + if (sparkline_enable ) display_sparkline.add(p); if (button(pin_demobutton)) { display_logo(); delay(500); @@ -160,6 +172,7 @@ void ppm_demo() { delay(30); } display_logo(); + if (sparkline_enable ) display_sparkline.reset(); delay(5000); } @@ -392,6 +405,10 @@ void setup() { mqtt_template = WiFiSettings.string("operame_mqtt_template", "{} PPM", T.config_mqtt_template); WiFiSettings.info(T.config_template_info); + WiFiSettings.heading("sparkline"); + sparkline_enable = WiFiSettings.checkbox("operame_sparkline", false, T.config_sparkline); + sparkline_bufferlenght = WiFiSettings.integer("operame_sparkline_buffer", 0, 16383, 512, T.config_sparkline_buffer); ; + WiFiSettings.onConnect = [] { display_big(T.connecting, TFT_BLUE); check_portalbutton(); @@ -433,6 +450,7 @@ void setup() { if (mqtt_enabled) mqtt.begin(server.c_str(), port, wificlient); if (ota_enabled) setup_ota(); + } #define every(t) for (static unsigned long _lasttime; (unsigned long)((unsigned long)millis() - _lasttime) >= (t); _lasttime = millis()) @@ -443,6 +461,7 @@ void loop() { every(5000) { co2 = get_co2(); Serial.println(co2); + if (sparkline_enable ) display_sparkline.add(co2); } every(50) { diff --git a/operame_strings.h b/operame_strings.h index 1370aea..e24d438 100644 --- a/operame_strings.h +++ b/operame_strings.h @@ -26,6 +26,8 @@ struct Texts { *config_mqtt_interval, *config_mqtt_template, *config_template_info, + *config_sparkline, + *config_sparkline_buffer, *connecting, *wait ; @@ -73,6 +75,8 @@ bool select(Texts& T, String language) { T.config_mqtt_interval = "Publication interval [s]"; T.config_mqtt_template = "Message template"; T.config_template_info = "The {} in the template is replaced by the measurement value."; + T.config_sparkline = "Show sparkline graph"; + T.config_sparkline_buffer= "sparkline buffer length"; T.connecting = "Connecting to WiFi..."; T.portal_instructions = { { @@ -139,6 +143,8 @@ bool select(Texts& T, String language) { T.config_mqtt_interval = "Publicatie-interval [s]"; T.config_mqtt_template = "Berichtsjabloon"; T.config_template_info = "De {} in het sjabloon wordt vervangen door de gemeten waarde."; + T.config_sparkline = "Laat grafiek zien"; + T.config_sparkline_buffer= "grafiek buffer lengte"; T.connecting = "Verbinden met WiFi..."; T.portal_instructions = { {