Pojďme programovat elektroniku

Toto vaše žárovka neumí. Proměníme ji v teploměr, vlhkoměr, indikátor CPU a parodii na Philips Ambilight

  • Skoro každou chytrou žárovku můžete ovládat ve vlastním programu
  • Ať už pomocí oficiálního API, nebo hackingu
  • Dnes proměníme WiFi žárovku WiZ v barevný semafor

Máte doma alespoň jednu chytrou RGB LED žárovku? Pak ji můžete ovládat z mobilní aplikace a nastavovat barvu a jas dle libosti. Fajn, ale to je málo. Z toho se v roce 2023 na zadek nikdo neposadí.

Ukážeme si, co s takovou žárovkou dokáže každý domácí kutil, který ji na pár řádcích kódu promění v barevný semafor. Ten pak bude měnit odstín od temně modré po sytou rudou třeba podle teploty vzduchu, vlhkosti, dění na ploše počítače nebo zátěže procesoru.

Něco takového drtivá většina chytrých světýlek vůbec neumí, ale my to zvládneme levou zadní v Pythonu na velkém počítači a v Arduinu na malé destičce s Wi-Fi čipem ESP32. Takže dost řečí, jdeme na to.

Dnešní experiment provádíme s chytrými RGB LED žárovkami značky WiZ od Signify (někdejší Philips Lighting). Konkrétně pak s nejvýkonnějším 13W modelem pro závit E27.

RGB LED žárovka jako teploměr

Co kdyby měnila žárovka odstín podle teploty? Pak by mohla sloužit jako efektní indikátor třeba ve skleníku, kde nesmí klesnout teplota pod stanovenou mez, anebo v serverovně, kde by byla zase její rudá barva dostatečně důrazným varováním, že je nejvyšší čas investovat do lepšího chlazení.

Podívejte se, jak jsme proměnili lampu WiZ v teploměr:

Vyzkoušíme si to s digitálním teploměrem řady Sensirion SHT3x na některém z mnoha prototypovacích modulů, který skrze sběrnici I²C připojíme k základní desce s Wi-Fi konektivitou.

V našem seriálu pracujeme povětšinou s českou deskou ESP32-LPKit s Wi-Fi čipem ESP32, ale samozřejmě můžete sáhnout po čemkoliv jiném. K desce připojíme skrze sběrnici I²C ještě maličký 0,96“ OLED za stokorunu s řadičem SSD1306, na kterém budeme pro kontrolu vypisovat aktuální teplotu vzduchu.

f09a1b8c-b0b5-4b72-86bf-1387edf039a2
Schéma zapojení na nepájivém prototypovacím poli

Teplotu přepočítáme na barevnou škálu HSV

JAK to bude fungovat? Každou sekundu změříme teplotu vzduchu a hodnotu převedeme na nějakou hezkou barevnou škálu od nejchladnějšího po nejteplejší odstín. Vyrobí ji za nás barevný model HSV.

11a620e4-951c-4a23-a5dd-d90ad93fe7b7f06eaddc-190a-450a-a8c4-afb99a7043be
Barevný model HSV

HSV pracuje se složkami hue (odstín), saturation (sytost) a value (hodnota jasu). Pro nás je klíčová služka hue, protože ji můžeme vyjádřit ve stupních 0-360 a celý tento rozsah představuje v podstatě po sobě jdoucí barvy duhy:

  • 0°: rudá
  • 60°: žlutá
  • 120°: zelená
  • 250°: modrá
  • 300°: fialová
  • 360°: opět rudá

Aby byl efekt změny barvy redakční LED dostatečně rychlý, budeme pracovat s teplotním rozsahem 10-30 °C a přepočítáme jej na rozsah odstínu HSL 200-0°.

81fb4941-6bbd-44de-a721-dbdb0b5f67db
Odstín HSL v rozsahu 200-0°, který použijeme jako barevnou škálu teplot 10-30 °C

Získáme tak krásné a spojité přechodové barvy od světlemodré pro teplotu 10 °C a méně po rudou pro teplotu 30 °C a výše. Jamile získáme barvu v modelu HSV, přepočítáme ji na model RGB a tuto hodnotu už pošleme do žárovky.

Barvu žárovky nastavíme pomocí UDP

Pro přímou komunikaci s žárovkou použijeme síťový protokol UDP, skrze který budeme do zařízení posílat krátké textové instrukce ve formátu JSON. Komunikace se zařízeními WiZ v lokální síti LAN totiž nevyžaduje žádnou formu autorizace a není ani nikterak šifrovaná.

