Pojďme programovat elektroniku | Tablety

Pojďme programovat elektroniku: Vyrobíme si vlastní ajPed

  • Vyrobíme si maličký tablet
  • Bude moci kreslit barevnými štětci
  • A vše se bude živě přenášet do webového prohlížeče

Loni v létě jsme si v našem seriálu vyzkoušeli práci s 2,4“ TFT LCD, na který jsme pomocí základního Arduina Una vykreslovali několik údajů z připojených čidel. Postavili jsme si tedy vlastně jednoduchou meteostanici s vlastním displejem.

2,8" dotykový TFT LCD

Po půlroce se k displeji vrátíme, ovšem k poněkud lepšímu modelu, tentokrát totiž půjde o 2,8“ cvalíka s rozlišením 320×240 pixelů a rezistivní dotykovou vrstvou. Na čínských e-shopech jej pořídíte za částku okolo 150 korun. Tyto displeje mají zároveň na zadní straně i samostatný obvod a pouzdro pro SD kartu, takže displej může mikrokontroleru poskytnout funkci rozšířeného úložiště, ze kterého může řídící čip načítat třeba rozměrnější grafiku.

7ab42edc-20ff-4ceb-9dd5-b1e36fba4ac6ed0f5c58-9fa5-432b-b3d1-f974ee4b7f458b73a112-6fcb-45b5-8ca7-ec6f3c722237
2,8" TFT LCD s rezistivní dotykovou vrstvou, obvodem čtečky SD karet a samostatnými komunikačními rozhraními SPI pro všechny tyto tři funkce

Namísto primitivního Arduina Una tentokrát jako základní desku zvolíme NodeMCU s čipem ESP8266, který má nejen mnohem větší flashové úložiště, ale také RAM v řádu mnoha desítek kB. To je ve srovnání s 2 kB na čipech ATmega328p naprostý luxus. Čipy ESP8266 ale mají především 2,4GHz Wi-Fi vysílač, kterým se připojíme k domácí síti. Cena destiček NodeMCU se na eBayi pohybuje okolo 70 korun.

1ffd6d63-1741-47c8-8498-e7863166d365
Prototypovací destička NodeMCu „v3“ s řídícím čipem ESP8266

Vyrobíme si malý ajPed

Dnes si pomocí těchto dvou součástek postavíme maličkou parodii na tablet. Bude se proto jmenovat ajPed. Když se spustí, v horní části obrazovky se zobrazí jeho textové logo a řada sedmi tlačítek. První bude sloužit ke smazání obrazovky a ty další k výběru barvy štětce.

Součástí balení s displejem je totiž i malý, plastový, a především bílý stylus, který použijeme jako kreslící pero. Vyrobíme si tedy primitivní kreslítko, kterému, aby mělo nějakou formu, vyrobíme na míru krabičku z plošných perforovaných dílů skvělé litevsko-norské stavebnice Totem.

P3090005.JPGP3090008.JPGP3090010.JPG
P3090018.JPGP3090017.JPGP3090020.JPG
Základní díly litevsko-norské stavebnice Totem

Totem se podobá tradiční české stavebnici Merkur, jeho díly jsou však z černého ABS, který se dá snáze řezat, a tvůrci se specializují na to, abyste ke stavebnici snadno přimontovali třeba desku Arduina, Raspberry Pi a další. Všechny otvory mají rozměr vhodný pro šrouby M3 a součástí stavebnice je i nespočet všemožných hliníkových pojících dílů.

P3090028.JPGa61bc2f0-ce7b-40ef-9e24-a68ccac6dee3P3090027.JPG
A je hotovo

Na stranu druhou, za tuto legraci si připlatíte, evropský Totem je totiž pochopitelně mnohem dražší než obvyklé čínské cetky z AliExpressu. Při svých cestách internetem jsem ale vyjma zmíněného Merkuru nenašel nic srovnatelného.

Streamování do prohlížeče

V každém případě, náš ajPed bude díky čipu ESP8266 připojený do internetu, čehož musíme využít. Přemýšlel jsem jak, až jsem si řekl, že v éře streamování téměř všeho bude streamovat i náš ajPed. A co vlastně? A jak?

