Obsah

LED mapa okresů ČR

Tvoje Máma (Twitter) má na svědomí výbornou mapu ČR - co okres, to jedna adresovatelná LEDka.

Informace o mapě, jejím osazení a další naleznete na oblíbeném serveru Chiptron.cz zde: Fantastická mapa České republiky. Co okres, to jedna adresovatelná RGB LED.

Formulář pro objednání: https://forms.gle/DAS6gksyBvNbf8TD8

GitHub se zdrojovým kódem: https://github.com/tomasbrincil/pcb_mapa_cr_1

Data pro okresy

Data dodává TMEP na základě mapy ČR a publikovaných venkovních čidel. Pokud měříte a jste na mapě venkovních čidel či ovzduší a máte vyplněný u čidla okres, pak je vaše měření zahrnuté do podkladových dat. V případě, že je čidel v okrese více, bere se průměr hodnot ze všech těchto čidel. Ve všech případech se bere pouze měření, které není starší jak 1 hodina. Pokud měření pro některý okres chybí, je vložena hodnota celorepublikového průměru ze všech naměřených okresů. Aktualizovaný seznam chybějících okresů naleznete zde: https://cdn.tmep.cz/app/export/okresy-cr-co-nemame.html

Data jsou JSON pole ve formátu:

[
    {
        "id":1,
        "h":100
    },
    {
        "id":2,
        "h":72.7
    },
    ...
]

Podkladová data, aktualizovaná po minutě:
http://cdn.tmep.cz/app/export/okresy-cr-teplota.json
http://cdn.tmep.cz/app/export/okresy-cr-vlhkost.json
http://cdn.tmep.cz/app/export/okresy-cr-tlak.json
http://cdn.tmep.cz/app/export/okresy-cr-prasnost.json

Všechny čtyři hodnoty v jednom:
http://cdn.tmep.cz/app/export/okresy-cr-vse.json
h1 - teplota, h2 - vlhkost, h3 - tlak, h4 - kvalita ovzduší

Číselník okresů

V datech se okres váže na ID s tím, že pořadí okresů odpovídá pořadí adresovatelných LEDek na mapě:

IDOkres
1Cheb
2Sokolov
3Karlovy Vary
4Chomutov
5Louny
6Most
7Teplice
8Litoměřice
9Ústí nad Labem
10Děčín
11Česká Lípa
12Liberec
13Jablonec nad Nisou
14Semily
15Jičín
16Trutnov
17Náchod
18Hradec Králové
19Rychnov nad Kněžnou
20Ústí nad Orlicí
21Pardubice
22Chrudim
23Svitavy
24Šumperk
25Jeseník
26Bruntál
27Olomouc
28Opava
29Ostrava-město
30Karviná
31Frýdek-Místek
32Nový Jičín
33Vsetín
34Přerov
35Zlín
36Kroměříž
37Uherské Hradiště
38Hodonín
39Vyškov
40Prostějov
41Blansko
42Brno-město
43Brno-venkov
44Břeclav
45Znojmo
46Třebíč
47Žďár nad Sázavou
48Jihlava
49Havlíčkův Brod
50Pelhřimov
51Jindřichův Hradec
52Tábor
53České Budějovice
54Český Krumlov
55Prachatice
56Strakonice
57Písek
58Klatovy
59Domažlice
60Tachov
61Plzeň-sever
62Plzeň-město
63Plzeň-jih
64Rokycany
65Rakovník
66Kladno
67Mělník
68Mladá Boleslav
69Nymburk
70Kolín
71Kutná Hora
72Benešov
73Příbram
74Beroun
75Praha-západ
76Praha-východ
77Praha

20230420-063341.jpeg

Zdrojové kódy

