Published: January 19, 2016, Edited by: Mads Hobye

Controlling Neopixels with processing via wifi to nodeMCU

This is a proof of concept on using a nodeMCU as wireless neopixel client. The pixels can be controlled via wifi from processing. The nodeMCUs 20KB memory enables it to control quite a lot of pixel (about 10k - without UPD - in theory 5k with UPD).

This is just a quick documentation of a hello world example the following still needs to be done:

  • Break data into multiple upd packages (the limit is about 150 pixels at the moment).
  • Footer error check on nodeMCU should be solved.
  • Better timing handling on server and client side.
  • Create a shared buffer strategy between the neoPixel library and the UDP library (right now the pixels are stored twice in memory). Another possible solution is to divide the pixel string data into multiple UDP packages.

Technical setup:

We are using TPM2.net: https://gist.github.com/jblang/89e24e2655be6c463c56

The basis for the code has been extracted from pixel invaders.

Processing:

Use and install UDP library: http://ubaa.net/shared/processing/udp/

Arduino:

Connect data to pin D4 on nodeMCU. Connect 5V to VIN.

Use the Uart version of Neopixelbus: https://github.com/Makuna/NeoPixelBus/tree/UartDriven

Upload via Aruino using:

https://github.com/esp8266/Arduino

Processing code:

/**
* (./) udp.pde - how to use UDP library as unicast connection
* (cc) 2006, Cousot stephane for The Atelier Hypermedia
* (->) http://hypermedia.loeil.org/processing/
*
* Create a communication between Processing<->Pure Data @ http://puredata.info/
* This program also requires to run a small program on Pd to exchange data  
* (hum!!! for a complete experimentation), you can find the related Pd patch
* at http://hypermedia.loeil.org/processing/udp.pd
* 
* -- note that all Pd input/output messages are completed with the characters 
* ";\n". Don't refer to this notation for a normal use. --
*/

// import UDP library
import hypermedia.net.*;


UDP udp;  // define the UDP object

/**
* init
*/
void setup() {

// create a new datagram connection on port 6000
// and wait for incomming message
udp = new UDP( this, TPM2_NET_PORT );
//udp.log( true );         // <-- printout the connection activity
udp.listen( true );
}

//process events
void draw() {
for(int b = 0; b < 1;b++)
{
// String message  = str( key );  // the message to send
String ip       = "192.168.1.139";  // the remote IP address
int port        = TPM2_NET_PORT;    // the destination port


int size = 150*3;
byte[] data = new byte[size];
for(int i = 0; i < size; i++)
{
data[i] = 0;
}
data[counter] = (byte)0;
counter = counter + 3;
counter = counter % size;
data[counter] = (byte)255;
packetNumber ++;
byte[] ms = createImagePayload(0, 0, data); 
// send the message
udp.send( ms, ip, port );
}
}

/** 
* on key pressed event:
* send the current key value over the network
*/

int packetNumber = 0;
int counter = 0;
void keyPressed() {



}

/**
* To perform any action on datagram reception, you need to implement this 
* handler in your code. This method will be automatically called by the UDP 
* object each time he receive a nonnull message.
* By default, this method have just one argument (the received message as 
* byte[] array), but in addition, two arguments (representing in order the 
* sender IP address and his port) can be set like below.
*/
// void receive( byte[] data ) {             // <-- default handler
void receive( byte[] data, String ip, int port ) {    // <-- extended handler


// get the "real" message =
// forget the ";\n" at the end <-- !!! only for a communication with Pd !!!
data = subset(data, 0, data.length-2);
String message = new String( data );

// print the result
println( "receive: \""+message+"\" from "+ip+" on port "+port );
}



public static final int TPM2_NET_PORT = 65506;

private static final int TPM2_NET_HEADER_SIZE = 6;    
private static final byte START_BYTE = (byte) 0x9C;
private static final byte DATA_FRAME = (byte) 0xDA;
private static final byte CMD_FRAME = (byte) 0xc0;
private static final byte BLOCK_END = (byte) 0x36;

/**
* Create a TPM2.Net payload. Hint: this is the 2nd release of the protocol, added totalPackets
* 
* @param frame
* @return
*/
public static byte[] createImagePayload(int packetNumber, int totalPackets, byte[] data) {
int frameSize = data.length;
byte[] outputBuffer = new byte[frameSize + TPM2_NET_HEADER_SIZE + 1];

outputBuffer[0] = ((byte)(START_BYTE&0xff));
outputBuffer[1] = ((byte)(DATA_FRAME&0xff));
outputBuffer[2] = ((byte)(frameSize >> 8 & 0xFF));
outputBuffer[3] = ((byte)(frameSize & 0xFF));
outputBuffer[4] = ((byte)packetNumber);
outputBuffer[5] = ((byte)totalPackets);

//write footer
outputBuffer[TPM2_NET_HEADER_SIZE + frameSize] = BLOCK_END;    
println(TPM2_NET_HEADER_SIZE + frameSize);
//copy payload
System.arraycopy(data, 0, outputBuffer, TPM2_NET_HEADER_SIZE, frameSize);    
return outputBuffer;
}