Bude streamovat veškeré dění na obrazovce do webového prohlížeče, ve kterém načtete jeho webovou adresu. Při startu se na něm totiž spustí primitivní HTTP server na standardním TCP portu 80, ale zároveň server pro komunikaci na protokolu WebSocket a portu 81.

Video: Podívejte se, co umí ajPed:

WebSocket je skvělá technologie, které dnes rozumějí prakticky všechny hlavní webové prohlížeče s podporou HTML5. Umožňuje obousměrnou komunikaci na jednom TCP spojení, takže když se webový prohlížeč spojí s naším ajPedem, ten mu může průběžně zasílat data, aniž bychom museli v Javascriptu používat techniku klasického AJAXu a periodicky si data vynucovat. Když nějaká budou, prostě sama dorazí.

Když tedy začnu perem čmárat po displeji, souřadnice dotyků, jak je rozpoznala rezistivní vrstva, se budou skrze WebSocket zasílat do prohlížeče, kde bude jednoduchý Javascript podle souřadnic a s pomocí technologie HTML5 Canvas kreslit to samé.

d1e304eb-2c96-49c4-ac3a-98eb9f8f260c
Co nakreslím na displej ajPedu, se okamžitě dokreslí i ve webovém prohlížeči. O synchronizaci s minimální latencí se stará technologie WebSocket.

Díky rychlosti a minimální režii WebSocketu bude přenos velmi svižný a prakticky bez jakékoliv viditelné latence. Bude to opravdu vypadat, jako by byl ajPed připojený k počítači USB kabelem a choval se jako běžné HID zařízení – třeba zrovna grafický tablet.

e7acd918-2b0c-4e08-8963-ba2fe9ddf5cf3ba0aab2-06c0-4cc9-8117-f0974456639ecbf1aefd-c3a6-4bc7-bb0d-3a7c94659216
6dbb924b-9b57-49a1-95ad-2d3d40700f80da008890-5c04-4d2a-bf98-efeccdf976b2c148b16c-5521-42b4-ac50-b55bfc7b0a86
Po startu se ajPed pokusí připojit k Wi-Fi. Pokud se mu to podaří, cokoliv nakreslíte, se bude živě streamovat do webového prohlížeče.

Sběrnice SPI: hromada kablíků, hromada knihoven

Fajn, co k tomu všemu budeme potřebovat? Knihovny. 2,8“ displej pro komunikaci používá sběrnici SPI a několik dalších pomocných pinů. Dotyková vrstva má samostatné ovládání také skrze SPI a několik dalších pinů, takže na straně displeje budu muset zapojit dohromady 14 vodičů! Díky tomu, že displej i vrstva používají sběrnici SPI, mohu sice několik vodičů propojit dohromady, i tak ale na straně desky NodeMCU zaberou téměř všechny jeho použitelné piny.

4e17516c-b40e-43b9-b799-c907c638bbdd
Jedno z možných propojení NodeMCU a displeje. V mém zdrojovém kódu je pin displeje RESET připojený na pin desky 3, jinak je vše stejné. Podobně bychom mohli alternativně zapojit pin LED a programově zapínat/vypínat podsvícení. Bylo by to bezpečné, podsvícení je totiž napájené přes VCC a nikoliv přímo tímto pinem, který slouží jen k nastavení logického stavu.

Displej k vlastnímu chodu používá čip ILI9341 a dotyková vrstva pak čip XPT2046, takže bych mohl buď několik dnů zabít četbou dokumentace, jak se tyto čipy skrze sběrnici SPI programují, aby se na displeji zobrazilo třeba červené srdíčko, anebo se poohlédnu na internetu po nějaké hotové knihovně pro Arduino a další podporované desky.

S ILI9341 si velmi dobře rozumí třeba knihovna od Adafruitu, která je určená i pro desky s mikrokontrolerem ESP8266, a o čip dotykové vrstvy XPT2046/ADS7843 se zase postará knihovna od Spirose Papadimitriua (snad jsem to napsal správně).

