16 X 16 Pixel RGB LED Artboard Part 4

The fourth and final post in my 16 X 16 Pixel RGB LED Artboard project.

In this article the programming behind the project will be discussed. The code is run on a NodeMCU (ESP12) 8266 dev board. These little gems are available all over Ebay from China for a couple dollars, usually with free shipping. This link should get you started: NodeMcu boards on Ebay

I like the models with CP2102 usb-to-serial IC’s built in as they play nicely with windows 10, and driver installation is automatic and trouble free… I’m still not used to saying that about windows yet lol.

But its True 😉

If this is your first time programming a NodeMcu board using Arduino checkout this tutorial, your going to have to install the boards so they are available in the Arduino GUI.

See here: How to add NodeMcu and ESP8266 Boards to Arduino GUI

Next – Grab this code from GitHub: https://github.com/lincomatic/WS2812B

Now unzip that little gem and open the .ino file using Arduino.  Now depending on the NodeMcu dev board you purchased you may need to configure Arduino differently… for the upload. Normally choosing “NodeMcu 0.9” from the list of pre-configured boards just works, so I would suggest beginning there.

Ok so the code is pretty simple, I’m using good old Arduino to upload via USB cable to my pc as mentioned above, so by now you should be ready to configure some wifi network settings and get this thing lit up right?

After unzipping the library above open up the WS2812Artnet.ino file using Arduino and you should see something like this:

Hacked by SCL from

 This example will receive multiple universes via Artnet and control a strip of ws2811 leds

//#include <eagle_soc.h>
#include <EEPROM.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include "./ArtnetWifi.h"
#include "./ArduinoOTAMgr.h"

// --- begin configuration
#define WIFI_MGR // use AP mode to configure via captive portal
#define OTA_UPDATE // OTA firmware update

#define PIXEL_CNT 255 //16 // number of LED's

#define PIN_DATA  15 
//#define PIN_LED    0
// n.b. pin 15 internal pullup doesn't seem to work so shouldn't be used for reset
#define PIN_FACTORY_RESET 12 // ground this pin to wipe out EEPROM & WiFi settings

#define AP_PREFIX "WS2812ArtNet_"

#define OTA_HOST "WS2812ArtNet"
#define OTA_PASS "ws2812artnet"

#ifndef WIFI_MGR
//Wifi settings
const char* ssid = "yourssid";
const char* password = "yourpswd";
#endif // !WIFI_MGR

// --- end configuration

#define CHANNEL_CNT (PIXEL_CNT*3) // Total number of channels you want to receive (1 led = 3 channels)
#define BYTE_CNT (PIXEL_CNT*3)

// Artnet settings
int startUniverse = 0; 
const int maxUniverses = CHANNEL_CNT / 512 + ((CHANNEL_CNT % 512) ? 1 : 0);

ArduinoOTAMgr AOTAMgr;

