• Система автоматизации с открытым исходным кодом на базе esp8266/esp32 микроконтроллеров и приложения IoT Manager. Наша группа в Telegram

Рисуем графики

Памяти у нас много, флэш тоже. Поэтому мы можем изгаляться как хотим.
Тема урока: графики.
Есть такая штука для рисования графиков - http://www.highcharts.com/
Все достаточно просто, но вот примеров простых(!) не хватает. Все бы им PHP, MySQL, и прочие навороты. Но у нас то этого барахла нет ! А красивый график показаний охота.

А пожалуйста !

В примере 2 типа основных графиков - chart и stock.
Странички в виде констант в PROGMEM.
Так же пример работы ntp, потому как график показаний без реального времени фигня.

Разбираемся, комментируем ...

Код:
#include <FS.h>                   //this needs to be first, or it all crashes and burns...
#include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiUdp.h>
#include <WiFiManager.h>          //https://github.com/tzapu/WiFiManager
#include <time.h>

#define debug true

ESP8266WebServer server(80);
int co2a[60];
long co2t[60];

const char PAGE_stock[] PROGMEM = R"=====(
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <script type="text/javascript" src="//code.jquery.com/jquery-1.9.1.js"></script>
  <title>Stock</title>
<script type='text/javascript'>//<![CDATA[
$(function () {
    $.getJSON('data.json', function (data) {
        $('#container').highcharts('StockChart', {
            rangeSelector : {
                selected : 1
            },
            title : {
                text : 'CO2PPM'
            },
            series : [{
                name : 'CO2:',
                data : data,
                tooltip: {
                    valueDecimals: 0
                }
            }]
        });
    });
});
//]]>
</script>
</head>
<body>
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<script src="https://code.highcharts.com/stock/modules/exporting.js" type="text/javascript"></script>
<div id="container" style="height: 400px; min-width: 310px"></div>
<a href="/chart.html">Chart</a>
</body>
</html>)=====";

const char PAGE_chart[] PROGMEM = R"=====(
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Chart</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script type='text/javascript'>//<![CDATA[
$(document).ready(function() {
    var options = {
        chart: {
            renderTo: 'container',
            type: 'spline'
        },
        title: {
            text: 'CO2'
        },
        xAxis: {
            type: 'datetime'
        },
        yAxis: {
          title: {
            text: 'CO2 (PPM)'
                }
        },
        plotOptions: {
            line: {
                dataLabels: {
                    enabled: true
                },
                enableMouseTracking: true
            }
        },
            series: [{
            type: 'spline',
            name: 'CO2 Sensor data',
            tooltip: {
            valueDecimals: 0,
            valueSuffix: 'PPM'
            }
        }]
    };
    $.getJSON('data.json', function(data) {
        options.series[0].data = data;
        var chart = new Highcharts.Chart(options);
    });
});
//]]>
</script>
</head>
   <body>
<script src="https://code.highcharts.com/stock/highstock.js" type="text/javascript"></script>
<script src="https://code.highcharts.com/stock/modules/exporting.js" type="text/javascript"></script>
<div id="container" style="height: 400px; min-width: 310px"></div>
<a href="/stock.html">Stock</a>
</body>
</html>
)=====";

void wifimanstart() { // Волшебная процедура начального подключения к Wifi.
                      // Если не знает к чему подцепить - создает точку доступа ESP8266 и настроечную таблицу http://192.168.4.1
                      // Подробнее: https://github.com/tzapu/WiFiManager
  WiFiManager wifiManager;
  wifiManager.setDebugOutput(debug);
  wifiManager.setMinimumSignalQuality();
  if (!wifiManager.autoConnect("ESP8266")) {
  if (debug) Serial.println("failed to connect and hit timeout");
    delay(3000);
    //reset and try again, or maybe put it to deep sleep
    ESP.reset();
    delay(5000); }
if (debug) Serial.println("connected...");
}

