ESP32 - ovládání NeoPixel LED pásku

25.12.2023 Arduino #esp32 #neopixel #rgb #led

Na LED pásku NeoPixel můžete ovládat barvu a jas každé LED samostatně. Zde je základní návod, jak používat ESP32 k ovládání NeoPixel RGB LED pásku. K ovládání všech LED NeoPixel pásku potřebujete pouze jeden pin na ESP32.


Již dříve byl uveden návod jak, zprovoznit LED pásky na Arduino UNO. Nyní je zde využita platforma ESP32.

NeoPixels, také známé jako WS2812B LED, lze použít ve spojení s mikrokontrolérem ESP32 k vytvoření barevných a přizpůsobitelných světelných efektů. ESP32 může ovládat NeoPixel prostřednictvím digitálních výstupních pinů pomocí knihovny NeoPixel pro ESP32.

Chcete-li používat NeoPixely s ESP32, musíte nejprve připojit NeoPixely k digitálnímu výstupnímu pinu ESP32. To lze provést připojením datového vstupního kolíku prvního NeoPixelu k digitálnímu výstupnímu kolíku ESP32 a připojením datového výstupního kolíku posledního NeoPixelu k datovému vstupnímu pinu prvního, čímž vznikne řetězec NeoPixelů.

Jakmile jsou NeoPixel připojeny, můžete pomocí knihovny NeoPixel ovládat barvu a jas každého jednotlivého NeoPixelu. Knihovna poskytuje různé funkce, jako je nastavení barvy, jasu a animace NeoPixelů. Pomocí knihovny a možností ESP32 můžete vytvářet širokou škálu světelných efektů.

Je důležité poznamenat, že NeoPixely vyžadují k provozu velké množství proudu, proto byste měli pro NeoPixely použít samostatný zdroj napájení a také byste měli použít obvod pro řazení úrovně nebo napěťový dělič, aby nedošlo k poškození ESP32.

Schéma zapojení

Programový kód

Kód je rozdělen do dvou souborů - main.cpp a htmlui.h

Main.cpp

Jádro aplikace. Využívá se zde i knihovny pro práci s JSON pro ukládání konfigurace LED pásku. V tuto chvíli je zde uložena naposledy uložená animace. Do GUI aplikace, lze přidat další pole zadávání potřebných parametrů. K ulože níse využívá uložiště SPIFFS.

#include "FS.h"
#include "SD.h"
#include "SPI.h"

#include "Arduino.h"
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ESP32AnalogRead.h>
#include <math.h>

#include <SPIFFS.h>
#include <ArduinoJson.h>

#include <PubSubClient.h>

#include <Wire.h>
#include <Adafruit_NeoPixel.h>
#include <htmlui.h>

#define FORMAT_SPIFFS_IF_FAILED true
#define PIN 4

int countled = 30;
int anim = 0;


const char* ssid = "NeoPixel";
const char* password = "12345678";

AsyncWebServer server(80);

String fileConfig = "/config.json";
using namespace std;
vector<String> v;

String readFile(fs::FS &fs, String filename){
  Serial.println("readFile -> Reading file: " + filename);

  File file = fs.open(filename);
  if(!file || file.isDirectory()){
    Serial.println("readFile -> failed to open file for reading");
    return "";
  }

  String fileText = "";
  while(file.available()){
    fileText = file.readString();
  }

  file.close();
  return fileText;
}

bool readConfig(String file_name) {
  String file_content = readFile(SPIFFS, file_name);

  int config_file_size = file_content.length();
  Serial.println("Config file size: " + String(config_file_size));

  if(config_file_size > 1024) {
    Serial.println("Config file too large");
    return false;
  }
  StaticJsonDocument<1024> doc;
  auto error = deserializeJson(doc, file_content);
  if ( error ) { 
    Serial.println("Error interpreting config file");
    return false;
  }

  const int _countled = doc["countled"];
  const int _anim = doc["anim"];

  countled = _countled;
  anim = _anim;

  Serial.print("Read config:");   
  Serial.print(countled);
  Serial.print(" - ");
  Serial.println(anim);  
  return true;
}

void writeFile(fs::FS &fs, String filename, String message){
  Serial.println("writeFile -> Writing file: " + filename);

  File file = fs.open(filename, FILE_WRITE);
  if(!file){
    Serial.println("writeFile -> failed to open file for writing");
    return;
  }
  if(file.print(message)){
    Serial.println("writeFile -> file written");
  } else {
    Serial.println("writeFile -> write failed");
  }
  file.close();
}

