Vykresľovanie sviečkového grafu v reálnom čase

Zápisník experimentátora

Hierarchy: Sviečkové grafy

V dnešnom pokračovaní seriálu si naprogramujeme triedu, ktorá bude vykresľovať sviečkové grafy na displeji Nokia 5110. Bude vykresľovať údaje v reálnom čase a ako zdroj údajov nám poslúži modifikovaný kód z prvého blogu seriálu. Triedu si naprogramujeme pomocou šablón a bude sa skladať z jednej triedy, ktorá sa bude starať o zber údajov a tri triedy sa budú starať o vykresľovanie rôznymi spôsobmi.

Čaká nás síce zložitejší kód, v ktorom budeme používať C++ šablóny, ale výsledkom budú nádherné grafy, ktoré sa zobrazia na displeji Nokia 5110. Šablóny sa používajú najmä preto, aby mohol kompilátor optimalizovať kód pre konkrétny displej a nemusel zbytočne do programu zahrnúť riadky, ktoré by sa vo výsledku nepoužili.

Z priložených obrázkov je zrejmé, ako bude výsledok vyzerať. Budeme používať tri rôzne spôsoby zobrazenia:

  • Stĺpcový graf.
  • Čiarový graf.
  • Výstup na sériový port.

Schéma zapojenia sa v ničom neodlišuje od predchádzajúcich zapojení s displejom Nokia 5110, kde sme nepotrebovali pripojiť žiadne ďalšie súčiastky. Iba Arduino, level shifter a displej Nokia 5110.

Program

Budeme programovať dva zodrojové kódy:

  • ohlc_nokia_random.ino - Kód hlavného programu. Tu je toho relatívne málo na pozeranie, pretože väčšina kódu tu iba zabezpečuje vytváranie súvislého prúdu náhodných údajov. A je tam aj pár riadkov navyše ku vykresľovanie, pretože na grafe máme aj pomocnú mierku, poslednú hodnotu v grafe a indikátor aktuálnej poslednej hodnoty.
  • candlestick.h - Tu je naprogramovaná celá logika zberu údajov a ich vykresľovania.

ohlc_nokia_random.ino

Vysvetlenie hlavného programu.

#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
#include "candlestick.h"

Tieto hlavičkové súbory potrebujeme na to, aby nám program fungoval. Dve knižnice od Adafruit, ktoré si nainštalujete v správcovi knižníc. Tretí súbor je náš kód, ktorý sa nachádza v tom istom adresári.

Adafruit_PCD8544 display = Adafruit_PCD8544(7, 6, 5, 4, 3);

// 12 bars, 10 sec/bar, bar renderer
OHLCChart<int, 12, 10000, OHLCNokia5110BarRender<int> > ohlc;

int value=24;

void setup() {
  Serial.begin(9600);

  // connect renderer to Nokia 5110
  ohlc.getRender().setDisplay(&display);

  // Nokia 5110 init
  display.begin();
  display.setContrast(60);
  display.clearDisplay();
  display.display();
}

Definícia objektu displeja. Definícia šablónového objektu ohlc, ktorý je typu OHLCChart. Tie záhadné konštrukcie v zátvorkách predstavujú parametre šablóny. Jednotlivé parametre majú nasledovný význam. Pozor na posledný parameter, tá medzera tam naozaj musí byť.

  • int - OHLC údaje sú ukladané ako celé čísla. Pretože náhodné údaje sú generované v rozsahu 0-47, čo zodpovedá výške displeja, používame v grafe celé čísla. V prípade potreby by sme mohli použiť aj typ float, šablóna sa postará o vygenerovanie správneho kódu aj pre tento typ.
  • 12 - Šírka displeja je 84 pixelov, čo nám umožňuje naraz zobraziť 12 stĺpcov histórie OHLC.
  • 10000 - Jeden stĺpec zaznamenáva časové obdobie 10 s. Celý graf preto zobrazí časové obdobie 120 s. Čo je viac, než nám poskytuje šírka displeja pre normálne zobrazenie generovaných hodnôt. Ak by sme natiahli jeden stĺpec napríklad na hodinu, celý graf by zobrazoval 12 hodín. A to všetko dosiahneme iba nastavením jedného parametra. O zvyšok sa postará šablóna.
  • OHLCNokia5110BarRender<int> - Toto je možno najkomplikovanejšia časť na pochopenie. Takto si zadefinujeme, že chceme na vykreslenie použiť šablónovú triedu OHLCNokia5110BarRenderer, ktorá bude OHLC interpretovať ako celé čísla. Aby to fungovalo, musí byť aj prvý parameter aj tento parameter rovnakého typu int. V tomto príklade som naprogramoval tri renderery.
    • OHLCNokia5110BarRenderer - Výsledok vidíte na prvom obrázku.
    • OHLCNokia5110LineRenderer - Výsledok vidíte na druhom obrázku.
    • OHLCSerialRenderer - OHLC dáta zasiela na sériový port do počítača. Toto mi slúžilo počas vývoja programu na sledovanie, či správne vytváram OHLC.