void processCSV()
{       
    String str = "[\n";
    for (int i=0; i<=59; i++) {
      if ((i>0)&&(String(co2t[i])!="0")) str +=",\n";
      if (String(co2t[i])!="0") str += "["+String(co2t[i])+"000,"+String(co2a[i])+"]";}
      str +="\n]";
    server.send ( 200, "application/json", str);  
}

void ServerInit() {
  server.on ( "/", []() { server.send ( 200, "text/html", PAGE_chart ); });
  server.on ( "/chart.html", []() { server.send ( 200, "text/html", PAGE_chart ); });
  server.on ( "/stock.html", []() { server.send ( 200, "text/html", PAGE_stock ); });
  server.on ( "/data.json", processCSV  );
     server.onNotFound ( []() { Serial.println("Page Not Found"); server.send ( 400, "text/html", "Page not Found" );   }  );
    server.begin();
}

int timezone = 3;
int dst = 0;
time_t now;

void setup() {
  Serial.begin(115200);
  wifimanstart();
  configTime(timezone * 3600, 0, "pool.ntp.org", "time.nist.gov");
    if (debug) Serial.print("\nWaiting for time");
    while (!time(nullptr)) { if (debug) Serial.print("."); delay(200);} if (debug) Serial.println("");
  now = time(nullptr);
  Serial.print("Unix time:");Serial.println(now);
  Serial.println(ctime(&now));
  for (int i=0; i<=59; i++) {co2t[i]=(now-3600)+(60*i); co2a[i]=random(100,1000);} //co2t - datetime измерения в формате unixtime, co2a - значение измерения

  ServerInit();
  Serial.print("http://"); Serial.print(WiFi.localIP());Serial.println("/");
 
}
  void loop() {
 
    server.handleClient();
    yield();

  }
 

romanko

New member
Не всегда есть возможность использовать сторонние js, например нет доступа к интернету.
Использую http://dygraphs.com/
Если использовать кеширование и сжатие, то можно обойтись средствами контролера esp8266
Кеширование скрипта
Код:
server.serveStatic("/dygraph-combined.js",   SPIFFS, "/dygraph-combined.js"  ,"max-age=86400");
Файл dygraph-combined.js сжимаем gz, получается 37 кБт - время считывания файла первый раз около секунды потом берется из кеша браузера
Данные тоже пишу в файл флеш памяти.
Код:
f = SPIFFS.open(datafile, "a");
........
f.println(str);
Посуточный графики за неделю с шагом 5 минут помещаются в флеш памяти без проблем.
 
Не всегда есть возможность использовать сторонние js, например нет доступа к интернету.
Использую http://dygraphs.com/
...
Посуточный графики за неделю с шагом 5 минут помещаются в флеш памяти без проблем.
Пример в студию. :)

Ну у меня комплект js скриптов в 100К помещается, тоже можно на ESP-шку сложить. И тоже будет локально работать.
Мой пример сделан для того чтобы можно его было скопипастить, скомпилить и зашить. И чтобы сразу заработало :)

В основном проекте, где эти графики используются вообще идет выгрузка в MySQL, и обрабатывается на сервере. Но для отладки захотелось онлайн график непосредственно с железяки. Разобрался не сразу. Поэтому выложил рабочий пример. Может поможет кому.
 

romanko

New member
Это функция отдачи с сервера страницы с графиком -
Код:
void setup(void){
......
server.on ( "/graph.htm", []() {
if (server.args() > 0 ) getday=server.arg(0).toInt(); else getday=0;
server.send ( 200, "text/html", PAGE_Graph(getday));
} );
.....
}


