Pojďme programovat elektroniku

Arduino a čeština. I drobný mikročip umí psát háčky a čárky a zvládne samozřejmě i emoji

  • Vývojové prostředí Arduino ukládá zdrojové kódy ve formátu UTF-8
  • UTF-8 už nějaký pátek používá i jeho monitor sériové linky
  • Ukážeme si, jak používat češtinu i emoji a na co si dát pozor v C/C++

Zatímco prakticky ve všech vyšších programovacích jazycích už dnes bez problému bastlíme v češtině, jakmile přijde řeč na malé mikrokontrolery a Arduino, dokonce i pro mnohé ostřílené makery je občas ohromným překvapením, že i zde samozřejmě mohou používat háčky, čárky a klidně i pestrobarevná emoji.

Arduino IDE ukádá text ve formátu UTF-8

Stejnojmenné vývojové prostředí Arduino IDE totiž ukládá zdrojový kód – takže i všechny jeho textové konstanty – v širokém formátu UTF-8.

Počkat, počkat, tady něco nehraje. Jak může Arduino používat znakovou sadu Unicode a její kódování UTF-8, když v něm programujeme v jazycích C/C++ a pro nízkoúrovňovou práci s textovými řetězci používáme zpravidla datový typ char, který má šířku jen 8 bitů?

Každé malé dítě přece ví, že do 8 bitů se vejde jen 256 kombinací, a právě proto do charu obvykle ukládáme jen základní znaky tabulky ASCII! Jak se tedy proboha může do charu vejít třeba 🚀?

🚀 je F0, 9F, 9A, 80

No, samozřejmě nemůže, emoji rakety má totiž ve znakové sadě Unicode pořadové číslo 128640 (U+1F680) a v kódování UTF-8 zabírá rovnou 32 bitů – tyto čtyři bajty: F09F9A80.  Jakýkoliv široký s pořadovým číslem vyšším než 255 nicméně můžeme uložit jako řetězec těchto dílčích bajtů.

047827a4-09a0-4b82-84eb-205c7e4d974b
Emoji rakety, její reprezantace v kódování UTF a zápis pomocí zástupných znaků napříč programovacími jazyky

Pojďme si to vyzkoušet v Arduinu na nějakém skutečném čipu. Já sáhnu po libovolné desce s oblíbeným Wi-Fi procesorem z rodiny Espressif ESP32, češtinu a emoji si totiž později vyzkoušíme i na webu.

Co když v Arduinu uložíme 🚀 jako znak?

V prvním programu si ověříme, co se stane, když emoji rakety uložíme jako osmibitový znak char a vypíšeme jej včetně hexadecimální numerické hodnoty bajtu v sériovém monitoru prostředí Arduino:

char znak = '🚀';
Serial.printf("Textova reprezentace znaku: %c\n", znak);
Serial.printf("Numericka hodnota znaku (sestnactkove): %02X\n", znak);

Co myslíte, že se stane? Havaruje překlad, protože je raketa příliš široká? Způsobíme reset Matrixu a implozi vesmíru? Tentokrát ještě ne, 32bitové emoji se totiž prostě jen ořeže na posledních 8 bitů, které se vejdou do typu char a v sériovém monitoru se zobrazí tyto dva řádky:

Textova reprezentace znaku: �
Numericka hodnota znaku (sestnactkove): 80

Všimněte si, že z původního sledu bajtů F0, 9F, 9A, 80 nám zůstal jen ten poslední 80, no a protože na této pozici (dekadicky je to 128) se nachází v tabulce Unicode vyhrazená část pro řídící – netisknutelné – znaky, zobrazí se zpravidla nějaký zástupný symbol (třeba otazník jako v našem případě).

9c3f14ca-3d7e-4d1f-955e-89d389645f48
Emoji rakety se v datovém typu char ořezalo na nejnižší bajt 0x80

A teď to zkusíme s řetězcem

Co s tím? Zkusme emoji rakety namísto prostého znaku uložit a zobrazit jako řetězec znaků C:

char text[] = "🚀";
  Serial.printf("Textova reprezentace retezce: %s\n", text);
  Serial.print("Numericka hodnota retezce (sestnactkove): ");
  for (int i = 0; i < strlen(text); i++) {
    Serial.printf("%02X ", text[i]);
  }