bool saveConfig(String file_name) {
  StaticJsonDocument<1024> doc;

  // write variables to JSON file
  doc["countled"] = countled;
  doc["anim"] = anim;

  // write config file
  String tmp = "";
  serializeJson(doc, tmp);
  writeFile(SPIFFS, file_name, tmp);

  return true;
}

String valConfig(){
  readConfig(fileConfig);

  String result = String(countled) +"|"+ String(anim);
  Serial.print("Data send to config UI: ");
  Serial.println(result);
  return result;
}

// Parameter 1 = number of pixels in strip
// Parameter 2 = Arduino pin number (most are valid)
// Parameter 3 = pixel type flags, add together as needed:
//   NEO_KHZ800  800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
//   NEO_KHZ400  400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
//   NEO_GRB     Pixels are wired for GRB bitstream (most NeoPixel products)
//   NEO_RGB     Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
//   NEO_RGBW    Pixels are wired for RGBW bitstream (NeoPixel RGBW products)
Adafruit_NeoPixel strip = Adafruit_NeoPixel(countled, PIN, NEO_GRB + NEO_KHZ800);

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if(WheelPos < 85) {
    return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  if(WheelPos < 170) {
    WheelPos -= 85;
    return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
  WheelPos -= 170;
  return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}

// Fill the dots one after the other with a color
void colorWipe(uint32_t c, uint8_t wait) {
  for(uint16_t i=0; i<strip.numPixels(); i++) {
    strip.setPixelColor(i, c);
    strip.show();
    delay(wait);
  }
}

void rainbow(uint8_t wait) {
  uint16_t i, j;

  for(j=0; j<256; j++) {
    for(i=0; i<strip.numPixels(); i++) {
      strip.setPixelColor(i, Wheel((i+j) & 255));
    }
    strip.show();
    delay(wait);
  }
}

// Slightly different, this makes the rainbow equally distributed throughout
void rainbowCycle(uint8_t wait) {
  uint16_t i, j;

  for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
    for(i=0; i< strip.numPixels(); i++) {
      strip.setPixelColor(i, Wheel(((i * 256 / strip.numPixels()) + j) & 255));
    }
    strip.show();
    delay(wait);
  }
}

//Theatre-style crawling lights.
void theaterChase(uint32_t c, uint8_t wait) {
  for (int j=0; j<10; j++) {  //do 10 cycles of chasing
    for (int q=0; q < 3; q++) {
      for (uint16_t i=0; i < strip.numPixels(); i=i+3) {
        strip.setPixelColor(i+q, c);    //turn every third pixel on
      }
      strip.show();

      delay(wait);

      for (uint16_t i=0; i < strip.numPixels(); i=i+3) {
        strip.setPixelColor(i+q, 0);        //turn every third pixel off
      }
    }
  }
}

//Theatre-style crawling lights with rainbow effect
void theaterChaseRainbow(uint8_t wait) {
  for (int j=0; j < 256; j++) {     // cycle all 256 colors in the wheel
    for (int q=0; q < 3; q++) {
      for (uint16_t i=0; i < strip.numPixels(); i=i+3) {
        strip.setPixelColor(i+q, Wheel( (i+j) % 255));    //turn every third pixel on
      }
      strip.show();

      delay(wait);

      for (uint16_t i=0; i < strip.numPixels(); i=i+3) {
        strip.setPixelColor(i+q, 0);        //turn every third pixel off
      }
    }
  }
}

void animate(int num){
switch (num){
    case 1:
    // Some example procedures showing how to display to the pixels:
      colorWipe(strip.Color(255, 0, 0), 50); // Red
      colorWipe(strip.Color(0, 255, 0), 50); // Green
      colorWipe(strip.Color(0, 0, 255), 50); // Blue
      //colorWipe(strip.Color(0, 0, 0, 255), 50); // White RGBW
      // Send a theater pixel chase in...
      theaterChase(strip.Color(127, 127, 127), 50); // White
      theaterChase(strip.Color(127, 0, 0), 50); // Red
      theaterChase(strip.Color(0, 127, 0), 50); // Green
      theaterChase(strip.Color(0, 0, 127), 50); // Blue

      rainbow(20);
      rainbowCycle(20);
      theaterChaseRainbow(50);
      Serial.println("Animate 1");
      break;
    case 2:
      rainbowCycle(20);
      Serial.println("Animate 2");
      break;
    case 3:
      theaterChaseRainbow(50);
      Serial.println("Animate 3");
      break;  
    case 4:
      rainbow(20);
      Serial.println("Animate 4");
      break;
    case 5:
      theaterChase(strip.Color(127, 127, 127), 50);
      Serial.println("Animate 5");
      break;  
    case 6:
      theaterChase(strip.Color(127, 0, 0), 50);
      Serial.println("Animate 6");
      break; 
    case 7:
      theaterChase(strip.Color(0, 127, 0), 50);
      Serial.println("Animate 7");
      break;   
    case 8:
      theaterChase(strip.Color(0, 0, 127), 50);
      Serial.println("Animate 8");
      break;   
    case 9:
      colorWipe(strip.Color(255, 0, 0), 50);
      Serial.println("Animate 9");
      break; 
    case 10:
      colorWipe(strip.Color(0, 255, 0), 50);
      Serial.println("Animate 10");
      break;  
    case 11:
      colorWipe(strip.Color(0, 0, 255), 50);
      Serial.println("Animate 11");
      break;   
  }
}