Na GitHubu (https://github.com/tomasbrincil/pcb_mapa_cr_1) najdete dva příklady, které s daty s TMEPu pracují a potřebujete jen MapuTvojíMámy a vývojovou desku.

Pokud přidáte ještě dotykové tlačítko a OLED display, tak jsem připravil dva kódy, které je využívají. 20230402-211421.jpegZapojení komponent na PINy najdete na začátku kódu.

Model pro stojan na mapu najdete zde: https://www.printables.com/cs/model/457603-stojan-na-mapu-tvoji-mamy

Teplota a ovzduší z TMEPu

Dotykovým tlačítkem můžete přepínat mezi teplotou a ovzduším, na displeji se vám ukážou vždy minimální a maximální hodnoty pro celou republiku. Po pěti minutách zhasnou LEDky a displej, stisknutím tlačítka je opět proberete k životu.

main.cpp
/*
   __  __                _______         _ _ __  __
  |  \/  |              |__   __|       (_|_)  \/  |
  | \  / | __ _ _ __   __ _| |_   _____  _ _| \  / | __ _ _ __ ___  _   _
  | |\/| |/ _` | '_ \ / _` | \ \ / / _ \| | | |\/| |/ _` | '_ ` _ \| | | |
  | |  | | (_| | |_) | (_| | |\ V / (_) | | | |  | | (_| | | | | | | |_| |
  |_|  |_|\__,_| .__/ \__,_|_| \_/ \___/| |_|_|  |_|\__,_|_| |_| |_|\__, |
              | |                     _/ |                          __/ |
              |_|                    |__/                          |___/
 
Map with connected display and touch button, downloads data from TMEP.cz for
Czech districts and display temperature/air quality. You can cycle between those with touch
button. Display shows what data are you looking at and min/max value.
 
More info about map:
https://wiki.tmep.cz/doku.php?id=ruzne:led_mapa_okresu_cr
 
Used components:
- MapaTvojiMamy
- LaskaKit ESP32-LPkit v2.4
- 0.91" OLED
- Catalex touch sensor v2.0 (simply some capacative touch sensor)
*/
 
////////////////////
// Variables setup
////////////////////
 
// Wi-Fi, JSON with data
const char *ssid = "TVOJE_AP";
const char *password = "TVOJE_HESLO";
const char *json_url = "http://cdn.tmep.cz/app/export/okresy-cr-vse.json";
 
// After how long get new values - default 2 minutes
unsigned long timerDelay = 120000;
// After how go to "sleep" (LEDs off, display off, "wake up" by touching button) - default 5 minutes
unsigned long offDelay = 300000;
 
// LED strip - MapaTvojiMamy
#define LEDS_COUNT 77
#define LEDS_PIN 14
#define CHANNEL 0
#define LEDS_BRIGHTNESS 10
 
// Touch button
#define BUTTON_PIN 16
 
// Display PINs
#define DISPLAY_CLOCK_PIN 25
#define DISPLAY_DATA_PIN 26
 
// What to show as default - temp or air
char *defaultView = "temp";
 
// Display hello text
char *displayText1 = "MapaTvojiMamy";
char displayText2[14] = "";
 
///////////
// Code
///////////
 
// Libraries
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h> //https://github.com/bblanchon/ArduinoJson v6+
#include "Freenove_WS2812_Lib_for_ESP32.h"
 
// Display
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X32_UNIVISION_1_SW_I2C u8g2(U8G2_R0, DISPLAY_CLOCK_PIN, DISPLAY_DATA_PIN, /* reset=*/U8X8_PIN_NONE);
 
// Strip
Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(LEDS_COUNT, LEDS_PIN, CHANNEL, TYPE_GRB);
 
// Variables
unsigned long lastTime = 0;
unsigned long lastTouchTime = 0;
 
int lastid, value, color, firstTime = 1, isOn = 1;
 
// Button state
boolean oldState = LOW;
 
String sensorReadings;
double sensorReadingsArr[3];
// in JSON is h1 used for temperature and h4 for air quality, we will stick with these variables
double h1[77]; // array for temperature measurements
double h4[77]; // array for air quality measurements
double h1min, h1max, h4min, h4max;
 
void setup()
{
  u8g2.begin();
  pinMode(BUTTON_PIN, INPUT);
  Serial.begin(115200);
  delay(5);
  Serial.println();
  strip.begin();
  strip.setBrightness(LEDS_BRIGHTNESS);
}
 
void lightenUpYourMamaMap()
{
  // Map value to color, set it to corresponding LED and show it
  // We already have arrays populated 
  if(defaultView == "temp")
  {
    for(int i = 0; i < LEDS_COUNT; i = i + 1)
    {
      color = map(h1[i], -15, 40, 170, 0);
      strip.setLedColorData(i, strip.Wheel(color));
    }
  }
  else
  {
    for(int i = 0; i < LEDS_COUNT; i = i + 1)
    {
      color = map(h4[i], -15, 40, 170, 0);
      strip.setLedColorData(i, strip.Wheel(color));
    }
  }
  strip.show();
}
 
void setValuesForDisplay()
{
  if(defaultView == "temp")
  {
    displayText1 = "Teplota";
    sprintf(displayText2, "%d.%01d / %d.%01d", (int)h1min, abs((int)(h1min*10)%10), (int)h1max, abs((int)(h1max*10)%10));
  }
  else
  {
    displayText1 = "Ovzdusi";
    sprintf(displayText2, "%d.%01d / %d.%01d", (int)h4min, abs((int)(h4min*10)%10), (int)h4max, abs((int)(h4max*10)%10));
  }
}
 
String httpGETRequest(const char *serverName)
{
  WiFiClient client;
  HTTPClient http;
  http.begin(client, serverName);
  int httpResponseCode = http.GET();
  String payload = "{}";
  if(httpResponseCode > 0)
  {
    Serial.print("HTTP Response code: ");
    Serial.println(httpResponseCode);
    payload = http.getString();
  }
  else
  {
    Serial.print("Error code: ");
    Serial.println(httpResponseCode);
  }
  http.end();
  return payload;
}
 
void loop()
{
  boolean newState = digitalRead(BUTTON_PIN);
 
  // Touching the button?
  if((newState == LOW) && (oldState == HIGH))
  {
    // Short delay to debounce button.
    delay(40);
    // Check if button is still low after debounce.
    newState = digitalRead(BUTTON_PIN);
 
    // Button touched!
    if(newState == LOW)
    {
      Serial.print("Button touched, mode: ");
 
      lastTouchTime = millis();
 
      // Are we on? Change what to show
      if(isOn == 1)
      {
        if(defaultView == "temp")
        {
          defaultView = "air";
        }
        else
        {
          defaultView = "temp";
        }
 
        setValuesForDisplay();
        Serial.println(defaultView);
 
        // Let it shine
        lightenUpYourMamaMap();
      }
      else
      {
        // "Powering up"
        Serial.println("go online");
        isOn = 1;
        strip.setBrightness(LEDS_BRIGHTNESS);
        lightenUpYourMamaMap();
        firstTime = 1;
        u8g2.setPowerSave(false);
      }
    }
  }
 
  // Let's go "offline"
  if(isOn == 1 && ((millis() - lastTouchTime) > offDelay))
  {
      Serial.println("go offline");
      isOn = 0;
      strip.setBrightness(0);
      lightenUpYourMamaMap();
      u8g2.setPowerSave(true);
  }
 
  // Set the last-read button state to the old state.
  oldState = newState;
 
  // Did we wait long enough or was it just powered on?
  // Read JSON and populate variables with measurements from districts
  if(isOn == 1 && ((millis() - lastTime) > timerDelay || firstTime == 1))
  {
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
      delay(500);
      Serial.print(".");
    }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
 
    if(WiFi.status() == WL_CONNECTED)
    {
      firstTime = 0;
      sensorReadings = httpGETRequest(json_url);
      WiFi.disconnect();
      Serial.println("WiFi disconnected");
 
      DynamicJsonDocument doc(12288);
      DeserializationError error = deserializeJson(doc, sensorReadings);
      if(error)
      {
        Serial.print(F("deserializeJson() failed: "));
        Serial.println(error.f_str());
        return;
      }
 
      // reset min/max values
      h1min = 100;
      h1max = -100;
      h4min = 1000;
      h4max = 0;
 
      for (JsonObject item : doc.as<JsonArray>())
      {
        // Index, JSON starts with 1, we need to start with 0 so deduct 1
        int ledIndex = item["id"];
        ledIndex -= 1;
        double temp = item["h1"];
        double air = item["h4"];
 
        // Set minimums and maximums
        if(temp > h1max)
        {
          h1max = temp;
        }
        if(temp < h1min)
        {
          h1min = temp;
        }
        if(air > h4max)
        {
          h4max = air;
        }
        if(air < h4min)
        {
          h4min = air;
        }
 
        // Let's map air value to respective color on ColorWheel:
        // https://github.com/Freenove/Freenove_WS2812_Lib_for_ESP32/blob/master/extras/ColorWheel.jpg
        // green
        if(air < 20)
        {
          air = 85;
        }
        // orange
        else if(air < 40)
        {
          air = 45;
        }
        // red
        else if(air < 100)
        {
          air = 10;
        }
        // purple
        else if(air < 5000)
        {
          air = 200;
        }
 
        // Populate our two arrays
        h1[ledIndex] = temp;
        h4[ledIndex] = air;
      }
 
      setValuesForDisplay();
      lightenUpYourMamaMap();
 
      // Debugging values we got
      /*
      for(int i = 0; i < LEDS_COUNT; i = i + 1)
      {
          Serial.print("h1[");
          Serial.print(i);
          Serial.print("] value: ");
          Serial.println(h1[i]);
 
          Serial.print("h4[");
          Serial.print(i);
          Serial.print("] value: ");
          Serial.println(h4[i]);
      }
      */
    }
    else
    {
      displayText1 = "NO WIFI";
      Serial.println("WiFi not connected :( is reachable?");
    }
 
    lastTime = millis();
  }
 
  // Show texts on display
  if(isOn == 1)
  {
    u8g2.firstPage();
    do
    {
      u8g2.setFont(u8g2_font_ncenB10_tr);
      u8g2.drawStr(0, 12, displayText1);
      u8g2.drawStr(0, 26, displayText2);
    } while (u8g2.nextPage());
  }
}

Kvíz "uhodni okres"

Zábavná hra pro celou rodinu - po odstartování se rozbliká LEDka konkrétního okresu, můžete hádat o který se jedná a stiskem dotykového tlačítka se zobrazí odpověď a následně se rozbliká další okres.

Kód by se dal modifikovat i pro použití bez tlačítka (prostě se odstartuje samo a LEDky se po nějaké pauze mění). Okresy se rozblikávají čistě náhodně - do budoucna by bylo dobré, kdyby v rámci jedné hry nedošlo k opakování jedné oblasti a postupně se prostřídalo vše.

main.cpp
/*
   __  __                _______         _ _ __  __
  |  \/  |              |__   __|       (_|_)  \/  |
  | \  / | __ _ _ __   __ _| |_   _____  _ _| \  / | __ _ _ __ ___  _   _
  | |\/| |/ _` | '_ \ / _` | \ \ / / _ \| | | |\/| |/ _` | '_ ` _ \| | | |
  | |  | | (_| | |_) | (_| | |\ V / (_) | | | |  | | (_| | | | | | | |_| |
  |_|  |_|\__,_| .__/ \__,_|_| \_/ \___/| |_|_|  |_|\__,_|_| |_| |_|\__, |
              | |                     _/ |                          __/ |
              |_|                    |__/                          |___/
 
Simple quiz "which district is lightened up?"
 
Info about map:
https://wiki.tmep.cz/doku.php?id=ruzne:led_mapa_okresu_cr
 
Used components:
- MapaTvojiMamy
- LaskaKit ESP32-LPkit v2.4
- 0.91" OLED
- Catalex touch sensor v2.0 (simply some capacative touch sensor)
*/
 