Spolupráce obou knihoven na destičce NodeMCU mě však hodně zlobila, a tak jsem se nakonec inspiroval na webu nailbuster.com, kde najdete funkční demo a zapojení právě pro tuto desku. A co je nejdůležitější, autor se zde pochlubil i mírně upravenou knihovnou pro čip ILI9341, kterou jsem nakonec použil namísto té původní od Adafruitu.

P3090032.JPGP3090031.JPG
P3090033.JPGP3090029.JPGP3090035.JPG
Připojením displeje na NodeMCU obsadíme prakticky všechny jeho piny GPIO

Do třetice ještě jedna knihovna. Zatímco ty předchozí se postarají o to, aby se displej a dotyková vrstva vůbec rozjely, ještě potřebujeme nějakou grafickou knihovnu, pomocí které budeme na displej třeba psát text rastrovými fonty, kreslit základní tvary a tak dále, aniž bychom si pro to všechno museli psát vlastní rutiny.

K tomu slouží jedna z nejrozšířenějších knihoven světa Arduino – Adafruit GFX. Dnes s ní pracují nejen podobné LCD displeje, ale také OLED a další. A právě díky této knihovně snadno nakreslíte bod, čáru nebo třeba čtverec vyplněný barvou.

Málem bych zapomněl zmínit ještě čtvrtou klíčovou knihovnu, která není součástí základního balení – Arduino WebSockets pro samotné zprovoznění spojení skrze websocketovou linku.

Kalibrace

Tak, knihovny bychom měli, takže už zbývá jen jedna věc – kalibrace. Ta je v tomto konkrétním případě naprosto klíčová, bez ní totiž nedokážeme namapovat dotyk se souřadnicovým systémem samotného displeje. Knihovna XPT2046 od Spirose (příjmení už raději znovu psát nebudu) na to pamatuje samostatným programem XPTCalibrate, přičemž my použijeme jeho upravenou verzi pro NodeMCU a ILI9341 opět od nailbuster.com – vše je součástí výše odkazovaného ZIP balíčku.

Když tento program nahrajeme do čipu, na displeji se zobrazí nejprve křížek v levém horním rohu a pak v pravém dolním. Stačí na ně klepnout a zaznamenat si číselné hodnoty dotyků, které nyní budeme používat v každém dalším a už vlastním programu. Kalibraci tedy stačí provést jen jednou.

0d54e4bc-4a0a-4acd-85dc-b6a69d39efdad38d4526-0fe4-455d-af75-ef212826f865
Kalibrační program pomůže propojit signály z dotykové vrstvy se souřadnicovým systémem displeje pod ní

Když tyto hodnoty použijeme v kódu našeho programu při inicializaci dotykové vrstvy, po klepnutí na displej získáme rovnou souřadnice X a Y v pixelech a nikoliv surové číselné hodnoty z rezistivní vrstvy.

Pak už jen stačí pomocí knihovny Adafruit GFX stvořit uživatelské rozhraní našeho ajPedu, spustit servery HTTP a WebSocket a začít kreslit. Jak na to, se dozvíte v kódech níže, ve kterých jsem každý klíčový krok jako vždy podrobně okomentoval.

A konečně zdrojové kódy

Kódy jsou dnes dva. V prostředí Arduino jsem totiž vedle hlavního zdrojového souboru vytvořil ještě druhý jménem html.h. V něm je uložený samotný HTML/JS kód, který HTTP server předá webovému prohlížeči a ve kterém se pomocí Javascriptu otevře spojení se serverem WebSocket na portu 81 a podle jeho instrukcí se bude vykreslovat kopie malůvky na dotykovém displeji.

Jelikož je HTML kód včetně JavaScriptu docela rozměrný, vůbec se nenačte do RAM čipu ESP8266, ale server jej prohlížeči pošle přímo z rozměrné 4MB flashové paměti, ve které je uložený samotný program. Poslouží k tomu makro PROGMEM (program memory).

A na HTML kódu si demonstrujeme ještě jednu specialitu, kterou přináší jazyk C++ verze 11. Když totiž v C/C++ potřebujete zapsat jako proměnnou nějaký složitý a dlouhý text, namísto zápisu třeba:

char textovepole[] = "Toto je muj dlouhy text";