// IMPORTANT: To reduce NeoPixel burnout risk, add 1000 uF capacitor across
// pixel power leads, add 300 - 500 Ohm resistor on first pixel's data input
// and minimize distance between Arduino and first pixel.  Avoid connecting
// on a live circuit...if you must, connect GND first.

void setup() {
  Serial.begin(115200); 
  // Server
  WiFi.softAP(ssid, password);

  IPAddress IP = WiFi.softAPIP();
  Serial.print("AP IP address: ");
  Serial.println(IP);

  if(!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)){
    Serial.println("SPIFFS Mount Failed");
    return;
  }
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html);
  });

  server.on("/getdata", HTTP_GET, [] (AsyncWebServerRequest *request) {
    request->send(200, "text/plain", valConfig().c_str());
  });

  server.on("/setdata", HTTP_GET, [] (AsyncWebServerRequest *request) {
    int params = request->params();
    for(int i=0;i<params;i++){
      AsyncWebParameter* p = request->getParam(i);
      if (p->name() == "countled" ) {
        countled = p->value().toInt();
      }
      if (p->name() == "animation") {
        anim = p->value().toInt();
      }
    }

    saveConfig(fileConfig);
    request->send(200, "text/plain", "OK");

    //animate(anim);
  });

  server.begin();
  Serial.println("HTTP server started");

  readConfig(fileConfig);

  strip.begin();
  strip.setBrightness(50);
  strip.show(); // Initialize all pixels to 'off'
}

void loop() {
  animate(anim);
}

htmlui.h

Rozhraní aplikace v podobě html stránky. Je zde k dispozici ořezaný CSS bootstrap.

