Neopixels are absolutely fun to work with and adding a wifi control to them makes neopixels more fun.
However, most of the ESP8266 + NeoPixel projects I have referred on internet are programmed using Arduino, Micropython or WLED (these three are great BTW) and there are only few examples with NodeMCU Lua.
The NodeMCU Lua is excellent firmware for implementing Asynchronous Event-Driven solution in Nodejs style, Hence, I created this project to demonstrate the capabilities and ease of using NodeMCU Lua Firmware. Also, NodeMCU Lua provides ws2812b and pixbuf modules to make interactions with neopixels easier.
The Front-EndThe Front-end is a very simple static web page. This page contains an HTML form to send following 3 values using GET method :
- Pattern (or say effect or animation) : like Fill, Rainbow, etc.
- Color : Depending on Pattern, use this color (some effects don't use color value like rainbow)
- Frequency (in milliseconds) : Execute effect at what interval of time. Lesser the value, higher the speed of animation.
This webpage is attached as neopixel_ui.html. Note that you can not directly host this HTML page in nodemcu lua firmware. But instead you will need to convert this html page to a lua string and use that string to host the page.
For example, what looks like this in HTML page :
<option value="p3">Color Wheel</option>
it looks like this in lua string (note the backslashes used as escape characters before double quotes ) :
"<option value=\"p3\">Color Wheel</option>\r\n"
Now it would be tiresome to do it manually for each HTML statement.
Hence, I have included a python script read_html_to_write_lua.py in attachments, which simply reads an HTML file and generates a txt file with HTML statements converted to lua string statements, you can simple copy-paste this HTML string to your lua code.
Once you are done running the python script, update the state variables in Lua String for HTML code, like this :
-- Change this :
htmlstring = htmlstring.."<td style=\"width: 200px\"> <input type=\"color\" id=\"colour\" name=\"colour\" value=\"#ff0000\"> </td>\r\n"
-- To this :
htmlstring = htmlstring.."<td style=\"width: 200px\"> <input type=\"color\" id=\"colour\" name=\"colour\" value=\"#" .. h_color .. "\"> </td>\r\n"
The Back-EndBefore we move onto understanding the logic of this project, note that I have created a simple library (or say Lua module) called neopixel.lua to assist with various effects like rainbow, monochromatic scroll, etc. Feel free to modify it as per your need.
So, NodeMCU allows us to program esp8266 in event-driven approach, similar to node.js. What it means that we do not need to write our code in infinite super-loop. Everything happens based on events and callback functions registered to those events.
neopixel_control_STA_GET.lua is the main file for this project. First few lines connect to wifi, initialize frame buffer and state variables.
Then, there is a declaration of a timer object called animator, which executes once and turns of all the neopixels. We will use this animator to switch between different effects and animations. Note that reason why we are using timer object instead of delays is that because timer is non-blocking and delay is blocking execution.
animator = tmr.create()
animator:register(100, tmr.ALARM_SINGLE, np.all_off)
After that there are multiple declaration of functions for various effects like fill_all, color_scroll, color_wheel, cylon_eye, etc. Any one of this functions will be registered to animator timer based on effect chosen by User.
function color_scroll()
np.rotate(frame, 1, np.backward_shift)
ws2812.write(frame)
end
The further explanation is how a single event (GET request) from user controls the neopixels.
Now, we will move to line 310 of the code, where a TCP server object is created
server = net.createServer(net.TCP)-- create TCP server
and at the very end, there is code for server object to listens on port 80. Whenever a user opens web page connecting to port 80 of NodeMCU, the client_connected function will be called.
if server then
server:listen(80, client_connected) -- listen to the port 80
end
client_connected will check what kind of connection event it is, if it is a "receive" event then client_connected will call receiver function.
function client_connected (conn)
conn:on("receive", receiver)
print(conn:getpeer())
end
receiver function parses the get response for the values of Pattern, Color and Frequency (as per the webpage form). For example a sample GET request would look like this :
GET /?pattern=p0&colour=%2300ff00&frequency=22 HTTP/1.1
After parsing the values of Pattern, Color and Frequency; the receiver function initializes state variables with these values and calls Apply_Effect function and after that it will call SendHTML function to send updated webpage (with current settings chosen by user) to user.
Apply_Effect is where all the magic happens for neopixel effects. Whenever this function is called, it will first stop the animator timer object and also unregister the current callback function registered to animator. Then it will turn off all LEDs.
animator:stop()
animator:unregister()
all_leds_off()
After that, based on whatever Pattern, Color and Frequency is chosen by user, Apply_effect will register corresponding effects function to the animator object and start the animator.
if str_mode == "p0" then
animator:register(100, tmr.ALARM_SINGLE, all_leds_off)
animator:start()
elseif str_mode == "p1" then
animator:register(100, tmr.ALARM_SINGLE, fill_all)
animator:start()
elseif str_mode == "p2" then
np.monochromatic_gradient(frame, g1, r1, b1, 5)
ws2812.write(frame)
animator:register(freq_ms, tmr.ALARM_AUTO, color_scroll)
animator:start()
elseif str_mode == "p3" then
pos = 1
frame:fill(rainbow:get(1))
ws2812.write(frame)
animator:register(freq_ms, tmr.ALARM_AUTO, color_wheel)
animator:start()
...
A simple init.lua along with node_config.lua !If you have noticed, the main application file (neopixel_control_STA_GET.lua) doesn't have declaration of number of LEDs. this settings are stored in node_config.lua
-- Environment
-- can be "dev", "test" or "prod"
operating_env = "prod"
-- Application specific configuration,
-- which may require frequent changes,
-- hence not convenient to use in one of the LFS compiled files.
-- Number of LEDs (aka pixels) and number of channels.
leds = 5
channels = 3 -- RGB only, no W
The reason for storing this variables in node_config.lua is because the main file will get compiled to LFS image, which can't be edited. However, you might want to frequently change number of LEDs used depending on your setup. So, you can easily do that by changing node_config.lua which will not be part of the LFS image and you can easily edit it.
Now, how we can conveniently configure NodeMCU to autorun our main file on reboot, the simple answer is init.lua file. If nodemcu finds init.lua on boot, then it will automatically execute this code.
So, a question arises that why we shouldn't we rename the main file to init.lua. The catch is that if there is some error in init.lua, then your nodemcu will keep rebooting in an endless loop, and you might require to re-flash the nodemcu firmware erasing all flash memory to overcome this.
Hence, we can write an init.lua with a short delay (not delay actually, but a timer, so that lua interpreter isn't blocked.) of 30 seconds before any statements are executed. If we do encounter any errors after 30 seconds and nodemcu reboots, then we can simple run a rename statement to rename init.lua to something like init_old.lua. This will help us to prevent endless reboot sequence.
boot_timer = tmr.create()
boot_timer:alarm(30 * 1000, tmr.ALARM_SINGLE, on_boot)
After that, the init.lua initializes LFS and executes node_config.lua. If environment is "prod", then execute application files otherwise do not execute any statements. Also, this makes declarations of number of leds visible to next code being executed (main file).
if (operating_env == "prod") then
print("production node !")
print("executing application file(s)")
dofile(app_file_1)
--dofile(app_file_2)
--dofile(app_file_3)
elseif (operating_env == "test") then
print("testing node !")
print("executing testing file(s)")
dofile(test_file_1)
--dofile(test_file_2)
else
print("development node !")
print("no further commands to execute automatically")
end
print("done !")
Why and What is LFS ?The code is approx 500+ lines, if you run the code interactively on NodeMCU Lua interpreter, then you will quickly face "not enough memory" error. This is because all of your code is executed in RAM and you will run out of 44 KB available RAM.
To overcome this limitation, LFS allows code to be executed from Flash and hence RAM is available for variables. At the end of this article, there are instruction about how to compile code to an LFS image.
Did you Notice ??!!The beauty of NodeMCU is that if you don't use any blocking operations like delays or loops (which I haven't used) and control all of the code using events callbacks and timers (which I have done), then Lua interpreter is still available to execute commands while your application is still running.
Tryit yourself :)
ConnectionsRefer Adafruit guide here for detailed explanation of power supply and current draw per pixel, but basically :
- Add a resistor (200 - 500 Ohms) between D4 of wemos d1 mini and Data IN pin of neopixel strip.
- Add a capacitor (500-1000uF @ 6.3V) across power supply of 5V. (5V 2A = 10W power supply is enough to drive 60 neopixels)
I am using a wemos d1 mini with 4 MB flash, but below method should work for other esp8266 modules directly, some may require a little modification.
All of the files are available in attached zip file, download the zip file and follow the process mentioned in process.txt file.
Comments