Ve kterém například nemůžete jen tak použít uvozovky, protože uvozují obsah samotné proměnné, a tak je musíte zapsat pomocí escape sekvence \" – tak tedy namísto tohoto obvyklého zápisu můžete v C++ použít i tuto formu:

char textovepole[] = R"dlouhytext(

jakykoliv textovy obsah vcetne uvozovek,
zalomeni radku a tak dále a tak dale

)dlouhytext";

Tímto způsobem jsem si tedy v úplně jiném editoru připravil svůj HTML/JS kód a pak jej pouze zkopíroval do druhého hlavičkového souboru, který nesmím zapomenout odkázat v hlavním kódu, a do tohoto zápisu, aniž bych text musel jakkoliv upravovat.

Zdrojový kód hlavního programu:

#include <SPI.h>
// https://nailbuster.com/?page_id=341
// http://nailbuster.com/nailcode/tft28esp.zip
#include <Adafruit_ILI9341esp.h>
#include <Adafruit_GFX.h>
// https://github.com/spapadim/XPT2046
#include <XPT2046.h>
#include <Fonts/FreeSerifItalic9pt7b.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
// https://github.com/Links2004/arduinoWebSockets
#include <WebSocketsServer.h>
#include <vector>
#include "html.h"

// SPI CS a pomocne piny LCD
#define LCD_DC 2
#define LCD_CS 15
#define LCD_RESET 3

// SPI CS a pomocny pin dotykove vrstvy
#define TOUCH_CS 4
#define TOUCH_IRQ 5

// Pomocne hodnoty pro tlacitka
#define MEZERA 10
#define TLACITKA_Y 70
#define TLACITKA_SIRKA 20
#define TLACITKA_VYSKA 20

// Objekty LCD a dotykove vrstvy
Adafruit_ILI9341 lcd = Adafruit_ILI9341(LCD_CS, LCD_DC, LCD_RESET);
XPT2046 touch(TOUCH_CS, TOUCH_IRQ);

// Objekty serveru HTTP a WebSocket
ESP8266WebServer server(80);
WebSocketsServer websocket_server = WebSocketsServer(81);

// Nazev a heslo Wi-Fi, ke ktere se budu pripojovat
const char ssid[] = "Klobouk";
const char heslo[] = "mameradikubucizka";

// Pomocne promenne
uint16_t barva_pera = ILI9341_WHITE;
uint16_t tlacitka_posun = 30;
bool websocket_pripojeno = false;
bool konec_dotyku = false;
bool online = true;
uint16_t stary_x = 0xffff;
uint16_t stary_y = 0xffff;
uint8_t websocket_pocet_spojeni = 0;

// Struktura tlacitka
struct Tlacitko {
  uint8_t typ;
  uint16_t barva;
  char prikaz[4];
  Adafruit_GFX_Button objekt;
};

/* C++ vektor s tlacitky
   Mohl bych samozrejme pouzit jednoduche pole,
   ale stoji za pripomenuti, ze Arduino je
   postavene na C++, a pokud ma cip dostatek RAM,
   muzete pouzivat i pokrocilejsi objekty jeho
   standardni knihovny. Dynamicky vektor je
   jednim z nich.
*/
std::vector<Tlacitko> tlacitka;

/* Pokud WebSocket zachyti nejakou udalost,
   spusti se tato funkce. My timto zpusobem
   zjistime, jestli se k nemu nekdo pripojil. Pokud
   ano, budou se nasledujici dotyky odesilat
   pripojenemu klientu.
*/
void websocketUdalost(uint8_t num, WStype_t type, uint8_t* data, size_t length) {
  if (type == WStype_CONNECTED) {
    websocket_pocet_spojeni++;
    websocket_pripojeno = true;
  }
  if (type == WStype_DISCONNECTED) {
    websocket_pocet_spojeni--;
    if (websocket_pocet_spojeni == 0)
      websocket_pripojeno = false;
  }
}