typedef struct pixel_grb {
  uint8_t g;
  uint8_t r;
  uint8_t b;
PIXEL_GRB g_BlkPixel = {0,0,0};

class WS2812Strand {
  PIXEL_GRB pixelBuffer[PIXEL_CNT];
  uint8_t *displayBuffer;
  uint32_t endTime;       // Latch timing reference
  uint8_t dataPin;

  inline bool canShow(void) { return (micros() - endTime) >= 50L; }
  WS2812Strand(uint8_t datapin);

  void begin();
  void fill(PIXEL_GRB *pixel);
  void show();
  uint8_t *getDisplayBuffer() { return displayBuffer; }
  void setPixel(uint16_t idx,PIXEL_GRB *p);
  PIXEL_GRB *getPixel(uint16_t idx) { return pixelBuffer + idx; }

WS2812Strand::WS2812Strand(uint8_t datapin)
  displayBuffer = (uint8_t *)pixelBuffer;
  dataPin = datapin;
  endTime = 0;

void WS2812Strand::begin()
  pinMode(dataPin, OUTPUT);
  digitalWrite(dataPin, LOW);
  endTime = micros();

void WS2812Strand::fill(PIXEL_GRB *pixel)
  for (uint8_t i=0;i < PIXEL_CNT;i++) {
    pixelBuffer[i] = *pixel;

void WS2812Strand::setPixel(uint16_t idx,PIXEL_GRB *p)
  if (idx < PIXEL_CNT) { pixelBuffer[idx] = *p; } } #ifdef ESP8266 // ESP8266 show() is external to enforce ICACHE_RAM_ATTR execution extern "C" void ICACHE_RAM_ATTR espShow( uint8_t pin, uint8_t *pixels, uint32_t numBytes, uint8_t type); #endif // ESP8266 void WS2812Strand::show() { // Data latch = 50+ microsecond pause in the output stream. Rather than // put a delay at the end of the function, the ending time is noted and // the function will simply hold off (if needed) on issuing the // subsequent round of data until the latch time has elapsed. This // allows the mainline code to start generating the next frame of data // rather than stalling for the latch. while(!canShow()); espShow(dataPin,displayBuffer,BYTE_CNT,1); endTime = micros(); // Save EOD time for latch on next call } #define BTN_PRESS_SHORT 50 // ms #define BTN_PRESS_LONG 500 // ms #define BTN_STATE_OFF 0 #define BTN_STATE_SHORT 1 // short press #define BTN_STATE_LONG 2 // long press class Btn { uint8_t btnGpio; uint8_t buttonState; unsigned long lastDebounceTime; // the last time the output pin was toggled public: Btn(uint8_t gpio); void init(); void read(); uint8_t shortPress(); uint8_t longPress(); }; Btn::Btn(uint8_t gpio) { btnGpio = gpio; buttonState = BTN_STATE_OFF; lastDebounceTime = 0; } void Btn::init() { pinMode(btnGpio,INPUT_PULLUP); } void Btn::read() { uint8_t sample; unsigned long delta; sample = digitalRead(btnGpio) ? 0 : 1; if (!sample && (buttonState == BTN_STATE_LONG) && !lastDebounceTime) { buttonState = BTN_STATE_OFF; } if ((buttonState == BTN_STATE_OFF) || ((buttonState == BTN_STATE_SHORT) && lastDebounceTime)) { if (sample) { if (!lastDebounceTime && (buttonState == BTN_STATE_OFF)) { lastDebounceTime = millis(); } delta = millis() - lastDebounceTime; if (buttonState == BTN_STATE_OFF) { if (delta >= BTN_PRESS_SHORT) {
	  buttonState = BTN_STATE_SHORT;
      else if (buttonState == BTN_STATE_SHORT) {
	if (delta >= BTN_PRESS_LONG) {
	  buttonState = BTN_STATE_LONG;
    else { //!sample
      lastDebounceTime = 0;

uint8_t Btn::shortPress()
  if ((buttonState == BTN_STATE_SHORT) && !lastDebounceTime) {
    buttonState = BTN_STATE_OFF;
    return 1;
  else {
    return 0;

uint8_t Btn::longPress()
  if ((buttonState == BTN_STATE_LONG) && lastDebounceTime) {
    lastDebounceTime = 0;
    return 1;
  else {
    return 0;

#ifdef WIFI_MGR
#include "./WiFiManager.h"          // https://github.com/tzapu/WiFiManager

typedef struct config_parms {
int startUniverse;
  char artnet_universe[3];
  char staticIP[16]; // optional static IP
  char staticGW[16]; // optional static gateway
  char staticNM[16]; // optional static netmask

//flag for saving data

class WifiConfigurator {
  CONFIG_PARMS configParms;
  // a flag for ip setup
  boolean set_static_ip;

  //network stuff
  //default custom static IP, changeable from the webinterface
  void resetConfigParms() { memset(&configParms,0,sizeof(configParms)); }
  static bool shouldSaveConfig;


  void StartManager(void);
  String getUniqueSystemName();
  void printMac();
  void readCfg();
  void resetCfg(int dowifi=0);
  uint8_t getConfigSize() { return sizeof(configParms); }

bool WifiConfigurator::shouldSaveConfig; // instantiate


  set_static_ip = false;

//creates the string that shows if the device goes into accces point mode
String WifiConfigurator::getUniqueSystemName()
  uint8_t mac[WL_MAC_ADDR_LENGTH];

  String macID = String(mac[WL_MAC_ADDR_LENGTH - 2], HEX) + String(mac[WL_MAC_ADDR_LENGTH - 1], HEX);

  String UniqueSystemName = String(AP_PREFIX) + macID;

  return UniqueSystemName;

// displays mac address on serial port
void WifiConfigurator::printMac()
  uint8_t mac[WL_MAC_ADDR_LENGTH];

  Serial.print("MAC: ");
  for (int i = 0; i < 5; i++){
    Serial.print(mac[i], HEX);

//callback notifying us of the need to save config
void saveConfigCallback () {
  Serial.println("Should save config");
  WifiConfigurator::shouldSaveConfig = true;

void WifiConfigurator::StartManager(void)
  Serial.println("StartManager() called");

  shouldSaveConfig = false;

  // add parameter for artnet setup in GUI
  WiFiManagerParameter custom_artnet_universe("artnet_universe", "Art-Net Universe (Default: 0)", configParms.artnet_universe, sizeof(configParms.artnet_universe));

  // add parameters for IP setup in GUI
  WiFiManagerParameter custom_ip("ip", "Static IP (Blank for DHCP)", configParms.staticIP, sizeof(configParms.staticIP));
  WiFiManagerParameter custom_gw("gw", "Static Gateway (Blank for DHCP)", configParms.staticGW, sizeof(configParms.staticGW));
  WiFiManagerParameter custom_nm("nm", "Static Netmask (Blank for DHCP)", configParms.staticNM, sizeof(configParms.staticNM));
  //Local intialization. Once its business is done, there is no need to keep it around
  WiFiManager wifiManager;

  // this is what is called if the webinterface want to save data, callback is right above this function and just sets a flag.

  // this actually adds the parameters defined above to the GUI

  // if the flag is set we configure a STATIC IP!
  if (set_static_ip) {
    //set static ip
    IPAddress _ip, _gw, _nm;
    // this adds 3 fields to the GUI for ip, gw and netmask, but IP needs to be defined for this fields to show up.
    wifiManager.setSTAStaticIPConfig(_ip, _gw, _nm);
    Serial.println("Setting IP to:");
    Serial.print("IP: ");
    Serial.print("GATEWAY: ");
    Serial.print("NETMASK: ");
  else {
    // i dont want to fill these fields per default so i had to implement this workaround .. its ugly.. but hey. whatever.  

  //sets timeout until configuration portal gets turned off
  //useful to make it all retry or go to sleep in seconds
  // also really annoying if you just connected and the damn thing resets in the middle of filling in the GUI..

  //fetches ssid and pass and tries to connect
  //if it does not connect it starts an access point with the specified name
  //here  "AutoConnectAP"
  //and goes into a blocking loop awaiting configuration
  if (!wifiManager.autoConnect(getUniqueSystemName().c_str())) {
    Serial.println("timed out and failed to connect");
    //reset and try again, or maybe put it to deep sleep
    //   we never get here.

  //everything below here is only executed once we are connected to a wifi.

  //if you get here you have connected to the WiFi

  digitalWrite(LED_BUILTIN,LOW); // turn on LED
  Serial.print("CONNECTED to ");
  Serial.print("IP address: ");
  // this is true if we come from the GUI and clicked "save"
  // the flag is created in the callback above..
  if (shouldSaveConfig) {
    // connection worked so lets save all those parameters to the config file
    strcpy(configParms.artnet_universe, custom_artnet_universe.getValue());
    if (*configParms.artnet_universe) {
      startUniverse = atoi(configParms.artnet_universe);
    else {
      startUniverse = 0;
    strcpy(configParms.staticIP, custom_ip.getValue());
    strcpy(configParms.staticGW, custom_gw.getValue());
    strcpy(configParms.staticNM, custom_nm.getValue());
    // if we defined something in the gui before that does not work we might to get rid of previous settings in the config file
    // so if the form is transmitted empty, delete the entries.
    //if (strlen(ip) < 8) {
    //      resetConfigParms();
    //    }
    Serial.println("saving config");
    int eepidx = 0;
    int i;
    for (i=0;i < (int)strlen(configParms.artnet_universe);i++) {

    for (i=0;i < (int)strlen(configParms.staticIP);i++) {

    for (i=0;i < (int)strlen(configParms.staticGW);i++) {

    for (i=0;i < (int)strlen(configParms.staticNM);i++) { EEPROM.write(eepidx++,configParms.staticNM[i]); } EEPROM.commit(); Serial.print("artnet_universe: "); Serial.println(*configParms.artnet_universe ? configParms.artnet_universe : "(default)"); Serial.print("IP: "); Serial.println(*configParms.staticNM ? configParms.staticNM : "(DHCP)"); Serial.print("GW: "); Serial.println(*configParms.staticGW ? configParms.staticGW : "(DHCP)"); Serial.print("NM: "); Serial.println(*configParms.staticNM ? configParms.staticNM : "(DHCP)"); //end save } } void WifiConfigurator::resetCfg(int dowifi) { Serial.print("resetCfg()"); // reset config parameters resetConfigParms(); // clear EEPROM EEPROM.write(0,0); EEPROM.commit(); if (dowifi) { // erase WiFi settings (SSID/passphrase/etc WiFiManager wifiManager; wifiManager.resetSettings(); } } void WifiConfigurator::readCfg() { int resetit = 0; int eepidx = 0; int i; resetConfigParms(); uint8_t len = EEPROM.read(eepidx++); if ((len == 0) || (len >= sizeof(configParms.artnet_universe))) {
    // assume uninitialized
    resetit = 1;
  else {
    for (i=0;i < len;i++) { configParms.artnet_universe[i] = EEPROM.read(eepidx++); } configParms.artnet_universe[i] = 0; len = EEPROM.read(eepidx++); if ((len > 0) && (len < sizeof(configParms.staticIP))) {
      for (i=0;i < len;i++) { configParms.staticIP[i] = EEPROM.read(eepidx++); } configParms.staticIP[i] = 0; } else { resetit = 1; } if (!resetit) { len = EEPROM.read(eepidx++); if ((len > 0) && (len < sizeof(configParms.staticGW))) {
	for (i=0;i < len;i++) { configParms.staticGW[i] = EEPROM.read(eepidx++); } configParms.staticGW[i] = 0; } else { resetit = 1; } } if (!resetit) { len = EEPROM.read(eepidx++); if ((len > 0) && (len < sizeof(configParms.staticNM))) {
	for (i=0;i < len;i++) { configParms.staticNM[i] = EEPROM.read(eepidx++); } configParms.staticNM[i] = 0; } else { resetit = 1; } } } if (!resetit) { // valid config startUniverse = atoi(configParms.artnet_universe); if (*configParms.staticIP) { // lets use the IP settings from the config file for network config. Serial.println("setting static ip from config"); set_static_ip = 1; } else { Serial.println("using DHCP"); } } else { resetCfg(0); } } WifiConfigurator wfCfg; #endif // WIFI_MGR WS2812Strand leds(PIN_DATA); Btn btnReset(PIN_FACTORY_RESET); ArtnetWifi artnet; // Check if we got all universes bool universesReceived[maxUniverses]; bool sendFrame = 1; int previousDataLength = 0; #ifndef WIFI_MGR // connect to wifi – returns true if successful or false if not boolean ConnectWifi(void) { boolean state = true; int i = 0; WiFi.begin(ssid, password); Serial.println(""); Serial.println("Connecting to WiFi"); // Wait for connection Serial.print("Connecting"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); if (i > 20){
      state = false;
  if (state){
    digitalWrite(0,LOW); // turn on onboard LED

    Serial.print("Connected to ");
    Serial.print("IP address: ");
  } else {
    Serial.println("Connection failed.");
  return state;

#endif // !WIFI_MGR

void reboot()

void initTest()

  p.r = 127;
  p.g = 0;
  p.b = 0;

  p.r = 0;
  p.g = 127;
  p.b = 0;

  p.r = 0;
  p.g = 0;
  p.b = 127;


void onDmxFrame(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t* data)
  digitalWrite(LED_BUILTIN,LOW); // onboard LED on

  sendFrame = 1;
  // set brightness of the whole strip 
  if (universe == 15)

  // Store which universe has got in
  if ((universe - startUniverse) < maxUniverses)
    universesReceived[universe - startUniverse] = 1;

  for (int i = 0 ; i < maxUniverses ; i++)
    if (universesReceived[i] == 0)
      sendFrame = 0;

  // read universe and put into the right part of the display buffer
  PIXEL_GRB *p = (PIXEL_GRB *)data;
  for (int i = 0; i < length / 3; i++)
    int led = i + (universe - startUniverse) * (previousDataLength / 3);
  previousDataLength = length;     
  if (sendFrame)
    // Reset universeReceived to 0
    memset(universesReceived, 0, maxUniverses);

  digitalWrite(LED_BUILTIN,HIGH); // onboard LED off

void factoryReset()
  Serial.println("Factory Reset");

void setup()
  // onboard leds also outputs
  pinMode(LED_BUILTIN, OUTPUT); // onboard LED
  digitalWrite(LED_BUILTIN,HIGH); // turn off onboard LED


#ifdef WIFI_MGR

  // display the MAC on Serial

  Serial.println("readCfg() done.");

  Serial.println("StartManager() done.");
#endif // WIFI_MGR


  Serial.print("Art-Net universe: ");
  Serial.println("Starting Art-Net");

  // this will be called for each packet received

void loop()
  if (btnReset.longPress()) {
    Serial.println("long press");

  // we call the read function inside the loop



PIN_DATA is your digital pin on the NodeMcu board, I have mine set to 15 (which is actually labelled D8 on the NodeMcu board itself…. see below, just be aware of this)

*I use a 200R 1/4 W resistor in series on the data wires coming from the NodeMcu board, this is to delay the pwm signal slightly and also current limiting, its recommended on the ws2812 datasheet*

Now you also need to set PIXEL_CNT to the number of WS2812 LED’s in your strip, array, etc…

Next change the SSID and password to match your local wifi network, you can also play around and set static IP’s. Just browse through the code, there places to change these settings as well as others.

Now just upload your code to the nodemcu and you should be able to use a program like Jinx! found HERE

Its pretty easy to use, although setting up your arrays are tricky… I will most likely do a post on that in the future, if you have any questions feel free to ask in the comment section below or shoot me an email.

Until Next Time – Happy Coding



MQTT IR Blaster for my OpenHab Setup (nodemcu+MQTT+openhab+IR)

A few months back I started to setup a dedicated home automation system to control my ever-growing number of “smart” devices.

Smart devices are great but they usually have separate interfaces and mobile programs to install and control each device. What is really needed is a single program to control all of my devices… from a single interface.

Well that in a nutshell is Openhab, a software suite developed specifically for controlling smart devices from a range of manufacturers and industries, utilizing many different communication protocols.

One of these communication protocols is MQTT. I wont go into the specifics of this protocol, only that it is widely used in industrial applications and has even been used for face-books messenger app.

Now the reason for designing an IR blaster that uses MQTT to trigger the IR LED’s is that I’m already using it to control several appliances in my apartment.

Specifically I’m using Nodemcu boards with Arduino compatible code to receive an MQTT command and do some action… i.e. turn on or off a relay

Except this time instead of turning on and off a relay I will use a separate Arduino library called IRsend.h to activate the IR leds through a digital pin on the nodemcu board.

Here is the Arduino code for the Nodemcu boards:

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <IRsend.h>

IRsend irsend(14);  // An IR LED is controlled by GPIO pin 14 (D5) on nodemcu boards

// Update these with values suitable for your network.

//openhab MQQT settings
const char* ssid = "********";
const char* password = "********";
const char* mqtt_server = "xxx.xxx.xxx.xxx";
const char* brokerusername = "********";
const char* brokerpassword = "********";

IPAddress ip(192, 168, 0, 114); // where xx is the desired IP Address
IPAddress gateway(192, 168, 0, 1); // set gateway to match your network
IPAddress subnet(255, 255, 255, 0); // set subnet mask to match your network

WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;

void setup_wifi() {

  // We start by connecting to a WiFi network
  Serial.print("Connecting to ");

  WiFi.config(ip, gateway, subnet);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {


  Serial.println("WiFi connected");
  Serial.println("IP address: ");

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print("] ");
  String p_payload;
  for (int i = 0; i < length; i++) {

    // Convert from String HEX to Hex long
    long hexCmd;
    hexCmd = strtol(&p_payload[0], NULL, 16);
    irsend.sendNEC(hexCmd, 32);

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Create a random client ID
    String clientId = "ESP8266Client-";
    clientId += String(random(0xffff), HEX);
    // Attempt to connect
    if (client.connect(clientId.c_str(), brokerusername, brokerpassword)) {
    } else {
      Serial.print("failed, rc=");
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying

void setup() {
  client.setServer(mqtt_server, 1883);
  ArduinoOTA.onStart([]() {
  ArduinoOTA.onEnd([]() {
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");

void loop() {

  if (!client.connected()) {

The code posted above is pretty simple, its using an MQTT communication library for receiving commands from Openhab. It subscribes to the “apartment/livingroom/ir_blaster/state” topic. Openhab publishes an IR code to this topic and the code here receives that command, and uses the IRsend library to pulse pin 14 (D5) on the nodemcu boards.

I decided on using 6 IR LED’s arranged in a semicircular pattern to entirely cover my living room. Each LED is 30 degrees from the next, with 6 LED’s its able to cover more than 180 degrees. I used a single PN222A transistor driven through pin 14, which switches the IR LED’s. There are three series pairs of IR leds in parallel running through the PN222A, with this configuration I do not require current limiting resistors.

Here is the circuit:

And then I designed and 3D printed this case:

Now all that’s left to do is send some IR codes to the device through the subscribed MQQT channel.

I recorded the IR codes from several of my favourite devices remotes and then set up some items and button in my Openhab UI.

UI currently looks like this:

It works perfectly!! Next I need to design some nice UI’s to allow control of my home entertainment system through a tablet.

If anyone wants a tutorial on how to record IR codes from remotes let me know.

Here is a shot of the IR codes being recorded: