For one of our projects at circuit digest, we wanted to build a Persistence of Vision Display or POV display with WS2812B LEDs. While making the project, our only focus was on how we could make it simpler along with less hardware. As a result, we chose some simple WS2812B RGB LEDs and an ESP8266-01 as the brains for this project because they are lightweight and cost less, but little did we know that this hardware design would invite plenty of troubles.
So, in this article, we'll show you how to avoid making a POV display with WS2812B RGB LEDs due to timing concerns, as well as how to use this simple hardware design as a random pattern generator with its own web server. Based on this experience we also built a mono-color POV display using ESP32 which works very well, you can also check that out if intrested. So without further ado let's get right into it.
In one of our previous projects, we have built a simple Arduino Propeller LED Display that uses POV technology to display text. You can check that out if you want to print text on a self-made POV display. Other than that we have built many projects with WS2812B RGB LEDS. Some interesting ones are linked down below.
- Arduino Digital Clock, but It’s Unique and You Will Need It
- ESP8266 and Neopixel LED Strips Based RGB Night Lamp
- Arduino Based Decorative Christmas Tree with Neopixel and JQ6500 Voice Module
- DIY LED Music Spectrum using NeoPixel RGB LEDs and ARM Microcontroller
- Touch Capacitive Based PCB Light Panel to Control NeoPixel LED Strip
Components Required to Build Arduino Based POV Display
The components required to build the POV Display are easy to acquire and cheap. The component list is given below.
- ESP8266 - 01
- WS2812B RGB LEDS
- 3.7V 400mA Lithium Battery
- 12V DC Motor
- Perfboard
- Wires
- Connectors
- Two Component Adhesive
- And a 12V battery (to supply the motor)
ESP8266 POV Display Circuit Diagram
The schematic diagram for the WS2812B and ESP8266-01 based POV Display is shown below and as you can see it's pretty simple and easy to understand.
The brain of the circuit is the ESP8266-01 module which is powered by a 3.7V, 400mA Lithium battery, and the WS2812B LEDs are connected to the GPIO0 of the ESP8266-01 module. There are a total of 8 LEDs in the Neopixels, and we utilised a self-module to power them. If you don't have the module you can use LED strips as a replacement. In this circuit, we have used the GPIO0 instead of GPIO2 because after testing a lot of ESP8266-01 modules we have found that the GPIO0 is more stable than the GPIO2 of the module. Please note that while making this project we did not put any circuitry for charging the battery, because it will make the board heavy and it will be very difficult to drive that thing.
So for any POV Display, we need a powerful motor to work with and for that, we have used a 200RPM gear motor. We utilized it since we had multiple gear motors laying around at the time of the project, so we disassembled one to get the motor out, as shown below.-
To mount the motor, we have used a big aluminum channel with a small 3D printed L-shaped holder that is holding the motor to the aluminum channel. An image of the 3D printed channel is shown below-
To power the motor, we're using a 5V power supply from our lab bench.
Arduino Code to Build WS2812 Neopixels based POV Display
Now that we know all the hardware and working of this project, we can move on to the code portion of this project. The code portion for this project is not small but it's easy to understand.
We start off our code by including all the required libraries and defining all the necessary pins and parameters that we need to run WS2812B LEDs WI-Fi and OTA.
// Load Wi-Fi library, Fast-LED Library and Custom OTA Library #include <ESP8266WiFi.h> #include <FastLED.h> #include "OTA.h" #define LED_PIN 0 // LED is connected to the IO-0 of the ESP8266-01 #define NUM_LEDS 8 // We have 8 LEDs in the strip #define BRIGHTNESS 50 // set the brightness for the LEDs #define LED_TYPE WS2812 // We are using WS2812B LEDs #define COLOR_ORDER GRB // Colour order of the LEDs #define UPDATES_PER_SECOND 100 // FPS
Next, we create objects for the RGB LEDs and we also create the server object on port 80.
CRGB leds[NUM_LEDS]; // Create LED array for manipulation CRGBPalette16 currentPalette; // set color palet type TBlendType currentBlending;// set blend mode extern CRGBPalette16 myRedWhiteBluePalette; // create object for different palet extern const TProgmemPalette16 myRedWhiteBluePalette_p PROGMEM; // Set web server port number to 80 WiFiServer server(80);
Next, we declare all the required variables, including variables that stores the button states named Animation_1, Animation_2, and Animation_3, and variable to store the total html webpage which is named header. Finally, there are two sets of variable that is used to store the SSID and password for the Wi-Fi hotspot.
// variables to store the current Button state String Animation_1 = "off"; String Animation_2 = "off"; String Animation_3 = "off"; // Variable to store the HTTP request String header; const char* ssid = "Your SSID"; const char* password = "Your Password"; unsigned long currentTime = millis(); // Previous time unsigned long previousTime = 0; // Define timeout time in milliseconds (example: 2000ms = 2s) const long timeoutTime = 2000;
Next, we have our setup() function. In the setup function, we have our usual stuff like enabling the serial for debugging, setup OTA, setup the Neopixels, and setting up the webserver.
void setup() { Serial.begin(115200); Serial.println("Booting"); setupOTA("POV_Display", ssid , password); delay( 300 ); // power-up safety delay FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip ); FastLED.setBrightness( BRIGHTNESS ); currentPalette = RainbowColors_p; currentBlending = LINEARBLEND; server.begin(); }
Next, we have a list of patterns which we have defined with a pointer that will be called on when we switch between different patterns.
// List of patterns to cycle through. Each is defined as a separate function below. typedef void (*SimplePatternList[])(); SimplePatternList gPatterns = { rainbow, rainbowWithGlitter, confetti, sinelon, juggle, bpm }; uint8_t gCurrentPatternNumber = 0; // Index number of which pattern is current uint8_t gHue = 0; // rotating "base color" used by many of the patterns
Next, we have our loop section. In the loop section, we call the ArduinoOTA.handle() function so that we could do an OTA update of the ESP while the ESP is mounted on the PCB and attached to the motor. Next, we declare and define the WiFiClient object client, and check if any new connections are available or not.
void loop() { while ((unsigned long) millis() < 10000) { ArduinoOTA.handle(); delay(10); } ArduinoOTA.handle(); WiFiClient client = server.available(); // Listen for incoming clients
Now, if a new client connects and the statement becomes true, we take the current timestamp and store it in the currentTime variable and next we store the same data to the previous time variable for later use. Next, we print a statement to the serial monitor window to notify the user that a new user got connected and requested the webpage. We also define a string type variable to hold that incoming data so that we can parse the data later and check if the request is exactly what we want.
if (client) { // If a new client connects, currentTime = millis(); previousTime = currentTime; Serial.println("New Client"); // print a statement so the user knows that a new client got connected String incomingData = ""; // make a String to hold incoming data from client
Next, we define a while loop, this while loop is only here to serve the timeout sequence. If the timeout delay exceeds, the while loop will simply break. Inside the while loop, we simply check if any data is available on the client-side, if so then we create a local variable named read_byte and store the entire bitstream that is coming from the client. Then we print out the entire bitstream on the serial monitor window for debugging. Finally, we store the total bitstream inside the string type variable called header.
while (client.connected() && currentTime - previousTime <= timeoutTime) { // client connects and request the page from server currentTime = millis(); if (client.available()) { // if clent sends a byte to read char read_byte = client.read(); // read that byte from client Serial.write(read_byte); // print it to the serial monitor for debugging header += read_byte;
Next, we check if our byte variable contains a “\n” character or not. If so, then we can say that it's the end of the HTTP request. So now we can send our webpage as a response. In the next section of the code, we simply send the entire web page as a response. The response always starts with an OK response so at first, we send that when we know the request is complete. After that, we send other necessary parameters.
if (incomingData.length() == 0) { // HTTP response code always start with a oK statement (e.g. HTTP/1.1 200 OK) client.println("HTTP/1.1 200 OK"); client.println("Content-type:text/html"); client.println("Connection: close"); client.println();
The next part of the code is the most important part because in this section we have received the data from our web server and updated the string type variables that we have declared earlier and in the loop function, we are checking this variable and updating the LEDs.
//Set Different Mode For the LEDs if (header.indexOf("GET /Animation_1/on") >= 0) { // Serial.println("Animation 1 on"); Animation = "on"; } else if (header.indexOf("GET /Animation_1/off") >= 0) { // Serial.println("Animation 1 off"); Animation = "off"; } else if (header.indexOf("GET /Animation_2/on") >= 0) { // Serial.println("Animation 2 on"); Animation_2 = "on"; } else if (header.indexOf("GET /Animation_2/off") >= 0) { // Serial.println("Animation 2 off"); Animation = "off"; } else if (header.indexOf("GET /Animation_3/on") >= 0) { // Serial.println("Animation 3 on"); Animation = "on"; } else if (header.indexOf("GET /Animation_3/off") >= 0) { // Serial.println("Animation 3 off"); Animation = "off"; }
In the next part, we send the whole webpage to the client as a response and in this section, we mainly send the HTML and CSS files using the client.println() function.
client.println("<!DOCTYPE html><html>"); client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"); client.println("<link rel=\"icon\" href=\"data:,\">"); client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}"); client.println(".button { background-color: #3bc041; color: #ffffff; padding: 9px 30px ; text-decoration: none; font-size: 30px; margin: 2px ; border-radius: 5px ; border: 1px solid #ddffdf; box-shadow: 1px 2px 3px #0000001c; "); client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}"); client.println(".button:hover{ background-color: #31b237; }"); client.println(".button2 { background-color: #aaaaaa; border: 1px solid #bbbbbb; }</style></head>"); // This is the heading of the webpage client.println("<body><h1>ESP32 Web Server</h1>");
In the next part of the code, we update the webpage with the current state of the button and check for any changes in the button state. If a state change has occurred, we update the changes with the client.println() method. We do this for all three buttons. Finally, we end the response by sending a blank statement.
// Display current state, of the button 1 client.println("<p>Animation 1 - State " + Animation_1 + "</p>"); if (Animation_1 == "off") client.println("<p><a href=\"/Animation_1/on\"><button class=\"button\">ON</button></a></p>"); else client.println("<p><a href=\"/Animation_1/off\"><button class=\"button button2\">OFF</button></a></p>"); // Display current state, of the button 2 client.println("<p>Animation 2 - State " + Animation_2 + "</p>"); if (Animation_2 == "off") client.println("<p><a href=\"/Animation_2/on\"><button class=\"button\">ON</button></a></p>"); else client.println("<p><a href=\"/Animation_2/off\"><button class=\"button button2\">OFF</button></a></p>"); // Display current state, of the button 3 client.println("<p>Animation 3 - State " + Animation_3 + "</p>"); if (Animation_3 == "off") client.println("<p><a href=\"/Animation_3/on\"><button class=\"button\">ON</button></a></p>"); else client.println("<p><a href=\"/Animation_3/off\"><button class=\"button button2\">OFF</button></a></p>"); client.println("</body></html>"); // The HTTP response ends with another blank line client.println();
Next, we have some else and else if statements that handle any exceptions that occur during the server-client communication process. Now if everything is correct and executed successfully, we clear our main header variable and we terminate the connection.
else // if you got a newline, then clear incomingData incomingData = ""; } else if (read_byte != '\r') // if you got anything else but a carriage return character, incomingData += read_byte; // add it to the end of the currentLine } } // Clear the header variable header = ""; // Close the connection client.stop(); Serial.println("Client disconnected"); Serial.println("\n"); }
If the above process finishes successfully, we are back within our main loop function and in the next, in the loop function, we check for any updates in our string type variables Animation_1, Animation_2, and Animation_3. If any changes have occurred, we call the corresponding function and light up the display. If every button is on, we turn off the POV display.
if (Animation_1 == "on") { color_palette(); } else if (Animation_2 == "on") { animation(); //cyclone_colour(); } else if (Animation_3 == "on") { //first_light(); demo_reel100(); } else if (Animation_1 == "off" && Animation_2 == "off" && Animation_3 == "off" { for (int i = 0; i < 8; i++) { leds[i] = CRGB::Black; FastLED.show(); delay(2); } }
This marks the end of the code portion, but as you can see in the code, there is a huge portion of the code that is not explained because this entire portion of the code is taken from the fast LED library and you can check out the documentation for fastled library for more information. As we have displayed only three buttons, you can only choose between three patterns but there are other patterns available in the code as a function that you can call and display.
void color_palette() { Serial.println("working"); ChangePalettePeriodically(); static uint8_t startIndex = 0; startIndex = startIndex + 1; /* motion speed */ FillLEDsFromPaletteColors( startIndex); FastLED.show(); FastLED.delay(1000 / UPDATES_PER_SECOND); } void cyclone_colour() { static uint8_t hue = 0; Serial.print("x"); // First slide the led in one direction for (int i = 0; i < NUM_LEDS; i++) { // Set the i'th led to red leds[i] = CHSV(hue++, 255, 255); // Show the leds FastLED.show(); // now that we've shown the leds, reset the i'th led to black // leds[i] = CRGB::Black; fadeall(); // Wait a little bit before we loop around and do it again delay(10); } Serial.print("x"); // Now go in the other direction. for (int i = (NUM_LEDS) - 1; i >= 0; i--) { // Set the i'th led to red leds[i] = CHSV(hue++, 255, 255); // Show the leds FastLED.show(); // now that we've shown the leds, reset the i'th led to black // leds[i] = CRGB::Black; fadeall(); // Wait a little bit before we loop around and do it again delay(10); } } void animation() { EVERY_N_MILLISECONDS( 20) { pacifica_loop(); FastLED.show(); } } void fadeall() { for (int i = 0; i < NUM_LEDS; i++) { leds[i].nscale8(250); } } void first_light() { // Move a single white led for (int whiteLed = 0; whiteLed < NUM_LEDS; whiteLed = whiteLed + 1) { // Turn our current led on to white, then show the leds leds[whiteLed] = CRGB::White; // Show the leds (only one of which is set to white, from above) FastLED.show(); // Wait a little bit delay(100); // Turn our current led back to black for the next loop around leds[whiteLed] = CRGB::Black; } } void demo_reel100() { gPatterns[gCurrentPatternNumber](); // send the 'leds' array out to the actual LED strip FastLED.show(); // insert a delay to keep the framerate modest FastLED.delay(1000 / UPDATES_PER_SECOND); // do some periodic updates EVERY_N_MILLISECONDS( 20 ) { gHue++; // slowly cycle the "base color" through the rainbow } EVERY_N_SECONDS( 10 ) { nextPattern(); // change patterns periodically } } void FillLEDsFromPaletteColors( uint8_t colorIndex) { uint8_t brightness = 255; for ( int i = 0; i < NUM_LEDS; ++i) { leds[i] = ColorFromPalette( currentPalette, colorIndex, brightness, currentBlending); colorIndex += 3; } } void rgb_wheel() { for (int i = 0; i < 8; i++) leds[i] = CRGB::Red; FastLED.show(); delay(5); for (int i = 0; i < 8; i++) leds[i] = CRGB::Blue; FastLED.show(); delay(5); for (int i = 0; i < 8; i++) leds[i] = CRGB::Green; FastLED.show(); delay(5); for (int i = 0; i < 8; i++) leds[i] = CRGB::Black; FastLED.show(); } void ChangePalettePeriodically() { uint8_t secondHand = (millis() / 1000) % 60; static uint8_t lastSecond = 99; if ( lastSecond != secondHand) { lastSecond = secondHand; if ( secondHand == 0) { currentPalette = RainbowColors_p; currentBlending = LINEARBLEND; } if ( secondHand == 10) { currentPalette = RainbowStripeColors_p; currentBlending = NOBLEND; } if ( secondHand == 15) { currentPalette = RainbowStripeColors_p; currentBlending = LINEARBLEND; } if ( secondHand == 20) { SetupPurpleAndGreenPalette(); currentBlending = LINEARBLEND; } if ( secondHand == 25) { SetupTotallyRandomPalette(); currentBlending = LINEARBLEND; } if ( secondHand == 30) { SetupBlackAndWhiteStripedPalette(); currentBlending = NOBLEND; } if ( secondHand == 35) { SetupBlackAndWhiteStripedPalette(); currentBlending = LINEARBLEND; } if ( secondHand == 40) { currentPalette = CloudColors_p; currentBlending = LINEARBLEND; } if ( secondHand == 45) { currentPalette = PartyColors_p; currentBlending = LINEARBLEND; } if ( secondHand == 50) { currentPalette = myRedWhiteBluePalette_p; currentBlending = NOBLEND; } if ( secondHand == 55) { currentPalette = myRedWhiteBluePalette_p; currentBlending = LINEARBLEND; } } } // This function fills the palette with totally random colors. void SetupTotallyRandomPalette() { for ( int i = 0; i < 16; ++i) { currentPalette[i] = CHSV( random8(), 255, random8()); } } // This function sets up a palette of black and white stripes, // using code. Since the palette is effectively an array of // sixteen CRGB colors, the various fill_* functions can be used // to set them up. void SetupBlackAndWhiteStripedPalette() { // 'black out' all 16 palette entries... fill_solid( currentPalette, 16, CRGB::Black); // and set every fourth one to white. currentPalette[0] = CRGB::White; currentPalette[4] = CRGB::White; currentPalette[8] = CRGB::White; currentPalette[12] = CRGB::White; } // This function sets up a palette of purple and green stripes. void SetupPurpleAndGreenPalette() { CRGB purple = CHSV( HUE_PURPLE, 255, 255); CRGB green = CHSV( HUE_GREEN, 255, 255); CRGB black = CRGB::Black; currentPalette = CRGBPalette16( green, green, black, black, purple, purple, black, black, green, green, black, black, purple, purple, black, black ); } // This example shows how to set up a static color palette // which is stored in PROGMEM (flash), which is almost always more // plentiful than RAM. A static PROGMEM palette like this // takes up 64 bytes of flash. const TProgmemPalette16 myRedWhiteBluePalette_p PROGMEM = { CRGB::Red, CRGB::Gray, // 'white' is too bright compared to red and blue CRGB::Blue, CRGB::Black, CRGB::Red, CRGB::Gray, CRGB::Blue, CRGB::Black, CRGB::Red CRGB::Red, CRGB::Gray, CRGB::Gray, CRGB::Blue, CRGB::Blue, CRGB::Black, CRGB::Black }; #define ARRAY_SIZE(A) (sizeof(A) / sizeof((A)[0])) void nextPattern() { // add one to the current pattern number, and wrap around at the end gCurrentPatternNumber = (gCurrentPatternNumber + 1) % ARRAY_SIZE( gPatterns); } void rainbow() { // FastLED's built-in rainbow generator fill_rainbow( leds, NUM_LEDS, gHue, 7); } void rainbowWithGlitter() { // built-in FastLED rainbow, plus some random sparkly glitter rainbow(); addGlitter(80); } void addGlitter( fract8 chanceOfGlitter) { if ( random8() < chanceOfGlitter) { leds[ random16(NUM_LEDS) ] += CRGB::White; } } void confetti() { // random colored speckles that blink in and fade smoothly fadeToBlackBy( leds, NUM_LEDS, 10); int pos = random16(NUM_LEDS); leds[pos] += CHSV( gHue + random8(64), 200, 255); } void sinelon() { // a colored dot sweeping back and forth, with fading trails fadeToBlackBy( leds, NUM_LEDS, 20); int pos = beatsin16( 13, 0, NUM_LEDS - 1 ); leds[pos] += CHSV( gHue, 255, 192); } void bpm() { // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint8_t BeatsPerMinute = 62; CRGBPalette16 palette = PartyColors_p; uint8_t beat = beatsin8( BeatsPerMinute, 64, 255); for ( int i = 0; i < NUM_LEDS; i++) { //9948 leds[i] = ColorFromPalette(palette, gHue + (i * 2), beat - gHue + (i * 10)); } } void juggle() { // eight colored dots, weaving in and out of sync with each other fadeToBlackBy( leds, NUM_LEDS, 20); uint8_t dothue = 0; for ( int i = 0; i < 8; i++) { leds[beatsin16( i + 7, 0, NUM_LEDS - 1 )] |= CHSV(dothue, 200, 255); dothue += 32; } } CRGBPalette16 pacifica_palette_1 = { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117, 0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x14554B, 0x28AA50 }; CRGBPalette16 pacifica_palette_2 = { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117, 0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x0C5F52, 0x19BE5F }; CRGBPalette16 pacifica_palette_3 = { 0x000208, 0x00030E, 0x000514, 0x00061A, 0x000820, 0x000927, 0x000B2D, 0x000C33, 0x000E39, 0x001040, 0x001450, 0x001860, 0x001C70, 0x002080, 0x1040BF, 0x2060FF }; void pacifica_loop() { // Increment the four "color index start" counters, one for each wave layer. // Each is incremented at a different speed, and the speeds vary over time. static uint16_t sCIStart1, sCIStart2, sCIStart3, sCIStart4; static uint32_t sLastms = 0; uint32_t ms = GET_MILLIS(); uint32_t deltams = ms - sLastms; sLastms = ms; uint16_t speedfactor1 = beatsin16(3, 179, 269); uint16_t speedfactor2 = beatsin16(4, 179, 269); uint32_t deltams1 = (deltams * speedfactor1) / 256; uint32_t deltams2 = (deltams * speedfactor2) / 256; uint32_t deltams21 = (deltams1 + deltams2) / 2; sCIStart1 += (deltams1 * beatsin88(1011, 10, 13)); sCIStart2 -= (deltams21 * beatsin88(777, 8, 11)); sCIStart3 -= (deltams1 * beatsin88(501, 5, 7)); sCIStart4 -= (deltams2 * beatsin88(257, 4, 6)); // Clear out the LED array to a dim background blue-green fill_solid( leds, NUM_LEDS, CRGB( 2, 6, 10)); // Render each of four layers, with different scales and speeds, that vary over time pacifica_one_layer( pacifica_palette_1, sCIStart1, beatsin16( 3, 11 * 256, 14 * 256), beatsin8( 10, 70, 130), 0 - beat16( 301) ); pacifica_one_layer( pacifica_palette_2, sCIStart2, beatsin16( 4, 6 * 256, 9 * 256), beatsin8( 17, 40, 80), beat16( 401) ); pacifica_one_layer( pacifica_palette_3, sCIStart3, 6 * 256, beatsin8( 9, 10, 38), 0 - beat16(503)); pacifica_one_layer( pacifica_palette_3, sCIStart4, 5 * 256, beatsin8( 8, 10, 28), beat16(601)); // Add brighter 'whitecaps' where the waves lines up more pacifica_add_whitecaps(); // Deepen the blues and greens a bit pacifica_deepen_colors(); } // Add one layer of waves into the led array void pacifica_one_layer( CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) { uint16_t ci = cistart; uint16_t waveangle = ioff; uint16_t wavescale_half = (wavescale / 2) + 20; for ( uint16_t i = 0; i < NUM_LEDS; i++) { waveangle += 250; uint16_t s16 = sin16( waveangle ) + 32768; uint16_t cs = scale16( s16 , wavescale_half ) + wavescale_half; ci += cs; uint16_t sindex16 = sin16( ci) + 32768; uint8_t sindex8 = scale16( sindex16, 240); CRGB c = ColorFromPalette( p, sindex8, bri, LINEARBLEND); leds[i] += c; } } // Add extra 'white' to areas where the four layers of light have lined up brightly void pacifica_add_whitecaps() { uint8_t basethreshold = beatsin8( 9, 55, 65); uint8_t wave = beat8( 7 ); for ( uint16_t i = 0; i < NUM_LEDS; i++) { uint8_t threshold = scale8( sin8( wave), 20) + basethreshold; wave += 7; uint8_t l = leds[i].getAverageLight(); if ( l > threshold) { uint8_t overage = l - threshold; uint8_t overage2 = qadd8( overage, overage); leds[i] += CRGB( overage, overage2, qadd8( overage2, overage2)); } } } // Deepen the blues and greens void pacifica_deepen_colors() { for ( uint16_t i = 0; i < NUM_LEDS; i++) { leds[i].blue = scale8( leds[i].blue, 145); leds[i].green = scale8( leds[i].green, 200); leds[i] |= CRGB( 2, 5, 7); } }
Once the code is uploaded and you get your IP in the serial monitor window, you get something that is shown above. By pressing the activate button, you can change the animation that is shown in the display.
Testing the Neopixel POV Display
To test the POV Display we used some double-sided tape to temporarily stick the aluminum bar to the table and we are powering the motor with 5V power from our regulated power supply. The image below displays our test setup.
Once the hardware setup was done, we opened our web server app that we have previously built and tested different animations and effects. A collage image of some of those is shown below-
With this, we are concluding our project on POV Display with WS2812B RGB LEDs. Hope you have enjoyed reading this and learned something useful. Now it’s time for you to build it on your own and let us know what you are doing with this project in the comment section below. If you have any questions or problems feel free to post them on our forums and we will get back to you.
// Load Wi-Fi library, Fast-LED Library and Custom OTA Library
#include <ESP8266WiFi.h>
#include <FastLED.h>
#include "OTA.h"
#define LED_PIN 0 // LED is connected to the IO-0 of the ESP8266-01
#define NUM_LEDS 8 // We have 8 LEDs in the strip
#define BRIGHTNESS 50 // set the brightness for the LEDs
#define LED_TYPE WS2812 // We are using WS2812B LEDs
#define COLOR_ORDER GRB // Colour order of the LEDs
#define UPDATES_PER_SECOND 100 // FPS
CRGB leds[NUM_LEDS]; // Create LED array for manipulation
CRGBPalette16 currentPalette; // set color palet type
TBlendType currentBlending;// set blend mode
extern CRGBPalette16 myRedWhiteBluePalette; // create object for different palet
extern const TProgmemPalette16 myRedWhiteBluePalette_p PROGMEM;
// Set web server port number to 80
WiFiServer server(80);
// variables to store the current Button state
String Animation_1 = "Deactivate";
String Animation_2 = "Deactivate";
String Animation_3 = "Deactivate";
// Variable to store the HTTP request
String header;
const char* ssid = "Semicon Media 2.4";
const char* password = "cdfiP29to665";
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0;
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;
void setup() {
Serial.begin(115200);
Serial.println("Booting");
setupOTA("POV_Display", ssid , password);
delay( 300 ); // power-up safety delay
FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip );
FastLED.setBrightness( BRIGHTNESS );
currentPalette = RainbowColors_p;
currentBlending = LINEARBLEND;
server.begin();
}
// List of patterns to cycle through. Each is defined as a separate function below.
typedef void (*SimplePatternList[])();
SimplePatternList gPatterns = { rainbow, rainbowWithGlitter, confetti, sinelon, juggle, bpm };
uint8_t gCurrentPatternNumber = 0; // Index number of which pattern is current
uint8_t gHue = 0; // rotating "base color" used by many of the patterns
void loop() {
while ((unsigned long) millis() < 10000)
{
ArduinoOTA.handle();
delay(10);
}
ArduinoOTA.handle();
WiFiClient client = server.available(); // Listen for incoming clients
if (client) { // If a new client connects,
currentTime = millis();
previousTime = currentTime;
Serial.println("New Client"); // print a statement so the user knows that a new client got connected
String incomingData = ""; // make a String to hold incoming data from client
while (client.connected() && currentTime - previousTime <= timeoutTime) { // client connects and request the page from server
currentTime = millis();
if (client.available()) { // if client sends a byte to read
char read_byte = client.read(); // read that byte from client
Serial.write(read_byte); // print it to the serial monitor for debugging
header += read_byte; // Store the bit stream for debugging
if (read_byte == '\n') { // if the byte is a newline character
// if you got two newline characters simultaneously. we can say its the end of the HTTP Request
if (incomingData.length() == 0) {
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
// and a content-type so the client knows what's coming, then a blank line:
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
//Set Different Mode For the LEDs
if (header.indexOf("GET /Animation_1/Activate") >= 0) {
// Serial.println("Animation 1 on");
Animation_1 = "Activate";
} else if (header.indexOf("GET /Animation_1/Deactivate") >= 0) {
// Serial.println("Animation 1 off");
Animation_1 = "Deactivate";
} else if (header.indexOf("GET /Animation_2/Activate") >= 0) {
// Serial.println("Animation 2 on");
Animation_2 = "Activate";
} else if (header.indexOf("GET /Animation_2/Deactivate") >= 0) {
// Serial.println("Animation 2 off");
Animation_2 = "Deactivate";
} else if (header.indexOf("GET /Animation_3/Activate") >= 0) {
// Serial.println("Animation 3 on");
Animation_3 = "Activate";
} else if (header.indexOf("GET /Animation_3/Deactivate") >= 0) {
// Serial.println("Animation 3 off");
Animation_3 = "Deactivate";
}
client.println("<!DOCTYPE html><html>");
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\">");
client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
client.println(".button { background-color: #3bc041; color: #ffffff; padding: 9px 30px ; text-decoration: none; font-size: 30px; margin: 2px ; border-radius: 5px ; border: 1px solid #ddffdf; box-shadow: 1px 2px 3px #0000001c; ");
client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
client.println(".button:hover{ background-color: #31b237; }");
client.println(".button2 { background-color: #aaaaaa; border: 1px solid #bbbbbb; }</style></head>");
// This is the heading of the webpage
client.println("<body><h1>POV Display Controller</h1>");
// Display current state, of the button 1
client.println("<p>Pattern 1 - State " + Animation_1 + "</p>");
if (Animation_1 == "Deactivate")
client.println("<p><a href=\"/Animation_1/Activate\"><button class=\"button\">Activate</button></a></p>");
else
client.println("<p><a href=\"/Animation_1/Deactivate\"><button class=\"button button2\">Deactivate</button></a></p>");
// Display current state, of the button 2
client.println("<p>Pattern 2 - State " + Animation_2 + "</p>");
if (Animation_2 == "Deactivate")
client.println("<p><a href=\"/Animation_2/Activate\"><button class=\"button\">Activate</button></a></p>");
else
client.println("<p><a href=\"/Animation_2/Deactivate\"><button class=\"button button2\">Deactivate</button></a></p>");
// Display current state, of the button 3
client.println("<p>Pattern 3 - State " + Animation_3 + "</p>");
if (Animation_3 == "Deactivate")
client.println("<p><a href=\"/Animation_3/Activate\"><button class=\"button\">Activate</button></a></p>");
else
client.println("<p><a href=\"/Animation_3/Deactivate\"><button class=\"button button2\">Deactivate</button></a></p>");
client.println("</body></html>");
// The HTTP response ends with another blank line
client.println();
break;
}
else // if you got a newline, then clear incomingData
incomingData = "";
}
else if (read_byte != '\r') // if you got anything else but a carriage return character,
incomingData += read_byte; // add it to the end of the currentLine
}
}
// Clear the header variable
header = "";
// Close the connection
client.stop();
Serial.println("Client disconnected");
Serial.println("\n");
}
if (Animation_1 == "Activate")
{
color_palette();
}
else if (Animation_2 == "Activate")
{
animation();
//cyclone_colour();
}
else if (Animation_3 == "Activate")
{
//first_light();
//demo_reel100();
for (int i = 0; i < 8; i++)
leds[i] = CRGB::Red;
FastLED.show();
delay(5);
for (int i = 0; i < 8; i++)
leds[i] = CRGB::Green;
FastLED.show();
delay(5);
for (int i = 0; i < 8; i++)
leds[i] = CRGB::White;
FastLED.show();
delay(5);
}
else if (Animation_1 == "Deactivate" && Animation_2 == "Deactivate" && Animation_3 == "Deactivate")
{
for (int i = 0; i < 8; i++)
{
leds[i] = CRGB::Black;
FastLED.show();
delay(2);
}
}
}
void color_palette()
{
Serial.println("working");
ChangePalettePeriodically();
static uint8_t startIndex = 0;
startIndex = startIndex + 1; /* motion speed */
FillLEDsFromPaletteColors( startIndex);
FastLED.show();
FastLED.delay(1000 / UPDATES_PER_SECOND);
}
void cyclone_colour()
{
static uint8_t hue = 0;
Serial.print("x");
// First slide the led in one direction
for (int i = 0; i < NUM_LEDS; i++) {
// Set the i'th led to red
leds[i] = CHSV(hue++, 255, 255);
// Show the leds
FastLED.show();
// now that we've shown the leds, reset the i'th led to black
// leds[i] = CRGB::Black;
fadeall();
// Wait a little bit before we loop around and do it again
delay(10);
}
Serial.print("x");
// Now go in the other direction.
for (int i = (NUM_LEDS) - 1; i >= 0; i--) {
// Set the i'th led to red
leds[i] = CHSV(hue++, 255, 255);
// Show the leds
FastLED.show();
// now that we've shown the leds, reset the i'th led to black
// leds[i] = CRGB::Black;
fadeall();
// Wait a little bit before we loop around and do it again
delay(10);
}
}
void animation()
{
EVERY_N_MILLISECONDS( 20) {
pacifica_loop();
FastLED.show();
}
}
void fadeall() {
for (int i = 0; i < NUM_LEDS; i++) {
leds[i].nscale8(250);
}
}
void first_light()
{
// Move a single white led
for (int whiteLed = 0; whiteLed < NUM_LEDS; whiteLed = whiteLed + 1) {
// Turn our current led on to white, then show the leds
leds[whiteLed] = CRGB::White;
// Show the leds (only one of which is set to white, from above)
FastLED.show();
// Wait a little bit
delay(100);
// Turn our current led back to black for the next loop around
leds[whiteLed] = CRGB::Black;
}
}
void demo_reel100()
{
gPatterns[gCurrentPatternNumber]();
// send the 'leds' array out to the actual LED strip
FastLED.show();
// insert a delay to keep the framerate modest
FastLED.delay(1000 / UPDATES_PER_SECOND);
// do some periodic updates
EVERY_N_MILLISECONDS( 20 ) {
gHue++; // slowly cycle the "base color" through the rainbow
}
EVERY_N_SECONDS( 10 ) {
nextPattern(); // change patterns periodically
}
}
void FillLEDsFromPaletteColors( uint8_t colorIndex)
{
uint8_t brightness = 255;
for ( int i = 0; i < NUM_LEDS; ++i) {
leds[i] = ColorFromPalette( currentPalette, colorIndex, brightness, currentBlending);
colorIndex += 3;
}
}
void rgb_wheel()
{
for (int i = 0; i < 8; i++)
leds[i] = CRGB::Red;
FastLED.show();
delay(5);
for (int i = 0; i < 8; i++)
leds[i] = CRGB::Blue;
FastLED.show();
delay(5);
for (int i = 0; i < 8; i++)
leds[i] = CRGB::Green;
FastLED.show();
delay(5);
for (int i = 0; i < 8; i++)
leds[i] = CRGB::Black;
FastLED.show();
}
void ChangePalettePeriodically()
{
uint8_t secondHand = (millis() / 1000) % 60;
static uint8_t lastSecond = 99;
if ( lastSecond != secondHand) {
lastSecond = secondHand;
if ( secondHand == 0) {
currentPalette = RainbowColors_p;
currentBlending = LINEARBLEND;
}
if ( secondHand == 10) {
currentPalette = RainbowStripeColors_p;
currentBlending = NOBLEND;
}
if ( secondHand == 15) {
currentPalette = RainbowStripeColors_p;
currentBlending = LINEARBLEND;
}
if ( secondHand == 20) {
SetupPurpleAndGreenPalette();
currentBlending = LINEARBLEND;
}
if ( secondHand == 25) {
SetupTotallyRandomPalette();
currentBlending = LINEARBLEND;
}
if ( secondHand == 30) {
SetupBlackAndWhiteStripedPalette();
currentBlending = NOBLEND;
}
if ( secondHand == 35) {
SetupBlackAndWhiteStripedPalette();
currentBlending = LINEARBLEND;
}
if ( secondHand == 40) {
currentPalette = CloudColors_p;
currentBlending = LINEARBLEND;
}
if ( secondHand == 45) {
currentPalette = PartyColors_p;
currentBlending = LINEARBLEND;
}
if ( secondHand == 50) {
currentPalette = myRedWhiteBluePalette_p;
currentBlending = NOBLEND;
}
if ( secondHand == 55) {
currentPalette = myRedWhiteBluePalette_p;
currentBlending = LINEARBLEND;
}
}
}
// This function fills the palette with totally random colors.
void SetupTotallyRandomPalette()
{
for ( int i = 0; i < 16; ++i) {
currentPalette[i] = CHSV( random8(), 255, random8());
}
}
// This function sets up a palette of black and white stripes,
// using code. Since the palette is effectively an array of
// sixteen CRGB colors, the various fill_* functions can be used
// to set them up.
void SetupBlackAndWhiteStripedPalette()
{
// 'black out' all 16 palette entries...
fill_solid( currentPalette, 16, CRGB::Black);
// and set every fourth one to white.
currentPalette[0] = CRGB::White;
currentPalette[4] = CRGB::White;
currentPalette[8] = CRGB::White;
currentPalette[12] = CRGB::White;
}
// This function sets up a palette of purple and green stripes.
void SetupPurpleAndGreenPalette()
{
CRGB purple = CHSV( HUE_PURPLE, 255, 255);
CRGB green = CHSV( HUE_GREEN, 255, 255);
CRGB black = CRGB::Black;
currentPalette = CRGBPalette16(
green, green, black, black,
purple, purple, black, black,
green, green, black, black,
purple, purple, black, black );
}
// This example shows how to set up a static color palette
// which is stored in PROGMEM (flash), which is almost always more
// plentiful than RAM. A static PROGMEM palette like this
// takes up 64 bytes of flash.
const TProgmemPalette16 myRedWhiteBluePalette_p PROGMEM =
{
CRGB::Red,
CRGB::Gray, // 'white' is too bright compared to red and blue
CRGB::Blue,
CRGB::Black,
CRGB::Red,
CRGB::Gray,
CRGB::Blue,
CRGB::Black,
CRGB::Red,
CRGB::Red,
CRGB::Gray,
CRGB::Gray,
CRGB::Blue,
CRGB::Blue,
CRGB::Black,
CRGB::Black
};
#define ARRAY_SIZE(A) (sizeof(A) / sizeof((A)[0]))
void nextPattern()
{
// add one to the current pattern number, and wrap around at the end
gCurrentPatternNumber = (gCurrentPatternNumber + 1) % ARRAY_SIZE( gPatterns);
}
void rainbow()
{
// FastLED's built-in rainbow generator
fill_rainbow( leds, NUM_LEDS, gHue, 7);
}
void rainbowWithGlitter()
{
// built-in FastLED rainbow, plus some random sparkly glitter
rainbow();
addGlitter(80);
}
void addGlitter( fract8 chanceOfGlitter)
{
if ( random8() < chanceOfGlitter) {
leds[ random16(NUM_LEDS) ] += CRGB::White;
}
}
void confetti()
{
// random colored speckles that blink in and fade smoothly
fadeToBlackBy( leds, NUM_LEDS, 10);
int pos = random16(NUM_LEDS);
leds[pos] += CHSV( gHue + random8(64), 200, 255);
}
void sinelon()
{
// a colored dot sweeping back and forth, with fading trails
fadeToBlackBy( leds, NUM_LEDS, 20);
int pos = beatsin16( 13, 0, NUM_LEDS - 1 );
leds[pos] += CHSV( gHue, 255, 192);
}
void bpm()
{
// colored stripes pulsing at a defined Beats-Per-Minute (BPM)
uint8_t BeatsPerMinute = 62;
CRGBPalette16 palette = PartyColors_p;
uint8_t beat = beatsin8( BeatsPerMinute, 64, 255);
for ( int i = 0; i < NUM_LEDS; i++) { //9948
leds[i] = ColorFromPalette(palette, gHue + (i * 2), beat - gHue + (i * 10));
}
}
void juggle() {
// eight colored dots, weaving in and out of sync with each other
fadeToBlackBy( leds, NUM_LEDS, 20);
uint8_t dothue = 0;
for ( int i = 0; i < 8; i++) {
leds[beatsin16( i + 7, 0, NUM_LEDS - 1 )] |= CHSV(dothue, 200, 255);
dothue += 32;
}
}
CRGBPalette16 pacifica_palette_1 =
{ 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117,
0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x14554B, 0x28AA50
};
CRGBPalette16 pacifica_palette_2 =
{ 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117,
0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x0C5F52, 0x19BE5F
};
CRGBPalette16 pacifica_palette_3 =
{ 0x000208, 0x00030E, 0x000514, 0x00061A, 0x000820, 0x000927, 0x000B2D, 0x000C33,
0x000E39, 0x001040, 0x001450, 0x001860, 0x001C70, 0x002080, 0x1040BF, 0x2060FF
};
void pacifica_loop()
{
// Increment the four "color index start" counters, one for each wave layer.
// Each is incremented at a different speed, and the speeds vary over time.
static uint16_t sCIStart1, sCIStart2, sCIStart3, sCIStart4;
static uint32_t sLastms = 0;
uint32_t ms = GET_MILLIS();
uint32_t deltams = ms - sLastms;
sLastms = ms;
uint16_t speedfactor1 = beatsin16(3, 179, 269);
uint16_t speedfactor2 = beatsin16(4, 179, 269);
uint32_t deltams1 = (deltams * speedfactor1) / 256;
uint32_t deltams2 = (deltams * speedfactor2) / 256;
uint32_t deltams21 = (deltams1 + deltams2) / 2;
sCIStart1 += (deltams1 * beatsin88(1011, 10, 13));
sCIStart2 -= (deltams21 * beatsin88(777, 8, 11));
sCIStart3 -= (deltams1 * beatsin88(501, 5, 7));
sCIStart4 -= (deltams2 * beatsin88(257, 4, 6));
// Clear out the LED array to a dim background blue-green
fill_solid( leds, NUM_LEDS, CRGB( 2, 6, 10));
// Render each of four layers, with different scales and speeds, that vary over time
pacifica_one_layer( pacifica_palette_1, sCIStart1, beatsin16( 3, 11 * 256, 14 * 256), beatsin8( 10, 70, 130), 0 - beat16( 301) );
pacifica_one_layer( pacifica_palette_2, sCIStart2, beatsin16( 4, 6 * 256, 9 * 256), beatsin8( 17, 40, 80), beat16( 401) );
pacifica_one_layer( pacifica_palette_3, sCIStart3, 6 * 256, beatsin8( 9, 10, 38), 0 - beat16(503));
pacifica_one_layer( pacifica_palette_3, sCIStart4, 5 * 256, beatsin8( 8, 10, 28), beat16(601));
// Add brighter 'whitecaps' where the waves lines up more
pacifica_add_whitecaps();
// Deepen the blues and greens a bit
pacifica_deepen_colors();
}
// Add one layer of waves into the led array
void pacifica_one_layer( CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff)
{
uint16_t ci = cistart;
uint16_t waveangle = ioff;
uint16_t wavescale_half = (wavescale / 2) + 20;
for ( uint16_t i = 0; i < NUM_LEDS; i++) {
waveangle += 250;
uint16_t s16 = sin16( waveangle ) + 32768;
uint16_t cs = scale16( s16 , wavescale_half ) + wavescale_half;
ci += cs;
uint16_t sindex16 = sin16( ci) + 32768;
uint8_t sindex8 = scale16( sindex16, 240);
CRGB c = ColorFromPalette( p, sindex8, bri, LINEARBLEND);
leds[i] += c;
}
}
// Add extra 'white' to areas where the four layers of light have lined up brightly
void pacifica_add_whitecaps()
{
uint8_t basethreshold = beatsin8( 9, 55, 65);
uint8_t wave = beat8( 7 );
for ( uint16_t i = 0; i < NUM_LEDS; i++) {
uint8_t threshold = scale8( sin8( wave), 20) + basethreshold;
wave += 7;
uint8_t l = leds[i].getAverageLight();
if ( l > threshold) {
uint8_t overage = l - threshold;
uint8_t overage2 = qadd8( overage, overage);
leds[i] += CRGB( overage, overage2, qadd8( overage2, overage2));
}
}
}
// Deepen the blues and greens
void pacifica_deepen_colors()
{
for ( uint16_t i = 0; i < NUM_LEDS; i++) {
leds[i].blue = scale8( leds[i].blue, 145);
leds[i].green = scale8( leds[i].green, 200);
leds[i] |= CRGB( 2, 5, 7);
}
}