////////////////////
// Variables setup
////////////////////
 
// After how go to "sleep" (LEDs off, display off, "wake up" by touching button) - default 5 minutes
unsigned long offDelay = 300000;
 
// LED strip - MapaTvojiMamy
#define LEDS_COUNT 77
#define LEDS_PIN 14
#define CHANNEL 0
#define LEDS_BRIGHTNESS 10
 
// Touch button
#define BUTTON_PIN 16
#define STATE_WHEN_TOUCHED HIGH
 
// Display PINs
#define DISPLAY_CLOCK_PIN 25
#define DISPLAY_DATA_PIN 26
 
// Variables for two lines of OLED display
char *displayText1 = "";
char *displayText2 = "";
 
// Array with districts, index from 0 to corresponding LED
char *districts[]
{
  "Cheb",
  "Sokolov",
  "Karlovy Vary",
  "Chomutov",
  "Louny",
  "Most",
  "Teplice",
  "Litomerice",
  "Usti nad Labem",
  "Decin",
  "Ceska Lipa",
  "Liberec",
  "Jablonec nad Nisou",
  "Semily",
  "Jicin",
  "Trutnov",
  "Nachod",
  "Hradec Kralove",
  "Rychnov nad Kneznou",
  "Usti nad Orlici",
  "Pardubice",
  "Chrudim",
  "Svitavy",
  "Sumperk",
  "Jesenik",
  "Bruntal",
  "Olomouc",
  "Opava",
  "Ostrava-mesto",
  "Karvina",
  "Frydek-Mistek",
  "Novy Jicin",
  "Vsetin",
  "Prerov",
  "Zlin",
  "Kromeriz",
  "Uherske Hradiste",
  "Hodonin",
  "Vyskov",
  "Prostejov",
  "Blansko",
  "Brno-mesto",
  "Brno-venkov",
  "Breclav",
  "Znojmo",
  "Trebic",
  "Zdar nad Sazavou",
  "Jihlava",
  "Havlickuv Brod",
  "Pelhrimov",
  "Jindrichuv Hradec",
  "Tabor",
  "Ceske Budejovice",
  "Cesky Krumlov",
  "Prachatice",
  "Strakonice",
  "Pisek",
  "Klatovy",
  "Domazlice",
  "Tachov",
  "Plzen-sever",
  "Plzen-mesto",
  "Plzen-jih",
  "Rokycany",
  "Rakovnik",
  "Kladno",
  "Melnik",
  "Mlada Boleslav",
  "Nymburk",
  "Kolin",
  "Kutna Hora",
  "Benesov",
  "Pribram",
  "Beroun",
  "Praha-zapad",
  "Praha-vychod",
  "Praha"
};
 