a1166a07-0965-434f-b66e-2eb9a706ad1b
Kontrolní výstup do sériového monitoru. Žárovka má IP adresu 172.17.16.198 a na každou instrukci s barvami RGB odpoví potvrzovací zprávou

Kdybychom chtěli nastavit barvu světla třeba na plnou zelenou ve formátu RGB s osmibitovou hloubkou kanálů (0, 255, 0), bude JSON instrukce, kterou skrze UDP odešleme do žárovky, vypadat takto:

{"method": "setPilot", "params": {"r": 0, "g": 255, "b": 0, "dimming": 100}}

Všimněte si posledního klíče dimming (100 %), kterým nastavujeme jas. V tomto příkladu tedy svítí žárovka zeleně a zároveň s plným jasem.

Program najde všechny žárovky v síti

Ještě ale musíme znát cílovou IP adresu zařízení. Buď ji vyhledáme ručně (já pro sken sítě LAN používám skvělou mobilní apku WiFiman od Ubiquiti), anebo to za nás udělá přímo program pro Arduino.

Na jeho začátku totiž odešleme na předdefinovanou broadcastovací LAN IP a UDP port 38899 instrukci:

{"method": "registration", "params": {"phoneMac": "AAAAAAAAAAAA", "register": false, "phoneIp": "1.2.3.4"}}

Podstatný je klíč registration. Klíče phoneMac a phoneIp jsou fiktivní.

Díky broadcastu se zpráva rozešle na všechna zařízení v místní síti, zareagují ale jen žárovky WiZ, protože jako jediné poslouchají na UDP portu 38899. Zprávu rozluští, všimne si požadavku registration a odpoví nám krátkou zprávou, že existují.

48534534-9784-46ff-89b2-7d8d5c2dfca4
Kontrolní výstup do sérového monitoru. Na začátku program rozeslal do sítě broadcastovou zprávu a uložil si IP adresy zařízení WiZ, která odpověděla

Jakmile zachytíme odpověď, získáme zároveň IP adresu odesílatele, no a budeme ji používat v programu jako adresáta všech dalších zpráv. Pokud bude v síti více zařízení WiZ, na broadcast mohou odpovědět také, no a náš program pak bude posílat instrukci pro nastavení barvy odpovídající teplotě i na ně.  Podle teploty by se pak nastavila všechna světla v bytě.

Zdrojový kód RGB LED teploměru

A to je celé. Níže si můžete projít kompletní kód aplikace pro vývojové prostředí Arduino s doinstalovanou podporou pro čipy ESP32. V kódu používáme knihovny ArduinoJson, Adafruit SSD1306, Adafruit GFX a ClosedCube SHT31D, které budete muset doinstalovat ze správce knihoven Arduina, anebo stáhnout z GitHubu.

// Vestavene knihovny pro
// praci s WI-Fi a UDP
#include <WiFi.h>
#include <WiFiUdp.h>

// Knihovna pro praci s JSON
// https://arduinojson.org
// Lze doinstalovat ze spravce knihoven
#include <ArduinoJson.h>

// Knihovny pro praci s 0.96" I2C OLED
// Lze doinstalovat ze spravce knihoven
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>

// Knihovny pro praci s I2C teplomery STH3x
// Lze doinstalovat ze spravce knihoven
#include <ClosedCube_SHT31D.h>

// Knihovna pro praci s C++ vektory
#include <vector>

// Funkce pro prevod barev mezi HSV a RGB
// Pouzijeme pro barevnou skalu od modre po cervenou
// Soubor hsv.h musi byt ve stejne slozce jakoc ztento zdrojovy kod
#include "hsv.h"

// Prihlasovaci udaje k Wi-Fi
const char *ssid = "Nazev2.4GHzWiFi";
const char *heslo = "velmiTajneHeslo";

// Broadcastovaci IP adresa v teto siti LAN
// Muzete zkusit i univerzalni 255.255.255.255
// Broadcastovaci IP adresa ma u beznych siti
// zpravidla formu xxx.xxx.xxx.255
// Treba: 192.168.1.255 aj.
IPAddress broadcast_ip(172, 17, 16, 255);

// Vektor pro IP adrssy nalezenych zarizeni WiZ
std::vector<IPAddress> wiz_ip;
// UDP port, na kterem poslouchaji zarizeni WiZ
uint16_t wiz_port = 38899;