String PAGE_Graph(int gd=0) {
    String begin, fname, end, result;
    int selday;
    if (DateTime.wday > gd) {
     selday=DateTime.wday-gd;
    } else {
     selday=DateTime.wday+7-gd;   
    }
    begin="<!DOCTYPE html><html><head><meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\"><meta name=\"viewport\" content=\"width=device-width\"/><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"/><title>Графік</title><script type=\"text/javascript\" src=\"dygraph-combined.js\"></script></head><body><div style=\"max-width:650px;\" class=\"btn btn--m\"><a class=\"wday\" href=\"/\"><< Home</a> <a class=\"wday\" href=\"/graph.htm?d=5\">-5днів</a> <a class=\"wday\" href=\"/graph.htm?d=4\">-4дня</a> <a class=\"wday\" href=\"/graph.htm?d=3\">-3дня</a> <a class=\"wday\" href=\"/graph.htm?d=2\">-2дня</a> <a class=\"wday\" href=\"/graph.htm?d=1\">Вчора</a> <a class=\"wday\" href=\"/graph.htm\">Сьогодні</a></div><br><div id=\"graphdiv2\" style=\"max-width:650px; width:100%; height:300px;\"></div><br><div>Power=(Вимкнено-0/Увімкнено-5); Auto=(Ручний-0/Автоматичний режим-10)</div><script type=\"text/javascript\"> g2 = new Dygraph(document.getElementById(\"graphdiv2\"), \"gr_";
    fname=(String)selday+".txt";
    end="\", {series: {Zavd: {stepPlot: true}, Temp: {stepPlot: false}, Power: {stepPlot: true}, Auto: {stepPlot: true}, Kotel: {stepPlot: true}}, fillGraph: false, stackedGraph: false, includeZero: true} ); </script></body></html>";
    result = begin+fname+end;
    return result;
}
это запись данных в файл данных для рисования графика
Код:
    String datafile="/gr_"+ (String)DateTime.wday+".txt";    
    if (!SPIFFS.exists(datafile)) {
        String grt="";
        f = SPIFFS.open(datafile, "w");
        if (config.pwm_stan) grt=",Kotel";
        f.println("Datetime,Auto,Power,Zavd,Temp"+grt);
    } else {
        f = SPIFFS.open(datafile, "a");
        String send_pow="0", send_avt="0";
        String str =FullDate()+" "+FullTime()+",";
        if (config.automat) send_avt="10";
        if (config.power) send_pow="5";
        str += send_avt+","+send_pow+",";
        str +=(String)zavd_temp+","+(String)real_temp;
        if (config.pwm_stan) str+=","+(String)(real_pwm*10+30);
        f.println(str);
    }
    f.close();
Это куски кода с рабочего проекта. Плюс кеширование и сжатие файла библиотеки /dygraph-combined.js описывал выше.
Решил пойти таким путем как раз что б отказаться от использования сторонних серверов.
 

pvvx

Активный участник сообщества
Озадачившись информативными графиками на автономном устройстве, после долгих переборов вариантов, вышло что для AJAX проще использовать генерацию XML и отображение к примеру с помощью уже готовых примитивов типа от fusioncharts, загружаемых с устройства.
Вот их примеры: http://www.fusioncharts.com/charts/msline_1/
Генераторы XML по скриптам/шаблонам очень легко встраиваются в устройство. Примеры уже есть в Web-свалке.
 

pvvx

Активный участник сообщества
Рисует графики в реальном времени.
А осциллограф на java или ещё как есть? Web у меня дает UDP поток со встроенного ADC в ESP до 192 кГц, но как отобразить на странице?
Web-flash бы какой для примеру с осциллографом в реал-тайм... а формат потока данных изменю без проблем. Java ужасно тормозит...
На Ардуине никаких приличных Web не сделать - там ужасная и тормозная файловая система Spiffs. :(
Ещё очень хотчется решение на java или другим путем загрузки со встроенного в esp-web отображения 3D датчиков. Сейчас сделал драйвер в ESP для 10-DOF (акселерометр, гироскоп, магнитометр, термометр, давление) с дополнительными хорошими датчиками влажности, освещенности, ИФК, кислородом... для ESP8266 с дискретностью в 100Гц на все точки и требуется отображение всего этого в реальном времени и 3D, а так-же статистики (усреднений по ним) по циклу за год с шагом точек на 5..15 минут, сохраняемыми в ESP с flash с 16 Мегабайт. :)
 