/**
* send a cmd data packet, used as PING command
* 
* @param data
* @return
*/
public static byte[] createCmdPayload(byte[] data) {
int frameSize = data.length;
byte[] outputBuffer = new byte[frameSize + TPM2_NET_HEADER_SIZE + 1];

outputBuffer[0] = ((byte)(START_BYTE&0xff));
outputBuffer[1] = ((byte)(CMD_FRAME&0xff));
outputBuffer[2] = ((byte)(frameSize >> 8 & 0xFF));
outputBuffer[3] = ((byte)(frameSize & 0xFF));
outputBuffer[4] = ((byte)0);
outputBuffer[5] = ((byte)0);

//write footer
outputBuffer[TPM2_NET_HEADER_SIZE + frameSize] = BLOCK_END;    

//copy payload
System.arraycopy(data, 0, outputBuffer, TPM2_NET_HEADER_SIZE, frameSize);
return outputBuffer;
}

Arduino code

/*
 * PixelInvaders tpm2.net implementation, Copyright (C) 2013 michael vogt <michu@neophob.com>
 *
 * If you like this, make sure you check out http://www.pixelinvaders.ch
 *
 * This file is part of PixelController.
 *
 * PixelController is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * PixelController is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *
 *
 */

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NeoPixelBus.h>




//define some tpm constants
#define TPM2NET_LISTENING_PORT 65506
#define TPM2NET_HEADER_SIZE 5
#define TPM2NET_HEADER_IDENT 0x9c
#define TPM2NET_CMD_DATAFRAME 0xda
#define TPM2NET_CMD_COMMAND 0xc0
#define TPM2NET_CMD_ANSWER 0xaa
#define TPM2NET_FOOTER_IDENT 0x36


#define NR_OF_PANELS 1
#define PIXELS_PER_PANEL 150

#define pixelPin D4  // make sure to set this to the correct pin, ignored for UartDriven branch
NeoPixelBus strip = NeoPixelBus(PIXELS_PER_PANEL, pixelPin);


//3 byte per pixel or 24bit (RGB)
#define BPP 3


//as the arduino ethernet has only 2kb ram
//we must limit the maximal udp packet size
//a 64 pixel matrix needs 192 bytes data
#define UDP_PACKET_SIZE 500


//package size we expect. the footer byte is not included here!
#define EXPECTED_PACKED_SIZE (PIXELS_PER_PANEL*BPP+TPM2NET_HEADER_SIZE)

//some santiy checks here
#if EXPECTED_PACKED_SIZE > UDP_PACKET_SIZE
#error EXPECTED PACKED SIZE is bigger than UDP BUFFER! increase the buffer
#endif

char ssid[] = "EmbedNet";  //  your network SSID (name)
char pass[] = "orangero8ot";       // your network password


byte packetBuffer[ UDP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets

// A UDP instance to let us send and receive packets over UDP
WiFiUDP udp;



void setup() {

  Serial.begin(115200);
  Serial.println();
  Serial.println();

  // We start by connecting to a WiFi network
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, pass);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");

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

  Serial.println("Starting UDP");
  udp.begin(TPM2NET_LISTENING_PORT);
  Serial.print("Local port: ");
  Serial.println(udp.localPort());
  Serial.print("Expected packagesize:");
  Serial.println(EXPECTED_PACKED_SIZE);

  strip.Begin();
  strip.Show();

  Serial.println("Setup done");
}


void loop() {
  // if there's data available, read a packet
  int packetSize = udp.parsePacket();
  if (packetSize > 0)
  {
    Serial.print("Received packet of size ");
    Serial.println(packetSize);

    //tpm2 header size is 5 bytes
       if (packetSize > EXPECTED_PACKED_SIZE) {

    // read the packet into packetBufffer
    udp.read(packetBuffer, UDP_PACKET_SIZE);

    // -- Header check

    //check header byte
    if (packetBuffer[0] != TPM2NET_HEADER_IDENT) {
      Serial.print("Invalid header ident ");
      Serial.println(packetBuffer[0], HEX);
      return;
    }

    //check command
    if (packetBuffer[1] != TPM2NET_CMD_DATAFRAME) {
      Serial.print("Invalid block type ");
      Serial.println(packetBuffer[1], HEX);
      return;
    }

    uint16_t frameSize = packetBuffer[2];
    frameSize = (frameSize << 8) + packetBuffer[3];
    Serial.print("Framesize ");
    Serial.println(frameSize, HEX);

    //use packetNumber to calculate offset
    uint8_t packetNumber = packetBuffer[4];
    Serial.print("packetNumber ");
    Serial.println(packetNumber, HEX);

    //check footer
    if (packetBuffer[frameSize + TPM2NET_HEADER_SIZE] != TPM2NET_FOOTER_IDENT) {
      Serial.print("Invalid footer ident ");
      Serial.println(packetBuffer[frameSize + TPM2NET_HEADER_SIZE], HEX);
     // return;
    }

    //calculate offset
    uint16_t currentLed = packetNumber * PIXELS_PER_PANEL;
    int x = TPM2NET_HEADER_SIZE;
    for (byte i = 0; i < frameSize; i++) {
       strip.SetPixelColor(currentLed++, packetBuffer[x], packetBuffer[x + 1], packetBuffer[x + 2]);
      x += 3;
    }

    //TODO maybe update leds only if we got all pixeldata?
    strip.Show();   // write all the pixels out
       }
  }
}