Today I am going to talk about a DIY LED strip controller based on an ESP-12E (ESP8266) module. All ideas are the same as they were in the Wi-Fi temperature-humidity sensor (Part 1 and Part 2). This controller is also going to have a web interface, through which you can control light intensity or power on/off all four channels. Also, this device is going to have a DIY PCB with a custom 3D printed enclosure – so, it is going to be more like an end-user’s product than a simple DIY thingy.
The Device’s Purpose
Well, 4 channel LED strip controller does what the name suggests – it controls up to 4 channels (zones) of LED strip lights. In my case, it controls up to 60 W total LED power (on all 4 channels). The transistors could drive up to 20 amperes of current each, but they are without any additional heat-sink – I would suggest connecting up to 100 W (~8A) in total.
This device has easy setup process – that is when you power on it for the first time it creates an Access Point. You connect to it with a smartphone or a laptop and go to 192.168.4.1 IP address. Then, a web interface opens for you to set up and save your router credentials. After that, ESP reboots and connects to the router. Finally, if you go to ESP IP address, you will get a web interface for controlling the lights. More about how device behaves and its operating modes you can read at the previous project’s post.
The Schematic
The schematic is straight forward: main control unit is an ESP-12e module. It connects to transistors which controls LED lights. LED dimming is implemented with PWM (Pulse Width Modulation). The PCB has 4 transistors – for one on each channel. Additionally, there is a button, accessible from outside the enclosure which resets the ESP module.
PCB has 34063 based DC/DC converting circuit, powering ESP-12e module. It converts from 12V to MCU suitable 3.3V.
Transistors IRLR2905 were chosen just because they were laying around. They are in a small SMD package, but with quite high-power driving capabilities. All resistors and capacitors (except for electrolytic) are in 0603 SMD package – they are small, but still manageable to solder with a regular soldering iron (with a small soldering tip).
If you look closely to the schematic, you will see that besides the ESP module, power circuit and transistors, there are nothing more on the PCB. It is a really simple LED strip controller’s implementation which can be even more simplified by using ESP development board with several transistors wired to it.
The PCB
Sorry, but I don’t have any photos with a finished PCB, as the controller is already finished and the enclosure is glued together.
It is a regular two sided PCB, made at home with UV lamp.
Note, that C1 capacitor (electrolytic, 100uF, 16V) is actually bent, because vertically it won’t fit in the enclosure. Also, for 12 V operation, this capasitor is rated for 16 V, but it can easily be changed to 25 or more rated voltage. Then, whole device could operate from a higher power supply voltage (if needed).
All PCB files and schematics were created with KiCad. All the links to these files you can find at the end of this post.
The Web Interface
First of all, the user interface is a bit different that the last project. As it should have some way of controlling individual channels and selected ones at the same time, a custom user interface was written.
The web interface has four buttons which turns ON/OFF each channel. If a channel was set to let’s say 50 percent intensity, when you turn off and on again with a button, the led lights on that channel are brought back again to 50%. Also, there is a circular middle button, which turns ON/OFF all four channels simultaneously. The logic behind it is that if at least one channel is turned on – the button turns everything off and if all channels are turned off – it turns them on to the same intensities as they were before turning the channels off with the middle button.
To control each channel’s intensity the web interface has four sliders. When you move the slider to the left – intensity is lowered or completely turned off (left-most position). When you move it to the right – the controller intensifies lighting on that channel. Also, there is a button next to each slider, labeled ‘M’. The letter stands for ‘Multiple’ which means that if you enable several ‘multiple’ buttons those chosen channels can be controlled together. So, if you turn on ‘M’ buttons let’s say on the first and the second channels, then by moving the first or the second slider you will change the intensity on both, the first and the second, channels. It is convenient when you want to set the same intensity on several channels – you don’t need to play around with every slider.
HTML Code
Front-end code (web interface) again consists of HTML files with some CSS and JavaScript. Most of these files are the same as in temperature-humidity project. Only index.html has some changes which I would like to talk about.
As always, the beginning of HTML file consists of CSS which defines how every single web interface’s component looks like. After that, there are several HTML lines. How HTML and CSS works you can read in my other post about basic web interface for an ESP (Part1 and Part2) or you can read about it at w3schools.com.
More interesting part might be JavaScript with custom functions. So, let’s see what does what.
Variables
In the beginning of JavaScript there are some variables:
var lamps = [0,0,0,0];
var lampsLastVal = [100,100,100,100];
var groupLastVal = [100,100,100,100];
var locks = [0,0,0,0];
var slider1 = document.getElementsByClassName("control-slider")[0];
var slider2 = document.getElementsByClassName("control-slider")[1];
var slider3 = document.getElementsByClassName("control-slider")[2];
var slider4 = document.getElementsByClassName("control-slider")[3];
var periodicCheck;
Variable lamps
keeps each channel’s intensity value and lampsLastVal
– keeps last lamp value which was set before turning it completely off. The groupLastVal
is similar to lampsLastVal
, but it is used when the middle circular web interfaces button is pressed and all channels are controlled simultaneously. The variable locks
represents which ‘M’ buttons are pressed. Also, there are four slider
variables which are object representations of the sliders in the web interface. Finally, the variable periodicCheck
is an object which is used for periodic data receptions from the web server.
Data functions
And then we have some functions:
function getDataFromServer()
{
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var myObj = JSON.parse(this.responseText);
lamps[0] = parseInt(myObj.ch1);
lamps[1] = parseInt(myObj.ch2);
lamps[2] = parseInt(myObj.ch3);
lamps[3] = parseInt(myObj.ch4);
rewriteLockDataFromString(myObj.locks);
updateView();
}
};
xmlhttp.open("GET", "/channels");
xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xmlhttp.send();
}
Function getDataFromServer
gets ESP’s response from /channels URL. The response is JSON data with each channels intensity value and lock data (which ‘M’ buttons are pressed). After receiving the data it updates the web interface by calling updateView()
function.
Function doStuff()
is somehow the reverse of the getDataFromServer()
function:
function doStuff(){
var xhr = new XMLHttpRequest();
xhr.open('post', '/channels');
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send('ch1=' + lamps[0] + '&ch2=' + lamps[1] + '&ch3=' + lamps[2] + '&ch4=' + lamps[3] + '&locks=' + generateLockData());
}
This function simply POSTs data to the server (ESP) with the lamps and lock data.
The function changeLockedSliders
looks up all sliders which are with ‘M’ button enabled and changes all those sliders simultaneously:
function changeLockedSliders(toValue){
for(var i = 0; i<locks.length; i++){
if(locks[i]==1){
lamps[i]=toValue;
}
}
}
The function generateLockData()
generates one integer from all available lock values:
function generateLockData(){
var data = 0;
for(var i=0; i<locks.length; i++){
data+= locks[i]*Math.pow(2, i);
}
return data;
}
Note that the result is a single integer and its each bit represents each lock position (either 0 or 1).
Function rewriteLockDataFromString
converts a String to an Integer and then translates each bit to lock values (reverse to the generateLockData
):
function rewriteLockDataFromString(dataString){
var dataInteger = parseInt(dataString);
for(var i = 0; i<locks.length; i++)
{
if ((dataInteger & (1<<i)) != 0)
locks[i] = 1;
else locks[i] = 0;
}
}
Slider code
There are also several sliderX.oniput
functions which look like this:
slider1.oninput = function() {
stopDataChecks()
if(locks[0]==1){
changeLockedSliders(this.value);
}
else lamps[0] = this.value;
doStuff();
updateView();
startDataChecks();
}
These functions tell what to do when you move a slider. Firstly, it stops data checks (so sliders value won’t change automatically). Then it changes lamps values, sends that data to the ESP, updates the interface and starts again regular data checks from the server.
View updates
Function updateView
is used every time when the web interface needs to be updated:
function updateView(){
for(var i = 0; i<lamps.length; i++)
{
if(lamps[i] > 0)
{
document.getElementsByClassName("button-shadow")[i].classList.add("active-shadow");
document.getElementsByClassName("button-number")[i].classList.add("active");
document.getElementsByClassName("button-number")[i].innerHTML = lamps[i] +"%";
}
else {
document.getElementsByClassName("button-shadow")[i].classList.remove("active-shadow");
document.getElementsByClassName("button-number")[i].classList.remove("active");
document.getElementsByClassName("button-number")[i].innerHTML = "OFF";
}
document.getElementsByClassName("control-slider")[i].value = lamps[i];
}
for(var i = 0; i<locks.length; i++)
{
if(locks[i] == 0) document.getElementsByClassName("lock-name")[i].classList.remove("active");
else document.getElementsByClassName("lock-name")[i].classList.add("active");
}
}
This function not only rewrites values (like intensity) in the buttons, but it also changes how some elements look like. It mainly adds or removes class ‘active’ to or from some elements. This class usually changes text color and/or shadow from grey to yellow.
Other functions
Function toggleLock
changes lock value, send the data to the web server and updates web interface:
function toggleLock(lockNo){
if(locks[lockNo] == 0) locks[lockNo] = 1;
else locks[lockNo] = 0;
doStuff();
updateView();
}
function invertLampVal(lamp){
lamps[lamp] = -1;
doStuff();
getDataFromServer();
updateView();
}
Where the function invertLampVal
is used when you press on/off button in the interface. The function changes lamp value to -1 which the server interprets as toggle and either turns off the light or dims to the last used value.
There is a similar function to the invertLampVal
, but it is used when you click on the middle circular button:
function invertGroupVal(){
for (var i=0; i<lamps.length; i++){
lamps[i] = -1;
}
doStuff();
getDataFromServer();
updateView();
}
And there are the last two functions:
function startDataChecks(){
periodicCheck = setInterval(getDataFromServer, 2500);
}
function stopDataChecks(){
clearInterval(periodicCheck);
}
The function startDataChecks()
starts periodic data checks from the server (every 2.5 seconds) and the function stopDataChecks()
stops those data checks.
Firmware’s Code
Firmware’s code is written in C++ using some Arduino libraries with Visual Studio Code and PlatformIO IDE. More about PlatformIO you can read on their official website. Again, most of the code is borrowed from beforementioned temperature-humidity project. And again, there are some changes which I would like to walk you through.
A hefty part of the code is similar to the previous project’s code, so I will mention maybe the most interesting parts of the code.
Variables
const int outputCount = 4;
const int output[4] = {4, 5, 12, 13}; //output channels 1,2,3,4
const char* ssid = "MC_S1"; //AP SSID
const char* passphrase = "12345678"; //AP Password
volatile int stepDelay[4] = {10, 10, 10, 10}; // delay used between PWM steps.
Firstly, there are some variables which can be changed. outputCout
defines how many outputs there are. The array output
holds all PIN numbers which are assigned to the outputs. The ssid
and passphrase
are ESP Access Point’s SSID name and password respectively. The variable stebDelay
sets how long it takes between the percentage steps. So, in this example, number 10 means that it takes 10ms to change the output by 1 percent. That means – it takes one second to switch the lights from 0 to 100 percent.
The function setOutputValue
sets output value in percent:
void setOutputValue(int outCh, int val)
{
oldSetValue[outCh] = setValue[outCh];
setValue[outCh] = val;
}
But it does not actually change any outputs, it just changes variables which are used setting output in the loop part. Then, function setOutputPwm
sets real PWM on the outputs:
void setOutputPwm(int chan, int val)
{
uint32_t mypwm = val * 1023 / 100;
analogWrite(output[chan], mypwm);
}
Note, that PWM is 10 bit resolution, so when changing from percents to a PWM value you need to multiply by 1023 and divide by 100.
When the server gets ‘-1’ value in the channel data, it inverts the channel:
void invertChannel(int number)
{
if (setValue[number] > 0)
{
oldValue[number] = setValue[number];
setOutputValue(number, 0);
}
else
{
setOutputValue(number, oldValue[number]);
}
}
There is also a bit different handler function:
void handleChannels()
{
String ch1(server.arg("ch1"));
String ch2(server.arg("ch2"));
String ch3(server.arg("ch3"));
String ch4(server.arg("ch4"));
String lcmem(server.arg("locks"));
bool needToResetGroup = false;
if (lcmem.length() > 0){
lockMem = lcmem;
}
if ((ch1.length() > 0) && (ch2.length() > 0) && (ch3.length() > 0) && (ch4.length() > 0))
{
if ((ch1.toInt() == -1) && (ch2.toInt() == -1) && (ch3.toInt() == -1) && (ch4.toInt() == -1))
{
needToResetGroup = true;
if ((setValue[0] > 0) || (setValue[1] > 0) || (setValue[2] > 0) || (setValue[3] > 0))
{
groupLastValue[0] = setValue[0];
groupLastValue[1] = setValue[1];
groupLastValue[2] = setValue[2];
groupLastValue[3] = setValue[3];
setOutputValue(0, 0);
setOutputValue(1, 0);
setOutputValue(2, 0);
setOutputValue(3, 0);
}
else
{
setOutputValue(0, groupLastValue[0]);
setOutputValue(1, groupLastValue[1]);
setOutputValue(2, groupLastValue[2]);
setOutputValue(3, groupLastValue[3]);
}
}
}
if (!needToResetGroup)
{
if (ch1.length() > 0)
{
if (ch1.toInt() != -1) setOutputValue(0, ch1.toInt());
else invertChannel(0);
}
if (ch2.length() > 0)
{
if (ch2.toInt() != -1) setOutputValue(1, ch2.toInt());
else invertChannel(1);
}
if (ch3.length() > 0)
{
if (ch3.toInt() != -1) setOutputValue(2, ch3.toInt());
else invertChannel(2);
}
if (ch4.length() > 0)
{
if (ch4.toInt() != -1) setOutputValue(3, ch4.toInt());
else invertChannel(3);
}
}
server.send(200, "text/json", getChannelsData());
}
This function runs when you visit /channels URL. It takes POST’ed data and translates it to channel intensity and lock data variables. Then it either sets channels to the needed output value or it changes all channels if it is needed or it does nothing if no channel data was received. Finally, it returns JSON formatted channel and lock data.
Setup
In the setup function there is only a bit of additional code:
pinMode(output[0], OUTPUT);
pinMode(output[1], OUTPUT);
pinMode(output[2], OUTPUT);
pinMode(output[3], OUTPUT);
// Set outputs to LOW
digitalWrite(output[0], LOW);
digitalWrite(output[1], LOW);
digitalWrite(output[2], LOW);
digitalWrite(output[3], LOW);
This part just sets some ESP pins as outputs and sets them LOW.
The loop
Besides OTA and Client handling functions the loop has a bit if code to set output PWM values:
void loop()
{
OtaHandleRequests();
delay(1);
for (int i = 0; i < outputCount; i++)
{
if (tempStepDelay[i] > 0)
{
tempStepDelay[i]--;
}
else if (oldSetValue[i] != setValue[i])
{
if (oldSetValue[i] < setValue[i])
{
oldSetValue[i]++;
}
else
{
oldSetValue[i]--;
}
tempStepDelay[i] = stepDelay[i];
setOutputPwm(i, oldSetValue[i]);
}
}
server.handleClient();
}
It uses tempStepDelay
value to set delay between changing the value of the output PWM. Also, the PWM signal is not changed at one increment, but it takes several iterations. This makes PWM value to change linearly in some time period so LED are not switched instantly but with a ramp.
The Enclosure
The case for this DIY led strip controller was 3D printed from black ABS plastic. It consists of two parts which are glued together during assembly process. After printing it needed a bit of sanding and cleaning of printing leftovers. If you need the STL files for printing – look at the end of this post in ‘LINKS’ section.
On top of the case I have glued peace of paper with some instructions and pin outs. In this way it is always easy to connect the wires without looking to the schematic or PCB layout. Also, you will never forget how to setup the device, because that is written on the housing.
Pins labeled ‘PWR’ are power inputs and connects to a 12 V power supply. +OUT are positive LED strip connections. -OUT are individual channel negative LED strip connection.
Summary
So, in conclusion the module is a great solution whenever you need to control several LED strips. It has easy to use interface and it even can operate without a router connection – it can be controlled through its own Access Point.
Only drawback could be that this device is intended to be controlled through local connection. It doesn’t have cloud connection, so it cannot be controlled with a remote connection (unless you open router ports – not a great solution, or use VPN connection).
Note: this project’s firmware was written PlatformIO IDE with ESP8266 platform version 1.8.0 installed. Keep in mind that other versions might or might not have some kind of problems.
LINKS
GitHub with all KiCad (Schematic, PCB) and PlatformIO (Firmware) files: HERE
Thingiverse with enclosure’s STL files for 3D printing: Here