///////////
// Code
///////////
 
// Display
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X32_UNIVISION_1_SW_I2C u8g2(U8G2_R0, DISPLAY_CLOCK_PIN, DISPLAY_DATA_PIN, U8X8_PIN_NONE);
 
// Strip
#include "Freenove_WS2812_Lib_for_ESP32.h"
Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(LEDS_COUNT, LEDS_PIN, CHANNEL, TYPE_GRB);
 
// To know when we should go to sleep
unsigned long lastTouchTime = 0;
// More things to initialize
int lastId = 50, isOn = 1, firstTime = 1, gameTime = 0;
 
void setup()
{
    u8g2.begin();
    pinMode(BUTTON_PIN, INPUT);
    Serial.begin(115200);
    delay(5);
    Serial.println();
    strip.begin();
    strip.setBrightness(LEDS_BRIGHTNESS);
}
 
void randomLEDs()
{
    for(int i = 0; i < LEDS_COUNT; i = i + 1)
    {
        strip.setLedColorData(i, strip.Wheel(map(rand() % 100, -15, 40, 170, 0)));
    }
    strip.show();
}
 
void powerDown()
{
    Serial.println("go offline");
    isOn = 0;
    strip.setBrightness(0);
    randomLEDs();
    u8g2.setPowerSave(true);
}
 
void powerUp()
{
    isOn = 1;
    firstTime = 1;
    Serial.println("go online");
    strip.setBrightness(LEDS_BRIGHTNESS);
    u8g2.setPowerSave(false);
}
 