Do proměnné text se nyní uloží všechny bajty pole znaků, které tvoří emoji rakety. Našemu firmwaru je úplně jedno, jakou mají hodnotu, o jejich reprezentaci se totiž postará až nějaký výstup, do kterého je pošleme. A protože jím bude monitor sériové linky v prostředí Arduino, který bajty dekóduje jako texty UTF-8, vyplivne nám tyto dva korektní řádky:

Textova reprezentace retezce: 🚀
Numericka hodnota retezce (sestnactkove): F0 9F 9A 80

Zdrojový kód znázorňuje raketu jako jeden zobrazitelný znak, ten ale tentokrát zabírá rovnou čtyři bajty. To sebou nese určité komplikace, s raketou si totiž nebudou rozumět některé primitivní funkce pro práci s textovými řetězci C.

db25338f-af20-4384-ad96-c819142f9cc7
Emoji rakety ve formě řetězce se už bez problému zobrazí jako každý jiný tisknutelný znak

Mimochodem, pro umocnění efektu jsem doposud psal v prostém ASCII bez háčků a čárek i informativní popisky. Teď už víme, že pokud budeme sériovou linku číst v monitoru, který rozumí UTF-8, můžeme si dopřát plnotučnou češtinu:

Serial.printf("Textová reprezentace řetězce: %s\n", text);
Serial.print("Numerická hodnota řetězce (šestnáctkově): ");

V dalších příkladech už proto budeme ve všech zdrojových k´dech diakritika.

Co když se pokusím změřit délku rakety

Typickým příkladem funkce, která neumí pracovat s širokými znaky, je strlen pro zjištění délky textové řetězce. Když se tedy pokusím v dalším příkladu vypsat do sériového monitoru Arduina velikost textu „🚀“:

char text[] = "🚀";
Serial.printf("Řetězec „%s“ má délku %zd znaků a %zd bajtů\n",
              text,
              strlen(text),
              sizeof(text));

Odpovědí bude řádek:

Řetězec „🚀“ má délku 4 znaků a 5 bajtů

Ještě jednou, protože funkce strlen nezná široké znaky, počítá mezi ně všechny bajty, ze kterých se skládá proměnná text, až po ukončovací nulový znak, který označuje konec řetězce v paměti. Právě proto je celková velikost proměnné ještě o jeden bajt delší.

dcb97e43-aa0e-41bb-84d5-fca04272b603
Standardní funkci strlen nemůžeme použít pro měření délky textu s šiorkými znaky 

Na proměnlivou délku textu s širokými znaky musíme myslet zvláště tehdy, pokud bychom pracovali s proměnnými a konstantami s fixní velikostí.

Zatímco toto je problém (paměť na jeden znak a ukončení řetězce):

char text[2] = "🚀";

Toto už bude v pořádku (čtyři bajty + ukončení řetězce):

char text[5] = "🚀";

Vlastní funkce pro změření délky rakety

Kódování UTF-8 se v posledních dvou dekádách pomalu stalo normou hlavně díky tomu, že reprezentuje znaky Unicode s proměnlivou šířkou. Díky tomu se ušetří hromada místa, což je klíčové zejména na internetu.

Takže zatímco znaky ASCII/ISO-8859-1 s pořadovým číslem 0-255 zabírají i v UTF-8 právě jeden bajt a UTF-8 je díky tomu s nimi zpětně kompatibilní, znaky na vyšších adresách Unicode už podle potřeby zabírají více bajtů.