Posledný zaujímavý riadok je ohlc.getRender().setDisplay(&display);. Pomocou neho sa napojí renderer na premennú display, aby vykresľoval údaje priamo na displej Nokia 5110.

void loop() {
  // update random data
  int valuemove=random(3)-1;
  value+=valuemove;
  value=constrain(value,0,47);
  //Serial.println(value);
  ohlc.addValue(value);

  // render chart
  display.clearDisplay();
  // draw candlesticks
  ohlc.draw();
  // draw scale
  for(int j=0;j<=84;j+=2)
    display.drawLine(j,0,j,(j%10==0 ? 2 : 1),BLACK);
  for(int j=0;j<=38;j+=2)
    display.drawLine(0,47-j,(j%10==0 ? 2 : 1),47-j,BLACK);
  // draw last value
  display.fillRect(5,5,15,11,BLACK);
  display.fillRect(6,6,13,9,WHITE);
  display.setCursor(7,7);
  display.print(value);
  // draw indicator
  display.drawLine(82,47-value,83,47-value,BLACK);
  display.drawLine(83,47-value+1,83,47-value-1,BLACK);
  display.display();
  
  delay(200); // 5 valus/second
}

Funkcia loop je dobre komentovaná v zdrojových textoch. V prvej časti generuje náhodné údaje a v druhej časti vykreslí všetky potrebné objekty na displej. Pre nás je podstatný riadok ohlc.draw(), ktorý žiada našu šablónu o vykreslenie grafu. Ostatné riadky okolo iba dopĺňajú údaje na displej, ktoré samotný sviečkový graf nevie vykresľovať.

candlestick.h

V tomto hlavičkovom súbore je toho na vysvetlenia viac.

///
/// One OHLC bar data
///
template<typename ohlcvalue>
struct OHLCData {
  ohlcvalue o;
  ohlcvalue h;
  ohlcvalue l;
  ohlcvalue c;  
};

Prvá šablóna nám definuje štruktúru OHLCData. V predchádzajúcom texte sme si vysvetlili, že ako parameter šablóny bude int. Šablóna sa teda pri kompilácii programu zmení tak, že všetky slová ohlcvalue sa zmenia na slová int.

///
/// Serial port renderer
///
template<typename ohlcvalue>
class OHLCSerialRender {
public:
  void drawHeader() {
    Serial.println("###");
    }
  void drawBar(int pos, OHLCData<ohlcvalue> *value) {
    Serial.print(pos);
    Serial.print(",");
    Serial.print(value->o);
    Serial.print(",");
    Serial.print(value->h);
    Serial.print(",");
    Serial.print(value->l);
    Serial.print(",");
    Serial.print(value->c);
    Serial.println("");
    }
  void drawFooter() {}
};

Toto je prvá trieda, ktorá vykresľuje údaje na sériová port. Aby ju bolo možné použiť, musí mať tri funkcie drawHeader, drawBar a drawFooter. Nie všetky funkcie musia mať aj vyplnené telo, podstatné je len to, aby tam boli a kompilátor ich mohol bez problémov využiť. Samotné využitie nájdete v tomto istom súbore vo funkcii OHLCChart::draw.

///
/// Nokia 5110 Line Candlestick renderer
///
template<typename ohlcvalue>
class OHLCNokia5110LineRender {
  Adafruit_PCD8544 *display;
public:
  OHLCNokia5110LineRender() : display(NULL) {}
  void drawHeader() {
    }
  void drawBar(int pos, OHLCData<ohlcvalue> *bar) {
    if(display==NULL)
      return;
      
    int START = 5 + pos * 6;
    const int BW = 2;
    const int HEIGHT = 47;

    // open
    display->drawLine(START,HEIGHT-bar->o,START+1*BW,HEIGHT-bar->o,BLACK);

    // high - low
    display->drawLine(START+1*BW,HEIGHT-bar->h,START+1*BW,HEIGHT-bar->l,BLACK);

    // close
    display->drawLine(START+1*BW,HEIGHT-bar->c,START+2*BW,HEIGHT-bar->c,BLACK);
    }
  void drawFooter() {}
  void setDisplay(Adafruit_PCD8544 *d) {display=d;}
};

