The sketch that currently runs the Roundhouse lighting (it will become part of a larger Yard & Turntable sketch that will be developed down the road) includes the ALED control methods discussed in An Introduction to Arduino & Addressable RGB LEDs, so I won’t discuss that part at length here. What’s new is the use of a shift register to control multiple outputs and the introduction of Ethernet networking to receive commands from a separate control device.
First, the included libraries:
#include <Adafruit_NeoPixel.h> #include <SPI.h> #include <Ethernet.h> #include <EthernetUdp.h> #include <EEPROM.h> #include <SystemCommon.h>
SPI.h is required for all spi bus functionality, such as accessing the Ethernet shield and EEPROM memory. Ethernet.h supports the basic functionality of an Ethernet shield. In addition, since I have elected to use UDP (a simple, short datagram protocol) for messaging, I have to include the EthernetUDP.h library as well. For more about network addresses and using EEPROM, see Ethernet Shields, Addresses & EEPROM. SystemCommon.h is my personal library containing (among other things) a function to retrieve address information from EEPROM (see Ethernet Shields, Addresses & EEPROM).
Next. the predefined values used by the sketch, including pin assignments.
#define NUMSTALLS 5 #define NUMLEDS 10 // Stall Settings #define STALL_HIGH 1 #define STALL_LOW -1 #define STALL_OFF 0 // pin assignments const int stripPIN = 2; const int clockPIN = 3; const int latchPIN = 4; const int dataPIN = 5;
A few global variables are needed. This sketch uses preset RGB values for the LEDs; these are set in three global arrays:
// RGB color arrays byte base_high[] = {128, 76, 25}; byte base_low[] = {32, 19 , 6}; byte blue_low[] = {0, 0, 8};
We need a global object to manage the LED strip, and a global object for handling UDP communication.
// An EthernetUDP instance to let us send and receive packets over UDP EthernetUDP Udp; // Global LED strip object Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUMLEDS, stripPIN, NEO_GRB + NEO_KHZ800);
Using Shift Registers
Before looking at setup() and loop(), we will need a method for handling shift register(s). For an excellent explanation and tutorial on shift registers, see the Arduino – ShiftOut Tutorial.
The short explanation of how they work is that each register has 8 outputs that can be ON or OFF. A pattern of bits is sent to each register via a single data connection, determining which outputs go on (binary 1) or off (binary 0). For example, sending the byte (8 bits) value of 6 to a register — 0110000 binary — turns the 2nd the 3rd outputs on and all the rest off. When there are multiple registers in series, a byte has to be sent out for each,
An important point about using shift registers: you must keep track of their state independently. This typically requires a global or static array representing all the shift register outputs and their current state.
This function maintains a static array of bytes representing the intended output state of all shift register outputs being tracked, and generates a corresponding bit mask to send to the registers. It is called every time you want to change the state of a register output.
void registerWrite(int whichOut, int whichState) { static byte outputStates[NUMSTALLS]; outputStates[whichOut] = whichState; // the bits to send byte bitsToSend = 0; // turn off the output while shifting bits digitalWrite(latchPIN, LOW); // Set bits according to the outputStates Array for(int i = 0; i < NUMSTALLS; i++){ bitWrite(bitsToSend, i, outputStates[i]); } // shift the bits out: shiftOut(dataPIN, clockPIN, MSBFIRST, bitsToSend); // turn on the output digitalWrite(latchPIN, HIGH); }
Setting Lighting
All lighting is set by stall number using another function, which has to manage both the shift register and the LED strip to set the light in a given stall.
void stall_set(int stall, int state){ byte *led_rgb; // pointer to a color array int lamp_state = HIGH; // set the led pointer to the correct color array for the requested state // set lamp off if new state is STALL_OFF switch(state){ case STALL_HIGH: led_rgb = base_high; break; case STALL_OFF: led_rgb = blue_low; lamp_state = LOW; break; case STALL_LOW: led_rgb = base_low; break; } registerWrite(stall, lamp_state); strip.setPixelColor(4 - stall, strip.Color(led_rgb[0], led_rgb[1], led_rgb[2])); //rear LED strip.setPixelColor(stall + 5, strip.Color(led_rgb[0], led_rgb[1], led_rgb[2])); //front LED strip.show(); }
Setup()
Those preliminaries out of the way, here is the setup function:
void setup(){ // start Ethernet and UDP: IPMAC ipm = readIPMAC(); Ethernet.begin(ipm.mac, ipm.ip); Udp.begin(UDP_PORT); // set pins used by the shift register pinMode(clockPIN, OUTPUT); pinMode(latchPIN, OUTPUT); pinMode(dataPIN, OUTPUT); // initialize the lighting system strip.begin(); strip.show(); // Initialize all pixels to 'off' delay(500); for(int i = 0; i < NUMSTALLS; i++){ // set stalls to off / night mode stall_set(i, STALL_OFF); } }
Loop()
The main loop is conceptually simple. On each repeat of the loop, the sketch checks to see if it has received a UDP packet. If it has a packet, the sketch executes the requested command.
void loop(){ // static array to hold stall states static int stalls[5] = {0, 0, 0, 0, 0}; int i, stall, req; // poll the network // if no actual packet, pkt.function == "0" PKT_DEF pkt = pollNet(); // process the packet // case pkt.function == 0 will fall through switch(pkt.function.toInt()){ case 1: //night/off [low blue ambient light] for(i = 0; i < NUMSTALLS; i++){ stall_set(i, STALL_OFF); stalls[i] = STALL_OFF; } break; case 2: //all lights low for(i = 0; i < NUMSTALLS; i++){ stall_set(i, STALL_LOW); stalls[i] = STALL_LOW; } break; case 3: //all lights high for(i = 0; i < NUMSTALLS; i++){ stall_set(i, STALL_HIGH); stalls[i] = STALL_HIGH; } break; case 4: //Stall Control // stall id is in pkt.option // pkt.data is used to convey a specific state request stall = pkt.option.toInt(); // stall id should be 1 - 5 // convert to internal numbering, 0 - 4 if(stall > 0) stall--; if(stall >= 0 && stall < NUMSTALLS){ if(pkt.data.length() > 0){ // specific state request was received req = pkt.data.toInt(); if(req >= -1 && req <= 1){ stall_set(stall, req); stalls[stall] = req; } } else { // general toggle request switch(stalls[stall]){ // act based on current state case STALL_LOW: stall_set(stall, STALL_HIGH); stalls[stall] = STALL_HIGH; break; case STALL_OFF: case STALL_HIGH: stall_set(stall, STALL_LOW); stalls[stall] = STALL_LOW; break; } } } break; } }
Simple Network Command System
UDP is a simple protocol. You can send a small (up to 24 bytes) packet containing anything you want. Its up to the receiving sketch to interpret the packet.
I’ve decided on a numerical command system. For now, commands are structured as a function, an option for executing the function if needed, plus a third optional data segment. The function and option are numbers; the data segment could be anything, but in this sketch is treated as a number.
The simplest way to build or interpret UDP packets is to use strings — a more advanced and useful form of character arrays intended to support human readable text. That is because packets are handled with character arrays and string semantics makes manipulation easier. Using strings also makes debugging easier!
For example, lets say that you want to tell the roundhouse to turn on stall 3 at full brightness. Stall control is function “4”, the stall id “3” is the option, and “1” (the value of STALL_HIGH in the sketch) requests the high brightness state. That packet would be a string of characters looking like this:
4/3/1/
The slash (“/”) is a delimiter (used to mark the end of a field) to allow interpretation of a packet where the elements are variable length. To go to low brightness state, the data field contains the value of STALL_LOW:
4/3/-1/
The call to pollNet() returns data in the form of a structure that separates out the three possible components of a packet. Here’s the type definition (I keep this in my library instead of individual sketches):
typedef struct PKT_DEF { String function; String option; String data; };
The pollNet() function checks to see if a new packet is available, and if so reads it into local memory, then passes the packet to the parsePKT() function which returns the packet data in the form of a PKT_DEF structure. PollNet() then sends a packet containing the string “ack” back to the originator of the command.
struct PKT_DEF pollNet(){ char packetBuffer[UDP_TX_PACKET_MAX_SIZE + 1]; String reply, strpkt; PKT_DEF pkt; int packetSize; // check for a new packet packetSize = Udp.parsePacket(); if(packetSize) { // read the packet into packetBuffer Udp.read(packetBuffer,UDP_TX_PACKET_MAX_SIZE); // convert packet to a string for(int i = 0; i < packetSize; i++){ strpkt += packetBuffer[i]; } // parse the packet pkt = parsePKT(strpkt); // send a reply, to the IP address and port that sent us the command reply = "ack"; reply.toCharArray(packetBuffer, UDP_TX_PACKET_MAX_SIZE); Udp.beginPacket(Udp.remoteIP(), Udp.remotePort()); Udp.write(packetBuffer); Udp.endPacket(); } else { // no new packet pkt.function = "0"; } return pkt; }
struct PKT_DEF parsePKT(String packet) { PKT_DEF pkt; int segment = 1; char delimiter = '/'; int delimIndex = packet.indexOf(delimiter); if(delimIndex == 0) { // drop leading delimiter, if any // some older experimental sketches may still be using a leading delimiter packet.remove(0,1); delimIndex = packet.indexOf(delimiter); } int lastDelim = -1; while(delimIndex >= 0 && delimIndex < packet.length()) { switch(segment){ case 1: pkt.function = packet.substring(lastDelim + 1, delimIndex); break; case 2: pkt.option = packet.substring(lastDelim + 1, delimIndex); break; case 3: pkt.data = packet.substring(lastDelim + 1, delimIndex); break; default: break; } segment++; lastDelim = delimIndex; delimIndex = packet.indexOf(delimiter, lastDelim + 1); } // if we don't already have a data field // any trailing element without a deliminter is data // makes the third delimiter optional if(pkt.data.length() == 0 && lastDelim < packet.length()){ pkt.data = packet.substring(lastDelim + 1); } return pkt; }
That is the entire sketch. Basically this is the first version of the command and control systems that will be running on the layout. As you can see, 24 bytes is more than enough to convey any command plus parameters I wish. This system, while rudimentary, is more than enough to command an entire layout full of Arduino controllers.
I’m already working on improvements.
One thought on “Roundhouse Rebuild Part 2 – The Sketch”