/* Funkce pro vykresleni noveho tlacitka
   na displej a vlozeni do vektoru tlacitek
*/
void pridejTlacitko(uint8_t typ, uint16_t barva, char* popisek, char* prikaz) {
  Tlacitko tlacitko;
  tlacitko.typ = typ;
  tlacitko.barva = barva;
  strcpy(tlacitko.prikaz, prikaz);
  tlacitko.objekt.initButton(&lcd, tlacitka_posun, TLACITKA_Y, TLACITKA_SIRKA, TLACITKA_VYSKA, ILI9341_WHITE, barva, ILI9341_WHITE, popisek, 2);
  tlacitko.objekt.drawButton();
  tlacitka.push_back(tlacitko);
  tlacitka_posun += TLACITKA_SIRKA + MEZERA;
}

/* Funkce pro zjisteni, zdali jsem klepnul na tlacitko.
   Pokud ano, zjistim, co to bylo za typ tlacitka a bud smazu
   obrazovku, nebo nastavim barvu stetce. Pokud je skrze WebSocket
   pripojeny nejaky klient, poslu mu prikaz, ktery dekoduje JavaScript
   a adekvatne provede podobnou operaci v prohlizeci.
*/
void zpracujTlacitka(uint16_t x, uint16_t y) {
  // Projdi vektor vsech tlacitek
  for (uint8_t i = 0; i < tlacitka.size(); i++) {
    // Pokud se dotykam tlacitka, oznac jej jako stisknute
    if (tlacitka[i].objekt.contains(x, y)){
      tlacitka[i].objekt.press(true);
    }
    else{
      tlacitka[i].objekt.press(false);
    }

    // Pokud jsem prave stisknul tlacitko...
    if (tlacitka[i].objekt.justPressed()) {
      // Prekresli tlacitko pro efekt s inverzni barvou
      tlacitka[i].objekt.drawButton(true);
      /* Pokud ma tlacitko typ 0, smaz platno a
         pripadnym WebSocket klientum odesli zpravu CLS
      */
      if (tlacitka[i].typ == 0) {
        lcd.fillRect(0, 90, 240, 230, ILI9341_BLACK);
        if (websocket_pripojeno) {
          websocket_server.broadcastTXT("CLS");
        }
      }
      /* Pokud se jedna o tlacitko s typem 1, je to
         tlacitko pro nastaveni barvy stetce. Zmenim
         tedy aktualni barvu a odeslu WebSocket klientum
         textovy prikaz, ktery jsem k tlacitku pripojil
         pri jeho vytvoreni.
      */
      else if (tlacitka[i].typ == 1) {
        barva_pera = tlacitka[i].barva;
        if (websocket_pripojeno) {
          websocket_server.broadcastTXT(tlacitka[i].prikaz);
        }
      }
    }
    // Pokud stisk skoncil, prekresli tlacitko zpet
    if (tlacitka[i].objekt.justReleased()) {
      tlacitka[i].objekt.drawButton();
    }
  }
}