// Trida pro praci s UDP, poslochaci port
// a pamet pro prichozi zpravu
WiFiUDP udp;
uint16_t udp_port = 12345;
char udp_zprava[200];

// Pamet pro JSON na 200 znaku
StaticJsonDocument<200> doc;

// Trida teplomeru SHT3x
ClosedCube_SHT31D sht3xd;

// Trida 0.96" OLED s radicwem SSD1306
Adafruit_SSD1306 display(128, 64, &Wire, -1);

// Prodleva mezi merenim teploty a aktua;lizaci zarovek WiZ
uint32_t prodleva_ms = 1000;
uint32_t t = 0;

// Textove zpravy v JSON, kterem budeme posilat do zarovky
String registrace = "{\"method\":\"registration\",\"params\":{\"phoneMac\":\"AAAAAAAAAAAA\",\"register\":false,\"phoneIp\":\"1.2.3.4\"}}";
String sablona_barva = "{\"method\":\"setPilot\",\"params\":{\"r\":%R%,\"g\":%G%,\"b\":%B%,\"dimming\":%JAS%}}";
String barva;

// Funkce pro odeslani UDP zpravy do vsech nalezenych zarovek WiZ
void odesliZpravuDoLampicek(String zprava) {
  for (auto ip = begin(wiz_ip); ip != end(wiz_ip); ++ip) {
    Serial.printf("Zprava pro %s: %s\r\n", ip->toString().c_str(), zprava.c_str());
    udp.beginPacket(*ip, wiz_port);
    udp.print(zprava);
    udp.endPacket();
    delay(10);
  }
}

// Funkce pro vyhledani vsech zarovek WiZ v teo siti LAN pomoci UDP broadcastu
void odesliUdpSken() {
  Serial.printf("Skenuji sit a hledam zarizeni WiZ na broadcast IP adrese %s...\r\n", broadcast_ip.toString().c_str());
  udp.beginPacket(broadcast_ip, wiz_port);
  udp.print(registrace);
  udp.endPacket();
}
// Funkce pro prekresleni displeje
void prekresliDisplej(String nadpis, float hodnota) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(0, 0);
  display.print(nadpis);
  display.setTextSize(3);
  display.setCursor(0, 30);
  display.print(hodnota);
  display.display();
}


// Funkce pro zmereni teploty, spocitani barvy a aktualizace zarovek
void teplomer() {
  // Zkopirujeme si sablonu s JSON pro nastaveni barev
  String barva = sablona_barva;
  // Zmerime teplotu
  SHT31D cidlo = sht3xd.readTempAndHumidity(SHT3XD_REPEATABILITY_LOW, SHT3XD_MODE_CLOCK_STRETCH, 50);
  float teplota = cidlo.t;
  // Nakreslime udaj o teplote na displej
  prekresliDisplej("TEPLOTA", teplota);
  // Prepocitame rozsah teplot 10-30 na rozsah odstinu HSV 200-0
  float hue = map(constrain((int)teplota, 10, 30), 10, 30, 200, 0);
  // Prepocitame model HSV s hodnotou saturace 100 % a jasu 10 % na RGB
  // Funkce pro prepocet jsou v druhem souboru naseho projektu hsv.h
  struct HSV hsv = {hue, 1.0, 0.1};
  struct RGB rgb = HSV2RGB(hsv);
  // V textovem retezci s sablonkou JSONu pro nastaveni barvy nahradime zastupne vyrazy hodnotami R, G, B a jasem zarovky na 100 %
  barva.replace("%R%", String(rgb.R));
  barva.replace("%G%", String(rgb.G));
  barva.replace("%B%", String(rgb.B));
  barva.replace("%JAS%", "100");
  // Odesleme textovy retezec skrze UDP do detekovanych zarovek WiZ
  odesliZpravuDoLampicek(barva);
}

// Hlavni funkce setup se spusti hned na zacatku
void setup() {
  Serial.begin(115200); // Nastartovani seriove linky rychlosti 115200 b/s
  delay(1000);
  Wire.begin(); // Nastartovani sbernice I2C
  sht3xd.begin(0x44); // Pripojeni k teplomeru SHT3x na I2C adrese 0x44
  display.begin(SSD1306_SWITCHCAPVCC, 0x3c); // Pripojeni k OLED displeji na I2C adrese 0x3c
  // Smazani displeje
  display.clearDisplay();
  display.display();
  barva.reserve(500); // Rezwervuj 500 bajtu pro textovy retezec barva
  Serial.println("*** START PROGRAMU ***");
  // Pripojime se k Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, heslo);
  Serial.printf("Pripojuji se k %s...", ssid);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
    Serial.print(".");
  }
  Serial.println(" OK");
  udp.begin(udp_port); // Nastartujeme UDP
  Serial.printf("Moje IP adresa: %s\r\n", WiFi.localIP().toString().c_str());
  Serial.printf("UDP posloucha na portu %d\r\n\r\n", udp_port);
}