4a2081cf-e5f9-432b-b7b3-4a730115f56f
Způsob kódování znaků UTF-8 s proměnlivou šířkou (Wikipedie

Na stranu druhou je pak ale pro začátečníka docela složité pochopit způsob, jak v proudu bajtů identifikovat začátky jednotlivých znaků UTF-8, když nemají pevnou šířku

V tabulce výše z si všimněte, že pokud znak UTF-8 zabírá více bajtů, ty další vždy začínají unikátní dvojicí bitů 1,0. Můžeme si tedy vyrobit alternativní funkci utf8_strlen, která projde bajt po bajtu, no a pokud bude začínat odlišnou dvojicí bitů, musí to být nový znak a  zvýšíme počítadlo:

size_t utf8_strlen(const char* text) {
  size_t delka = 0;
  while (*text) {
    if ((*text & 0b11000000) != 0b10000000) {  // Pokud bajt nezačíná 10xxxxxx, jde o nový znak
      delka++;
    }
    text++;
  }
  return delka;
}

Všimněte si, že pro kontrolu prvních dvou bitů v každém bajtu řetězce používáme bitový součin & s maskou 1, 1, 0, 0, 0, 0, 0, 0, který pro dva porovnávané bity vrátí 1 jen v případě, že mají oba hodnotu 1.

U všech následných bajtů jednoho znaku UTF-8, které začínají sekvencí 1,0 proto musí být výsledek součinu vždy roven hodnotě 1, 0, 0, 0, 0, 0, 0, 0 (viz přiložený obrázek). Pokud bude výsledek jiný, máme nový znak.

c39a7770-5b14-4012-a454-d3c40ea8a063
Zjištění nového znaku kontrolním bitovým součinem s maskou

Podobným způsobem bychom si mohli upravit i další funkce, které v nitru počítají jednotlivé bajty a předpokládají, že znak má vždy 8 bitů. Naopak funkce pro porovnání dvou řetězců strcmp nebo prosté kopírování strcpy budou fungovat vždy.

e20c01a8-1bd2-445b-aa6a-35500e9aa3e0
Vlastní funkce utf8_strlen spočítá správnou délku řetězce s širokými znaky UTF-8

Webový server s diakritikou

V posledním příkladu si proto napíšeme primitivní program, který spustí HTTP server na standardním TCP portu 80, a pokud po přihlášení do Wi-Fi zadáme místní IP adresu čipu, nebo vyťukáme v prohlížeči adresu http://mujweb.local, zobrazí se HTML stránka v perfektní češtině.

011664b2-e704-4da6-93b3-4caca5a135fc
Webová stránka v UTF-8, s hromadou emoji a v češtině

I v tomto případě totiž bude záležet až na čtenáři – webovém prohlížeči – jakým způsobem bude dekódovat příchozí bajty. Hned na začátku v HTTP hlavičce a posléze i v HTML kódu jej proto upozorníme, že se jedná o kódování UTF-8.

066f6fa4-23be-41bd-b8b7-71feea7392e2
HTML stránka plná barevných emoji (dle systémového fontu pro emoji)

Opět připomenu, že jednobajtové znaky UTF-8 jsou zpětně kompatibilní se základním ASCII/ISO-8859-1, takže tyto řídící povely bez háčků a čárek prohlížeč pochopí vždy.

#include <WiFi.h>
#include <NetworkClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>

const char ssid[] = "SSID Wi-Fi site (2,4GHz)";
const char heslo[] = "Heslo k siti";
const char mdns_domena[] = "mujweb";
WebServer server(80);

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, heslo);
  Serial.printf("Připojuji se k %s", ssid);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" OK");
  Serial.printf("Dostal jsem IP adresu %s\n", WiFi.localIP().toString().c_str());
  Serial.printf("Spouštím místní mDNS doménu http://%s.local\n", mdns_domena);
  MDNS.begin(mdns_domena);
  server.on("/", []() {
    const char html[] = R"(<!DOCTYPE html>
    <html lang="cs">
      <head>
        <title>Zkušební stránka v UTF-8</title>
        <meta charset="UTF-8">
      </head>
      <body>
        <h1>Ahoj, toto je moje webová stránka</h1>
        <h3>A toto je hromada veselých emoji!</h3>
        <p>🚀🧟‍♂️📺😯♥️👍✈️🍷🍇😍💻🤖🎂😀🪦🎐🚒🔥🧯🐻‍❄️🧊🍹🧁🍰</p>
      </body>
    </html>)";
    server.send(200, "text/html; charset=UTF-8", html);
  });
  server.onNotFound([]() {
    server.send(404, "text/html; charset=UTF-8", "Nemáme! Nevedeme!");
  });
  server.begin();
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);
}

void loop() {
  server.handleClient();
  delay(2);
}

Příště  budeme zobrazovat UTF-8 na displejích

Tak, už víme, že háčky a čárky nemusejí být ani na jednoduchých mikrokontrolerech žádné tabu, což je velmi praktické právě tehdy, když má nějaká ta krabička internetu věcí webové rozhraní.

Často má ale také displej. Jak na něm zobrazit v Arduinu češtinu kódovanou v UTF-8, si už ale ukážeme až příště. Dnes toho bylo až až.

Diskuze (5) Další článek: EU chce podle Němců zakázat provoz docela nových dieselových aut. Čeká se na soud

Témata článku: , , ,