void waitForButtonTouch()
{
    int ButtonTouched = 0;
 
    while(ButtonTouched == 0)
    {
        boolean newState = digitalRead(BUTTON_PIN);
 
        // Touching the button?
        if(newState == STATE_WHEN_TOUCHED)
        {
            ButtonTouched = 1;
 
            // Are we offline? "Power up"!
            if(isOn == 0)
            {
                powerUp();
            }          
 
            lastTouchTime = millis();
            Serial.print("Button touched.");
 
            // Reset button state to untouched
            break;
        }
 
        // Let's go "offline"
        if(isOn == 1 && ((millis() - lastTouchTime) > offDelay))
        {
            powerDown();
        }
    }
}
 
void drawOnDisplay()
{
    u8g2.firstPage();
    do
    {
        u8g2.setFont(u8g2_font_ncenB10_tr);
        u8g2.drawStr(0, 12, displayText1);
        u8g2.drawStr(0, 26, displayText2);
    } while (u8g2.nextPage());
}
 
void countDownProcedure()
{
    drawOnDisplay();
    strip.setBrightness(LEDS_BRIGHTNESS);
    randomLEDs();
    delay(200);
    strip.setBrightness(6);
    strip.setAllLedsColor(204, 204, 204);
    strip.show();
    delay(200);
}
 
void loop()
{
    // First time? Run intro!
    if(isOn == 1 && firstTime == 1)
    {
        // Something like "animation"
        randomLEDs();
        displayText1 = "Mapa";
        displayText2 = "OKR";
        drawOnDisplay();
        delay(1000);
 
        randomLEDs();
        displayText1 = "MapaTvoji";
        displayText2 = "OKRESNI";
        drawOnDisplay();
        delay(1000);
 
        randomLEDs();
        displayText1 = "MapaTvojiMamy";
        displayText2 = "OKRESNI KVIZ";
        drawOnDisplay();
 
        for(int i = 6; i > 0; i = i - 1)
        {
            delay(i * 100);
            randomLEDs();
        }
 
        displayText1 = "MapaTvojiMamy";
        displayText2 = "Dotkni se! :)";
        drawOnDisplay();
 
        waitForButtonTouch();
 
        displayText1 = "Budeme hrat!";
        displayText2 = "Priprav se...";
        drawOnDisplay();
        delay(1000);
 
        displayText1 = "HRAJEM!";
        displayText2 = "";
        drawOnDisplay();
        delay(1000);
 
        firstTime = 0;
    }
 
    // Let's play!
    while(isOn == 1 && firstTime == 0)
    {
        displayText2 = "";
 
        displayText1 = "3";
        countDownProcedure();
        displayText1 = "2";
        countDownProcedure();
        displayText1 = "1";
        countDownProcedure();
 
        // Random district
        int districtID = 0;
        int generate = 1;
        while(generate == 1)
        {
            districtID = esp_random() % 100;
            if(districtID < LEDS_COUNT)
            {
                generate = 0;
            }
        }
 
        // Show district on map
        for(int i = 0; i < LEDS_COUNT; i = i + 1)
        {
            if(i == districtID)
            {
                strip.setBrightness(LEDS_BRIGHTNESS);
                strip.setLedColorData(i, 0x00FF00);
            }
            else
            {
                strip.setBrightness(3);
                strip.setLedColorData(i, 0xCCCCCC);
            }
        }
        strip.show();
 
        displayText1 = "Jaky je to okres?";
        displayText2 = "Dotkni se!";
        drawOnDisplay();
 
        // Blink with district we are guessing
        strip.setBrightness(LEDS_BRIGHTNESS);
        for(int i = 0; i < 12; i = i + 1)
        {
            if(i % 2 == 1)
            {
                strip.setLedColorData(districtID, 0x00FF00);
            }
            else
            {
                strip.setLedColorData(districtID, 0xFF0000);
            }
            strip.show();
            delay(200);
        }
 
        waitForButtonTouch();
 
        // Didn't woke up? Show answer and move on
        if(firstTime == 0)
        {
            displayText1 = "Odpoved je...";
            displayText2 = "";
            drawOnDisplay();
 
            delay(500);
 
            displayText1 = districts[districtID];
            drawOnDisplay();        
 
            delay(3000);
        }
    }
 
    // Show texts on display
    if(isOn == 1)
    {
        drawOnDisplay();
    }
}