Toto je druhá trieda, ktorá vykresľuje údaje v podobe čiar. Je skoro kompletne prevzatá z predchádzajúceho blogu, iba sa v nej mierne zmenili názvy parametrov. Opäť má tri funkcie, ktoré robia niečo iné, než predchádzajúca trieda. Kreslí pomocou pointra display, ktorý sme jej nastavili v hlavnom programe volaním funkcie setDisplay.

///
/// Nokia 5110 Bar Candlestick renderer
///
template<typename ohlcvalue>
class OHLCNokia5110BarRender {
  Adafruit_PCD8544 *display;
public:
  OHLCNokia5110BarRender() : display(NULL) {}
  void drawHeader() {
    }
  void drawBar(int pos, OHLCData<ohlcvalue> *bar) {
    if(display==NULL)
      return;
      
    int START = 5 + pos * 6;
    const int BW = 2;
    const int HEIGHT = 47;
    int top;
    int bottom;

    if(bar->o < bar->c) {
      top = bar->c;
      bottom = bar->o;
    } else {
      top = bar->o;
      bottom = bar->c;
    }

    // high
    display->drawLine(START+1*BW,HEIGHT-bar->h,START+1*BW,HEIGHT-top,BLACK);

    // low
    display->drawLine(START+1*BW,HEIGHT-bar->l,START+1*BW,HEIGHT-bottom,BLACK);

    // bar
    if(bar->o < bar->c) 
      display->drawRect(START,HEIGHT-bottom,2*BW+1,bottom-top+1,BLACK);
    else
      display->fillRect(START,HEIGHT-bottom,2*BW+1,bottom-top+1,BLACK);
    }
  void drawFooter() {}
  void setDisplay(Adafruit_PCD8544 *d) {display=d;}
};

Tretia trieda sa líši od predchádzajúcej iba grafikou.

///
/// OHLC recorder and renderer
/// ohlcvalue - int or float
/// len - number of bars
/// stp - time window (10000 = 10 seconds)
/// renderclass - which renderer we want to use
///
template<typename ohlcvalue, const int len, const int stp, typename renderclass>
class OHLCChart {
  OHLCData<ohlcvalue> data[len];
  const int ohlc_step;
  int ohlc_first;
  OHLCData<ohlcvalue> *last;
  unsigned long ohlc_next_step;
  renderclass render;
  
public:
  OHLCChart() : ohlc_step(stp), ohlc_first(len), ohlc_next_step(0) {
    last = data + (len - 1);
  }

  renderclass& getRender() {
    return render;
  }
  
  boolean addValue(ohlcvalue v) {
    unsigned long m=millis();
    if(m>ohlc_next_step) {
      if(ohlc_first>0)
        ohlc_first--;
      for(int i=0;i<(len-1);i++)
        data[i] = data[i+1];
      last->o = v;
      last->h = v;
      last->l = v;
      last->c = v;
      ohlc_next_step+=ohlc_step;
      return true;
    } else {
      if(last->h<v)
        last->h=v;
      if(last->l>v)
        last->l=v;
      last->c=v;
    }
  return false;
  }

  void draw() {
    render.drawHeader();
    for(int i=ohlc_first;i<len;i++)
      render.drawBar(i,data+i);
    render.drawFooter();
  }
};

Posledná trieda OHLCChart je čerešnićkou na torte, ktorá pod sebou zapúzdruje všetky predchádzajúce triedy. Ako sa to robí, je vidno hneď na začiatku triedy. Parametre šablóny kompilátor upraví tak, ako sme mu to predpísali v hlavnom programe. V konštruktore nastavíme niekoľko hodnôt do interných premenných. Vo funkcii addValue sa staráme o spracovanie prichádzajúceho prúdu hodnôt. Každých 10 sekund ich posunieme do ďalšieho OHLCData v poli data a do posledného si zbierame aktuálne hodnoty. Posledná funkcia draw sa postará o zavolanie renderera pre každú jednu hodnotu OHLCData. Používame premennú ohlc_first, ktorá sa stará o sledovanie počiatku validných údajov a po niekoľkých iteráciách sa posunie na hodnotu 0, čo predstavuje prvý prvok v poli data.

Zdrojový kód

Zdrojový kód sa nachádza na GitHub.

Video

Video obshuje doplňujúce vysvetlenie ku sviečkovým grafom a príklady v texte spomínaných grafov.

Pokračovanie

Nabudúce budeme zobrazovať pomocnú mierku a pohráme sa trochu s dedičnosťou v šablónovom metaprogramovaní.


07.08.2016


Menu