// Hlavni funkce, ktera se spusti na zacatku
void setup() {
  // Pockej 1 s, vse se stabilizuje vcetne napajeni displeje
  delay(1000);
  // Nastaveni frekvence sbernice SPI
  SPI.setFrequency(ESP_SPI_FREQ);
  /* Vypni Wi-Fi a nastav jej do rezimu stanice.
     Vynucene vypnuti muze pomoci behem vyvoje,
     kdy cip stale flashujete a jeho registrace na
     Wi-Fi routeru pak nemusi probehnout vzdy
     uplne v poradku. Mam s tim bohate zkusenosti.
  */
  WiFi.mode(WIFI_OFF);
  WiFi.mode(WIFI_STA);

  // Nastartovani displeje
  lcd.begin();
  // Nastartovani dotykove vrstvy
  touch.begin(lcd.width(), lcd.height());
  /* Pomoci kalibracniho firmwaru jsem uz drive
     ziskal hodnoty krajnich dotyku. Ted je pouziji,
     aby body dotyku odpovidaly souradnicovemu
     systemu displeje.
  */
  touch.setCalibration(1848, 259, 257, 1815);
  // Prekresli obrazovku cernou barvou
  lcd.fillScreen(ILI9341_BLACK);
  // Nastav kurzor na 50x150
  lcd.setCursor(50, 150);
  // Nastave velikost pisma
  lcd.setTextSize(2);
  // Nastav barvu pisma
  lcd.setTextColor(ILI9341_WHITE);
  // Napis zpravu
  lcd.print("Hledam Wi-Fi");
  // Zmen kurzor
  lcd.setCursor(50, 160);

  // Zacni se pripojovat k Wi-Fi
  WiFi.begin(ssid, heslo);
  /* Kazdych 500 ms nakresli tecku a zjisti,
     jestli uz jsi pripojeny. Pokud nebudes
     pripojeny ani po 30 sekundach, prerus
     pripojovani a pokracuj v offline rezimu.
  */
  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    lcd.print('.');
    if ((millis() - start) > 30e3) {
      WiFi.mode(WIFI_OFF);
      online = false;
      break;
    }
  }

  /* Pokud otevru IP adresu cipu v prohlizeci,
     server mu preda text v promenne html. Jeji
     obsah po startu neprekazi v RAM, ale nacte se
     z rozmerneho flashoveho uloziste. Obsah je v
     samostatnem hlavickovem souboru html.h.
  */
  server.on("/", []() {
    server.send_P(200, "text/html", html);
  });
  // Nastartuj HTTP server na portu 80
  if (online) server.begin();

  /* Pokud se k WebSocket serveru nekdo pripoji,
     zpracuje se funkce websocketUdalost
  */
  websocket_server.onEvent(websocketUdalost);
  // Nastartuj WebSocket server na portu 81
  if (online) websocket_server.begin();

  // Pripojovani skoncilo, tak smaz displej
  lcd.fillScreen(ILI9341_BLACK);

  // Nakresli u horniho okraje IP adresu
  lcd.setCursor(60, 0);
  lcd.setTextSize(1);
  lcd.setTextColor(ILI9341_WHITE);
  if (online) {
    lcd.print("http://");
    lcd.print(WiFi.localIP());
  }
  else {
    lcd.setCursor(44, 0);
    lcd.print("Nejsem pripojeny k Wi-Fi");
  }

  // Pouzij rastrovy font a nakresli logo
  lcd.setFont(&FreeSerifItalic9pt7b);
  lcd.setCursor(40, 40);
  lcd.setTextColor(ILI9341_WHITE);
  lcd.setTextSize(2);
  lcd.print("ajPed Live");
  // Vrat se k systemovemu fontu
  lcd.setFont();

  // Nakresli tlacitka ovladaci listy.
  pridejTlacitko(0, ILI9341_BLACK, "x", "CLS");
  pridejTlacitko(1, ILI9341_BLACK, "", "C0");
  pridejTlacitko(1, ILI9341_RED, "", "CR");
  pridejTlacitko(1, ILI9341_GREEN, "", "CG");
  pridejTlacitko(1, ILI9341_BLUE, "", "CB");
  pridejTlacitko(1, ILI9341_WHITE, "", "CW");
  pridejTlacitko(1, ILI9341_YELLOW, "", "CY");

  /* Inicializace programu je hotova. Na obrazovce
     je vyskreslene GUI a zbytek programu
     se bude odehravat v nekonecne smycce loop,
     jejiz obsah se opakuje stale dokola.
  */
}