// Funkce loop se opakuje stale dokola
void loop() {
  // Pokud zatim neznam ani jednu IP adresu lampicky WiZ,
  // spustim broadcastovy UDP sken,
  // na ktery by mely lampicky odpovedet
  if (wiz_ip.size() == 0) {
    odesliUdpSken();
    delay(1000);
  }
  // Zjistim, jestli dorazil nejaky novy UDP datagram/packet
  int paket = udp.parsePacket();
  // Pokud ano, vypisu jeho obsah a IP adresu odesilatele
  if (paket) {
    IPAddress ip = udp.remoteIP();
    Serial.printf("Odpoved od %s: ", ip.toString().c_str());
    if (udp.read(udp_zprava, 200) == 0) Serial.printf("Zadna data\r\n");
    else {
      Serial.printf("%s\r\n", udp_zprava);
      // Pokusim se zpracovat JSON
      if (!deserializeJson(doc, udp_zprava)) {
        // Pokud ma klic "method" hodnotu "registration", je to odpoved lampicky na broadcastovy sken
        if (!doc["method"].isNull()) {
          if (strcmp(doc["method"].as<const char *>(), "registration") == 0) {
            // Pokud je klic "result/success" roven true,
            // Vypisu MAC a IP lampicky a IP zaroven ulozim do pole lampicek
            if (doc["result"]["success"]) {
              Serial.println("Nasel jsem zarizeni WiZ:");
              Serial.printf(" - IP: %s\r\n", ip.toString().c_str());
              Serial.printf(" - MAC: %s\r\n", doc["result"]["mac"].as<const char *>());
              wiz_ip.push_back(udp.remoteIP());
            }
          }
        }
      }
    }
  }

  // Se stanovenou prodlevou spoustim funkci teplomer
  if (millis() - t > prodleva_ms) {
    teplomer();
    t = millis();
  }
}

Zdrojový kód hlavičkového souboru hsv.h so funkcemi pro přepočet modelu HSV na RGB. Soubor hsv.h musí bát ve stejném adresáři jako halvní zdrojový kód:

// Struktua s 8bitovymi kanaly modelu RGB
struct RGB
{
  uint8_t R;
  uint8_t G;
  uint8_t B;
};

// Struktura se slozkami modelu HSV
struct HSV
{
  double H;
  double S;
  double V;
};

// Funbcke pro prevod HSV na RGB
struct RGB HSV2RGB(struct HSV hsv) {
  double r = 0, g = 0, b = 0;

  if (hsv.S == 0)
  {
    r = hsv.V;
    g = hsv.V;
    b = hsv.V;
  }
  else
  {
    int i;
    double f, p, q, t;

    if (hsv.H == 360)
      hsv.H = 0;
    else
      hsv.H = hsv.H / 60;

    i = (int)trunc(hsv.H);
    f = hsv.H - i;

    p = hsv.V * (1.0 - hsv.S);
    q = hsv.V * (1.0 - (hsv.S * f));
    t = hsv.V * (1.0 - (hsv.S * (1.0 - f)));

    switch (i)
    {
      case 0:
        r = hsv.V;
        g = t;
        b = p;
        break;

      case 1:
        r = q;
        g = hsv.V;
        b = p;
        break;

      case 2:
        r = p;
        g = hsv.V;
        b = t;
        break;

      case 3:
        r = p;
        g = q;
        b = hsv.V;
        break;

      case 4:
        r = t;
        g = p;
        b = hsv.V;
        break;

      default:
        r = hsv.V;
        g = p;
        b = q;
        break;
    }

  }

  struct RGB rgb;
  rgb.R = r * 255;
  rgb.G = g * 255;
  rgb.B = b * 255;

  return rgb;
}

V další kapitole proměníme žárovku ve vlhkoměr!

Diskuze (16) Další článek: Meta má vlastní konverzační AI. LLaMA je na úrovni GPT-3, ale provozovat ji může každý i doma

Když navštívíte internetový obchod přes náš katalog zboží, Živě za to může získat provizi. Službu Zboží Živě provozujeme ve spolupráci s Heureka.cz.

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