Последнее редактирование:

LeshaEzGaming

New member
Для графиков в реальном времени как аналог из примера посоветовал бы smoothiecharts. Сжатая Google Closure Compiler'ом весит 8.5кбайт, в gz 2.8кб.
 

pvvx

Активный участник сообщества
Для графиков в реальном времени как аналог из примера посоветовал бы smoothiecharts. Сжатая Google Closure Compiler'ом весит 8.5кбайт, в gz 2.8кб.
ina219grf.gif
При менее 10 ms на точку возникают какие-то странности... квадратные... А модуль ESP8266 (web-свалка) через websocket дает точки с шагом в 1..2 ms.
Ещё скачет масштабирование по всему телу графика. Надо переделывать автоустановку min/max в smoothie.js...

Попробовал без задержек:
Ina219_0.5Hz.gif
Клетка графика 100 ms, с генератора на INA219 идет синус 0.5Гц.
Модуль ESP-01 опрашивает датчик INA219 пo I2C по одному значению тока или напряжения через WebSocket в текстовом виде. За секунду по соединению проходит 510 опросов значений (отображено как samplerate). Вывод графика тормозит опрос в javascript - без графика опрос через WebSocket значений от датчика больше 850 раз в сек (упирается в скорость I2С)... :(
:
Никакой оптимизации, чисто для теста скорости websocket и smoothie.js
JavaScript:
<script type="text/javascript">
var line1 = new TimeSeries();
var line2 = new TimeSeries();
var smoothie1 = new SmoothieChart({ millisPerPixel: 3, maxValueScale:1.05,
  grid: { strokeStyle:'rgb(75, 75, 75)', lineWidth: 1, millisPerLine: 100, verticalSections: 6 },
  labels: { fillStyle:'rgb(255, 255, 255)' }
});
var smoothie2 = new SmoothieChart({ millisPerPixel: 3, maxValueScale:1.05,
  grid: { strokeStyle:'rgb(75, 75, 75)', lineWidth: 1, millisPerLine: 100, verticalSections: 6 },
  labels: { fillStyle:'rgb(255, 255, 255)' }
});
smoothie1.addTimeSeries(line1, { strokeStyle:'rgb(0, 255, 0)',  lineWidth: 2 });
smoothie2.addTimeSeries(line2, { strokeStyle:'rgb(255, 0, 255)', lineWidth: 2 });
smoothie1.streamTo(document.getElementById("mycanvas1"));
smoothie2.streamTo(document.getElementById("mycanvas2"));
var uicnt = 0;
var smprate = 0;
function onMessage(evt) {
    smprate++;
    if (evt.data) {
       if(uicnt) {
            newval = (eval(evt.data)*0.0964).toFixed(2);    
            line2.append(new Date().getTime(), newval);
            document.getElementById('xdata2').innerHTML = newval;
        }
        else {
            newval = (eval(evt.data)*0.000519).toFixed(4);    
            line1.append(new Date().getTime(), newval);
            document.getElementById('xdata1').innerHTML = newval;
        }
    }
    wsSendMsg();
}
var wsUri = "ws://192.168.4.1/web.cgi";
// var wsUri = "ws://"+window.location.host.toString()+"/web.cgi";
function testWebSocket() {
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) {onOpen(evt)};
    websocket.onmessage = function(evt) {onMessage(evt)};
}
function wsSendMsg() {
    if(uicnt) {uicnt = 0; websocket.send("ovl$3"); }
    else {uicnt = 1; websocket.send("ovl$4"); }
}
function onOpen(evt) {
    websocket.send("ovl$=2");
    wsSendMsg();
}
var wstt;
function wsSmpRate() {
    document.getElementById('smprate').innerHTML = smprate;
    smprate = 0;
    wstt = setTimeout(wsSmpRate, 1000);
}
wstt = setTimeout(wsSmpRate, 1000);
testWebSocket();
</script>
Примерно так это выглядит:
 
Последнее редактирование:
Сверху Снизу