void loop() {
  // Vychozi pomocna hodnota souradnic X,Y mimo plochu displeje
  uint16_t x, y = 0xffff;
  //Pokud cip zachyti dotyk
  if (touch.isTouching()) {
    // Uloz souradnice dotyku do promennych x a y
    touch.getPosition(x, y);
    // Pokud je Y > 90 px, jsem v kreslici oblasti
    if (y > 90) {
      /* Pokud neznam souradnice predchoziho dotyku,
         nakreslim ctverec s rozmerem 3x3 px
         v miste dotyku - tedy bod
      */
      if (stary_x == 0xffff) {
        lcd.fillRect(x - 1, y - 1, 3, 3, barva_pera);
      } else {
        /* Pokud znam souradnici predchoziho dotyku, nekreslim mezi
           starou a novou souradnici caru. Funkce drawLine vsak
           neumoznuje nastavit tloustku, a tak nakreslim nekolik
           car vedle sebe, aby byla dostatecne tlusta.
        */
        lcd.drawLine(stary_x - 1, stary_y, x - 1, y, barva_pera);
        lcd.drawLine(stary_x, stary_y - 1, x, y - 1, barva_pera);
        lcd.drawLine(stary_x, stary_y, x, y, barva_pera);
        lcd.drawLine(stary_x, stary_y + 1, x, y + 1, barva_pera);
        lcd.drawLine(stary_x + 1, stary_y, x + 1, y, barva_pera);
      }
      // Uloz aktualni souradnici jako starou
      stary_x = x;
      stary_y = y;
      // Pomocna promenna pro budouci konec dotyku
      konec_dotyku = true;
      /* Pokud je nekdo pripojeny k WebSocket serveru,
         vytvor textovy retezec ve formatu x:y a odesli
         jej klientovi. Kvuli co nejvyssi rychlosti a co
         nejmensi spotrebe RAM nepouziju vestaveny objekt
         String pro snadnou praci s retezci podobne jako
         treba v JavaScriptu, ale text vytvorim pomoci
         zakladnich nizkourovnovych funkci jazyka C.
      */
      if (websocket_pripojeno) {
        char txt_xy[8];
        char txt_x[4];
        char txt_y[4];
        memset(txt_xy, 0, sizeof txt_xy);
        // Preved desitkova cisla X a Y na retezec
        itoa(x, txt_x, 10);
        itoa(y, txt_y, 10);
        // Spoj retezce dohromady
        strcat(txt_xy, txt_x);
        strcat(txt_xy, ":");
        strcat(txt_xy, txt_y);
        /* Odesli retezec vsem pripojenym klientum,
           kteri otevreli WebSocket spojeni
        */
        websocket_server.broadcastTXT(txt_xy);
      }
    }
  }
  /* Pokud dotykova vsrtva neregistruje zadny dotyk
     nastav promennou starych souradnic na pomoocnou
     hodnotu 0xffffff, cimz kodu vyse davam najevo,
     ze kresba cary skoncila a pri dalsim dotyku
     na ni nemam navazovat a zacnu novou caru.
  */
  else {
    stary_x = stary_y = 0xffff;
    /* Pokud jsem skoncil s dotykem a je nejake
       WebSocket spojeni, odesli mu text EOT,
       ktery Javascriptu na webove strance da najevo,
       ze ma take ukoncit kresbu cary.
    */
    if (konec_dotyku && websocket_pripojeno) {
      websocket_server.broadcastTXT("EOT");
      konec_dotyku = false;
    }
  }

  // Zjisti, jestli jsem se nedotknul tlacitek
  zpracujTlacitka(x, y);
  // Zjisti, jestli probiha nejaka WebSocket komunikace
  websocket_server.loop();
  // Zjisti, jestli probiha nejaka HTTP komunikace
  server.handleClient();

  // Smycku zpomalim o 20 ms, cimz si oddychne i WebSocket
  // Kdyz budu kreslit moc rychle, bude se kresba skladat z mensiho poctu bodu
  // Kdyz budu kreslit pomalu, bude detailni a krivky budou mit vice bodu
  delay(20);
}

Zdrojový kód hlavičkového souboru html.h, ve kterém je uloženo HTML a JS stránky, která bude překreslovat malůvku ve webovém prohlížeči:

/* Promenna html je obrovska, a tak ji pomoci
   makra PROGMEM nenacitam do RAM, ale ctu
   rovnou z rozmerne flashove pameti cipu.
   Pomoci formatu R"surove_html(retezec)surove_html";
   mohu v jazyku C++ zapisovat retezec nehlede na
   uvozovky a radky, takze HTML kod mohu napsat v puvodni podobe.
   Viz http://en.cppreference.com/w/cpp/language/string_literal
*/