const char index_html[] PROGMEM = R"rawliteral(
 <!DOCTYPE HTML><html>
    <head>
      <title>ESP Web Server</title>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <style>

            html {
                box-sizing: border-box;
                -ms-overflow-style: scrollbar;
            }

                *,
                *::before,
                *::after {
                box-sizing: inherit;
            }
            .container{
                width: 100%;
                padding-right: 15px;
                padding-left: 15px;
                margin-right: auto;
                margin-left: auto;
            }

            .row {
                display: -ms-flexbox;
                display: flex;
                -ms-flex-wrap: wrap;
                flex-wrap: wrap;
                margin-right: -15px;
                margin-left: -15px;
            }

            .no-gutters {
                margin-right: 0;
                margin-left: 0;
            }

            .no-gutters > .col,
            .no-gutters > [class*="col-"] {
                padding-right: 0;
                padding-left: 0;
            }

            .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,
            .col-auto {
                position: relative;
                width: 100%;
                padding-right: 15px;
                padding-left: 15px;
            }

            .col {
                -ms-flex-preferred-size: 0;
                flex-basis: 0;
                -ms-flex-positive: 1;
                flex-grow: 1;
                max-width: 100%;
            }
            .col-auto {-ms-flex: 0 0 auto; flex: 0 0 auto;width: auto;max-width: 100%;}
            .col-1 {-ms-flex: 0 0 8.333333%;flex: 0 0 8.333333%; max-width: 8.333333%;}
            .col-2 {-ms-flex: 0 0 16.666667%;flex: 0 0 16.666667%;max-width: 16.666667%;}
            .col-3 {-ms-flex: 0 0 25%;flex: 0 0 25%; max-width: 25%;}
            .col-4 { -ms-flex: 0 0 33.333333%;flex: 0 0 33.333333%; max-width: 33.333333%;}
            .col-5 {-ms-flex: 0 0 41.666667%;flex: 0 0 41.666667%;max-width: 41.666667%; }
            .col-6 {-ms-flex: 0 0 50%;flex: 0 0 50%; max-width: 50%;}
            .col-7 {-ms-flex: 0 0 58.333333%;flex: 0 0 58.333333%;max-width: 58.333333%;}
            .col-8 {-ms-flex: 0 0 66.666667%;flex: 0 0 66.666667%;max-width: 66.666667%;}
            .col-9 { -ms-flex: 0 0 75%;flex: 0 0 75%; max-width: 75%; }
            .col-10 {-ms-flex: 0 0 83.333333%;flex: 0 0 83.333333%;max-width: 83.333333%;}
            .col-11 {-ms-flex: 0 0 91.666667%;flex: 0 0 91.666667%;max-width: 91.666667%;}
            .col-12 {-ms-flex: 0 0 100%;flex: 0 0 100%; max-width: 100%; }

            .m-0 {margin: 0 !important;}
            .mt-0,.my-0 {margin-top: 0 !important; }
            .mr-0,.mx-0 {margin-right: 0 !important;}
            .mb-0,.my-0 {margin-bottom: 0 !important;}
            .ml-0,.mx-0 {margin-left: 0 !important;}
            .m-1 { margin: 0.25rem !important;}
            .mt-1, .my-1 { margin-top: 0.25rem !important; }
            .mr-1,.mx-1 { margin-right: 0.25rem !important;}
            .mb-1, .my-1 {margin-bottom: 0.25rem !important; }
            .ml-1,.mx-1 {margin-left: 0.25rem !important; }
            .m-2 {margin: 0.5rem !important;}
            .mt-2,.my-2 {margin-top: 0.5rem !important;}
            .mr-2,.mx-2 { margin-right: 0.5rem !important; }
            .mb-2,.my-2 {margin-bottom: 0.5rem !important;}
            .ml-2,.mx-2 {margin-left: 0.5rem !important;}
            .m-3 { margin: 1rem !important;}
            .mt-3,.my-3 {margin-top: 1rem !important;}
            .mr-3,.mx-3 {margin-right: 1rem !important;}
            .mb-3,.my-3 {margin-bottom: 1rem !important;}
            .ml-3, .mx-3 {margin-left: 1rem !important; }
            .m-4 { margin: 1.5rem !important; }
            .mt-4, .my-4 {margin-top: 1.5rem !important;}
            .mr-4,.mx-4 { margin-right: 1.5rem !important;}
            .mb-4, .my-4 {margin-bottom: 1.5rem !important; }
            .ml-4,.mx-4 {margin-left: 1.5rem !important;}
            .m-5 {margin: 3rem !important; }
            .mt-5,.my-5 {margin-top: 3rem !important;}
            .mr-5,.mx-5 {margin-right: 3rem !important;}
            .mb-5, .my-5 { margin-bottom: 3rem !important;}
            .ml-5,.mx-5 { margin-left: 3rem !important; }
            .p-0 { padding: 0 !important;}
            .pt-0, .py-0 {padding-top: 0 !important;}
            .pr-0,.px-0 {padding-right: 0 !important;}
            .pb-0,.py-0 {padding-bottom: 0 !important;}
            .pl-0,.px-0 {padding-left: 0 !important;}
            .p-1 { padding: 0.25rem !important;}
            .pt-1, .py-1 {padding-top: 0.25rem !important;}
            .pr-1, .px-1 {padding-right: 0.25rem !important;}
            .pb-1,.py-1 { padding-bottom: 0.25rem !important;}
            .pl-1,.px-1 {padding-left: 0.25rem !important;}
            .p-2 {padding: 0.5rem !important;}
            .pt-2,.py-2 {padding-top: 0.5rem !important;}
            .pr-2, .px-2 {padding-right: 0.5rem !important;}
            .pb-2, .py-2 {padding-bottom: 0.5rem !important;}
            .pl-2,.px-2 {padding-left: 0.5rem !important;}
            .p-3 {padding: 1rem !important;}
            .pt-3, .py-3 {padding-top: 1rem !important;}
            .pr-3,.px-3 {padding-right: 1rem !important;}
            .pb-3, .py-3 {padding-bottom: 1rem !important; }
            .pl-3,.px-3 { padding-left: 1rem !important;}
            .p-4 {padding: 1.5rem !important; }
            .pt-4, .py-4 {padding-top: 1.5rem !important;}
            .pr-4,.px-4 {padding-right: 1.5rem !important;}
            .pb-4,.py-4 {padding-bottom: 1.5rem !important;}
            .pl-4,.px-4 {padding-left: 1.5rem !important; }
            .p-5 { padding: 3rem !important; }
            .pt-5, .py-5 {padding-top: 3rem !important;}
            .pr-5,.px-5 {padding-right: 3rem !important;}
            .pb-5,.py-5 {padding-bottom: 3rem !important;}
            .pl-5,.px-5 {padding-left: 3rem !important;}
            body{
                font-family: Arial, Helvetica, sans-serif;
            }
            .btn {
                display: inline-block;
                font-weight: 400;
                color: #212529;
                text-align: center;
                vertical-align: middle;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
                background-color: transparent;
                border: 1px solid transparent;
                padding: 0.375rem 0.75rem;
                font-size: 1rem;
                line-height: 1.5;
                border-radius: 0.25rem;
                transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
            }
            .btn:hover {
                color: #212529;
                text-decoration: none;
            }
            .btn-primary {color: #fff;background-color: #007bff; border-color: #007bff;}
            .btn-primary:hover {color: #fff;background-color: #0069d9;border-color: #0062cc;}
            .btn-success {color: #fff;background-color: #28a745;border-color: #28a745;}
            .btn-success:hover { color: #fff;background-color: #218838;border-color: #1e7e34;}
            .btn-danger {color: #fff;background-color: #d9534f;border-color: #d43f3a;}
            .btn-danger:hover {color: #fff;background-color: #c9302c;border-color: #ac2925;}

            input,button,select,optgroup,textarea {  margin: 0;font-family: inherit;font-size: inherit;line-height: inherit;}
            textarea {overflow: auto;resize: vertical;}
            textarea.form-control {height: auto;}

            .toggle {position: relative; display: inline-block; width: 48px; height:26px; float:right; margin-top:20px;} 
            .toggle input {display: none;}
            .shade {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; }
            .shade:before {position: absolute; content: ""; height: 18px; width: 18px; left:4px; bottom: 4px; background-color: #fff; -webkit-transition: .4s; transition: .4s;}
            input:checked+.shade {background-color: #2196F3}
            input:checked+.shade:before {-webkit-transform: translateX(22px); -ms-transform: translateX(22px); transform: translateX(22px)}
      </style>
    </head>
    <body onload = "iniPage()">
        <div class="container">
          <div id="content">
              <div class="row">
                <div class="col-8">
                  <h2>Setting NeoPixel</h2>
                </div>
              </div>
              <!--
              <div class="row">  
                <label class="col-4">Count LED:</label>
                <input type="text" id="countled">
              </div>
              //-->
              <div class="row mt-4">
                <label class="col-4 ">Animation:</label>
                <select id="animation" onChange="sendData()";>
                  <option value="1">All animate</option>
                  <option value="2">Rainbow Cycle</option>
                  <option value="3">Theater Chase Rainbow</option>
                  <option value="4">Rainbow</option>
                  <option value="5">Theater Chase White</option>
                  <option value="6">Theater Chase Red</option>
                  <option value="7">Theater Chase Green</option>
                  <option value="8">Theater Chase Blue</option>
                  <option value="9">Color Wipe Red</option>
                  <option value="10">Color Wipe Green</option>
                  <option value="11">Color Wipe Blue</option>
                </select>
              </div>
              <!--
              <div class="row mt-4">
                <div class="col-12">
                  <input type="button" name="senddata" id="senddata" value="Send" onclick="sendData();" class="btn btn-success">
                </div>
              </div>
              //-->
          </div>
        </div>
    <script>

    function sendData() {
      //var countled = document.getElementById("countled").value;
      var countled = 0;
      var animation = document.getElementById("animation").value;

      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/setdata?countled="+countled+"&animation="+animation, true); 
      xhr.send();
    }

    function iniPage(){
      var xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          var stateArr = this.responseText.split("|");
          //document.getElementById("countled").value = stateArr[0];
          document.getElementById("animation").value = stateArr[1];
        }
      };
      xhttp.open("GET", "/getdata", true);
      xhttp.send();     
    }

    </script>
    </body>
    </html>
)rawliteral";

Aplikace funguje tak, že je k dispozici rozhraní dostupné na IP adrese mikrokontroléru. Z výběrového pole, pak lze zvolit animaci LED pásku. Má to jeden nedostatek a to implementace přerušení, protože při náročnějších cyklech se změna neprojeví hned, ale to je již detail. Jednotlivé animace jsou v dílčích fiunkcích.