static const char PROGMEM html[] = R"surove_html(
<!DOCTYPE html>
<html>
<head>
<title>ajPed Live</title>
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P&amp;subset=latin-ext" rel="stylesheet">
<style>
body{
  background: black;
  color: white;
  font-family: "Press Start 2P", cursive;
  line-height: 150%;
  margin: 50px;
}
#tft{
  border: 4px solid #999999; 
}
</style>
<script>
// Tuto funkci stranka spusti po nacteni HTML kodu
function start() {
  var zvetseni = 2;
  // Nastartovani platna
  var canvas = document.getElementById("lcd");
  canvas.width = canvas.width * zvetseni;
  canvas.height = canvas.height * zvetseni;
  var ctx = canvas.getContext("2d");
  // Nastaveni cerneho pozadi a stetcu
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.lineWidth = 3
  ctx.fillStyle = "white";
  ctx.strokeStyle = "white";
  var x0 = -1;
  var y0 = -1;
  // Pokus o spojeni s WebSocket serverem na portu 81
  var ws = new WebSocket("ws://" + window.location.hostname + ":81/");
  // Pokud od WebSocketu dostavam nejaka data
  ws.onmessage = function(evt) {
    // Vypis data do konzole prohlizece
    console.log(evt.data);
    // Pokud dosel text EOT, konec dotyku/tahu stetcem
    if(evt.data == "EOT"){
      x0 = y0 = -1;
    }
    // Pokud dosel text C0, CB, CR, CW, CY nebo CG, zmen barvu stetce
    else if(evt.data == "C0"){
      ctx.strokeStyle = "black";
      ctx.fillStyle = "black";
      ctx.beginPath();
    }
    else if(evt.data == "CB"){
      ctx.strokeStyle = "blue";
      ctx.fillStyle = "blue";
      ctx.beginPath();
    }
    else if(evt.data == "CR"){
      ctx.strokeStyle = "red";
      ctx.fillStyle = "red";
      ctx.beginPath();
    }
    else if(evt.data == "CW"){
      ctx.strokeStyle = "white";
      ctx.fillStyle = "white";
      ctx.beginPath();
    }
    else if(evt.data == "CY"){
      ctx.strokeStyle = "yellow";
      ctx.fillStyle = "yellow";
      ctx.beginPath();
    }
    else if(evt.data == "CG"){
      ctx.strokeStyle = "green";
      ctx.fillStyle = "green";
      ctx.beginPath();
    }
    // Pokud dosel text CLS, smaz obrazovku
    else if(evt.data == "CLS"){
      ctx.fillStyle = "black";
      ctx.strokeStyle = "black";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.strokeStyle = "white";
      ctx.fillStyle = "white";
      ctx.beginPath();
    }
    // V opacnem pripade dorazily souradnice dotyku
    else{
      // Ziskej z nich X a Y
      souradnice = evt.data.split(":");
      x = parseInt(souradnice[0]) * zvetseni;
      y = parseInt(souradnice[1]) * zvetseni;
      // Pokud nenavazuji na caru, nakresli tecku
      if(x0 == -1){
        ctx.fillRect(x - 1, y - 1, 3, 3);
        x0 = x;
        y0 = y; 
      }
      // Pokud navazuji na caru, nakresli ji mezi predchozia soucasnou souradnici
      else{
        ctx.moveTo(x0, y0);
        ctx.lineTo(x, y);
        ctx.stroke();
        x0 = x;
        y0 = y;
      }
    }
  };
}
</script>
</head>
<body onload="javascript:start();">
<center>
<h1>ajPed Live</h1>
<p>Co nakresl&#x00ED;&#x0161; na displej sv&#x00E9;ho ajPedu,<br />to se okam&#x017E;it&#x011B; zobraz&#x00ED; i na monitoru</p>
<canvas id="lcd" width="240" height="320"></canvas>
</center>
</body>
</html>
)surove_html";

/*
Jelikoz je kod napsany v ASCII, pro zapis ceskych
znaku v HTML pouziji zastupne kody. Poslouzi k
tomu treba tento prevodnik:
https://r12a.github.io/apps/conversion/
a konverze do formatu HEX NCR.
*/

A to je pro dnešek už opravdu vše. Můj ajPed funguje, takže teď už zbývá jen nastartovat kickstarterovou kampaň, z výdělku koupit jachtu a odjet někam do tropů.

Diskuze (7) Další článek: SpaceX uveřejnila úchvatné video ze startu Falconu Heavy. Zachycuje i dopad centrálního stupně

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,