Introduction: Serial Box Controller

The goal of this project is to learn about serial communication, and to have fun in the process! The simple box controller will be used to control a digital object, and in turn, the digital object will control the output of certain physical elements. For my journey into serial communication, I made a very simple controller and game, but you could take the same components of this project to make any kind of simple game that interests you, it's all about learning and having fun in the process.

Supplies

Supplies:

- Cardboard (with access to a laser cutter)

- Arduino 33 IoT (or similar microcontroller)

- Mini Breadboard

- LEDs

- 2 Potentiometers

- Neopixel Strip

- Slider

- 22 Guage Stranded Wire or Jumper Wires

- Adhesive (Electrical Tape and Hot Glue Recommended)

Step 1: Setting Up Your Circuit

More than ever with this project, having a clean circuit is absolutely crucial. Having a clean circuit not only allows us to hide away our wires far easier but also because we will be working with two programs simultaneously and trying to get them to work together, it's essential that we can set up our wiring and then trust that it will do its job.

Ultimately, what inputs and outputs you pick for your controller is up to you, but for my project, I two potentiometers and a slider as my inputs, and 6 LEDs, as well as a Neopixel, LED strip as my outputs. You may also notice that there is a fairly sizeable button on the front of the controller that isn't being used at all. I incorporated the button into my project after laser cutting my box, but before completing my code. This meant that even when I realized that I did not want to use the button in the controller, I was still committed to leaving it in. I likely will assign this button a function down the road, but for now, I didn't want to cause any confusion.

When making your box, my first recommendation would be laser cutting. I got this box template from BLANK and was able to modify it in Adobe Illustrator which allowed me to include precise cutouts for my components. The box was then cut out of simple cardboard which I then assembled using hot glue. If however you do not have easy access to a laser cutter or you do not wish to follow this fabrication path that is completely fine as well. The most important part of this project is learning about serial communication, so your controller can look like whatever your access and interests allow for.

Nothing too exciting is going on in this circuit, the most important things were simply to make sure that the potentiometers and slider were connected to analog pins so that they would be able to send a wide range of values rather than just binary. In terms of power, all of the LEDs, as well as the button, are connected to the 3.3V power sources that are able to better send the controlled amount of power that these components require, and the potentiometers and slider are connected to the 5V power supply.

Step 2: Programming Your Inputs

Here is where the fun really starts. Now that we have our circuit set up, we can start the work of making our two programs communicate with each other through Serial. In the most simple terms, Serial is the postal service of our code, and so if we want our messages to get to the right place and in the right way there are certain guidelines that we need to follow. For this project, I used p5js as the interface output. P5 does not have a built in Serial library, there is however a tool called p5.serialcontrol which not only imports a Serial library into p5, but also helps us to set up our base code in p5 to establish communication, and acts as the gateway for Serial communication between Arduino and p5. This program is essential to working with Serial in p5, and it must be open when trying to establish a connection between the two programs.

Once you have a way for the two programs two communicate, we now want to set up our program to continuously write information to Serial in a way that can be parsed by p5. You'll notice on the Arduino side, this looks like a fairly standard setup of declaring variables, opening our Serial port, and getting readings from those variables. The important part comes in the way in which we send information to Serial. Because of the way we are going to parse information from Serial in p5, it's important that we send our input readings in this exact way. By putting a comma between each of our values, we are giving p5 a way to separate out each piece of information. In a similar vein, by making our final output a Serial.println, we are giving p5 a way of knowing when one set of variables is ending and the next one is coming. You can check that all these values are being sent to Serial correctly by checking the Serial monitor built into the Arduino editor, and I recommend that you do check that these values look appropriate before worrying about p5. Once they are looking good though, it's important that you close the Arduino editor so that p5 can read the Serial port you are writing to, both programs cannot be reading the same port at the same time.

When moving onto our p5 code, if you look at the code we're starting with it looks like a lot - and it is! The nice thing about using the p5.serialcontrol app though is that once you chose the port you want to use and open it, the program provides almost all of this code for you. The only things that the app doesn't provide are the variables where we'll store our values and the way in which we are going to get those values out of the Serial. Luckily, because of the way we've sent over our code from Arduino, pulling the string apart and getting data out of it is simple. Going all the way down to the gotData() function, we need to read one line of Serial information through serial.readline(). Once we have that line we can separate each value by comma and put them into an array where we can then assign our variables to each position in that array. Use console.log() to make sure that p5 is receiving and parsing your values correctly, the setup for this part of the project is luckily very straightforward.

Now that we're receiving inputs from our components that we read in Arduino and then sent to p5, we can actually affect things on the screen based on those readings. We want to write all of these visuals in the draw() function, which is why it's important to declare the values we're receiving from Serial outside of gotData() so they can be read in other functions. For my game, I went with some very simple mechanics of moving a circle around the screen using the potentiometers and then changing the color of the ball based on the slider value. To do both of these things, I had to remap the values from the readings so that they would have a more clear effect on the screen. WIth the potentiometers, this process was simple because it simply meant remapping the values to a variable and then setting that new variable to the coordinates of the circle. With the color, it was a bit more tricky because I wanted to only use specific colors defined in the HTML color library. In order to do this, I first had to make an array of the color names I wanted to use and then map the slider values to the range of indices in that array. Once I had that value remapped, I had to actually get the color defined at that specific index and then convert it to a string that the fill() function could read. All of these steps individually are simple, but without understanding arrays and how p5 color functions work, it can be a bit tricky.

ArduinoSerialSetup

//inputs
int slidePin = A2;
int potPin1 = A1;
int potPin2 = A0;
int buttonPin = 12;
//outputs
int LEDR1 = 11;
int LEDR2 = 10;
int LEDR3 = 9;
int LEDL1 = 6;
int LEDL2 = 5;
int LEDL3 = 17;
void setup() {
Serial.begin(9600); //opening our Serial pathway
//declaring outputs
pinMode(LEDR1, OUTPUT);
pinMode(LEDR2, OUTPUT);
pinMode(LEDR3, OUTPUT);
pinMode(LEDL1, OUTPUT);
pinMode(LEDL2, OUTPUT);
pinMode(LEDL3, OUTPUT);
}
void loop() {
//readings from the input pins
int slide = analogRead(slidePin);
int pot1 = analogRead(potPin1);
int pot2 = analogRead(potPin2);
int button = digitalRead(buttonPin);
//sends sensor values to P5
Serial.print(slide);
Serial.print(",");
Serial.print(pot1);
Serial.print(",");
Serial.print(pot2);
Serial.print(",");
Serial.println(button);
delay(10); // delay in between reads for stability
}

p5SerialSetup

//input your port on line 12, OR delete this and copy the starter code from the P5.serialcontrol application
let serial;
let latestData = "waiting for data";
let potLeft = 0;
let potRight = 0;
let slider = 0;
let button = 0;
function setup() {
createCanvas(windowWidth, windowHeight);
serial = new p5.SerialPort();
serial.list();
serial.open('/dev/tty.usbmodem14601');
serial.on('connected', serverConnected);
serial.on('list', gotList);
serial.on('data', gotData);
serial.on('error', gotError);
serial.on('open', gotOpen);
serial.on('close', gotClose);
}
function serverConnected() {
print("Connected to Server");
}
function gotList(thelist) {
// print("List of Serial Ports:");
for (let i = 0; i < thelist.length; i++) {
// print(i + " " + thelist[i]);
}
}
function gotOpen() {
print("Serial Port is Open");
}
function gotClose(){
print("Serial Port is Closed");
latestData = "Serial Port is Closed";
}
function gotError(theerror) {
print(theerror);
}
//where data comes in and we can parse it
//data comes in as a string
function gotData() {
let currentString = serial.readLine();//reading the code, looking for //commas and line breaks
if(currentString.length > 0) {
let sensorArray = split(currentString, ","); //splits string at //delimiter
slider = Number(sensorArray[0]);
potLeft = Number(sensorArray[1]);
potRight = Number(sensorArray[2]);
button = Number(sensorArray[3]);
}
}
function draw() {
background(255,255,255);
}
view rawp5SerialSetup hosted with ❤ by GitHub

MakingOurReaingsDoThingsInp5

let potLeft = 0;
let potRight = 0;
let slider = 0;
let button = 0;
//where data comes in and we can fuck it up
//data comes in as a string
function gotData() {
let currentString = serial.readLine();//reading the code, looking for //commas and line breaks
if(currentString.length > 0) {
let sensorArray = split(currentString, ","); //splits string at //delimiter
slider = Number(sensorArray[0]);
potLeft = Number(sensorArray[1]);
potRight = Number(sensorArray[2]);
button = Number(sensorArray[3]);
}
}
function draw() {
background(255,255,255);
var colorLibrary = ["LavenderBlush","LemonChiffon","Lavender","LightCyan","LightPink","PaleTurquoise","Pink","Plum","PowderBlue"];
sliderColor = floor(map(slider, 30, 1023, 0, 8)); //maps the outputs of the slider to the range of inputs in our array
thisColor = colorLibrary[sliderColor]; //gets the color from the colorLibrary based on the slider value
colorString = (String(thisColor)); //converts that value into a string that fill can read
fill(colorString); //sets the fill color of the ball to the color from the colorLibrary
noStroke();
x = map(potLeft,0,1023,0,windowWidth); //takes in the readings from the right potentiometer and maps them to the width of the window
y = map(potRight,0,1023,0,windowHeight); //takes in the readings from the left potentiometer and maps them to the heigh of the window
ellipse(x,y,50,50); //makes a circle at the x and y positions set by the remapped potentiometer readings
}

Step 3: Programming Our Outputs

Now that we've gotten Serial to work one way, it's easy to assume that sending information back will be simple, but this is unfortunately not the case. This is mainly due to the fact that though both Arduino and p5 can receive Serial information, the ways they read and interpret the sent data are quite different.

To start off, we'll make our p5 send the appropriate information to Serial. For the Arduino outputs, I will be turning on certain LEDs based on the position of the ball on the screen and changing the color of a Neopixel strip based on the color of the ball. To gather and send this information back to Arduino, I will create a new function called serialSend(). Within this new function, we will need the y position of the ball, and the index position of the color in the colorLibrary, so at the top of my code, I'll define these variables as global. Within the serialSend() function I will define two variables, neoByte and LEDByte to hold the readings we'll be sending back to Arduino. neoByte we will set to the index of our current color in string format. I have added one to this index value because of the way in which I set up my function in Arduino, but this is not necessary if you set your range of values in Arduino to the correct index values. For the LEDByte, we need to take in the y position of the ball and determine using if statements if that position correlates to the top, middle, or bottom third of the screen. Once we have this determined, we will set the LEDBtye to a string of a number that corresponds to a function in our Arduino code. With neoByte and LEDByte defined, we can send our string of data back to Arduino. You'll notice that we're doing this in a similar format to the way in which we sent information to p5, only we're using angle brackets as our way of splitting lines of data, and we're using serial.write rather than serial.print.

Once we're sending p5 values to Arduino, all that's left to finish our project is to recieve this information and make it do something in Arduino. To read and parse our Serial data coming from p5 I used code from the Arduino forum , which is a helpful resource all around but is particularly useful in this instance. I used example 5 from this post and modified it to only take in integers. Within the call function for our Serial parsing functions, I also called two functions which control one of our output values--the LEDs and the Neopixels. The Neopixel function is the most complex because it involves the use of a library called FastLED which allows us to control our Neopixels more easily. FastLED has each of the HTML colors built in as constants to the library, however because of the limitations of LEDs particularly when displaying pastel colors, I had to define variables under the color names used in our p5 code which correlated to different more rich HTML colors. Within the neoPixelController() function I then assigned each color to a number corresponding to the color library in our p5 code and set the LEDs to that value based on the incoming Serial byte. Each if statement which sets the LEDs to a certain color also gamma corrects those colors to get more vivid and nuanced outputs. I originally had this gamma correction in a seperate function which was called within these if statements, but I found that this slowed down the speed at which the Neopixels were able to change color and so I opted to embed the gamma correction into the if statements. At the end of the if statements, FastLED.show() is called which sets the Neopixels to the appropriate color.

The LED control function is then the last function that needs to be written, and it turns the appropriate LEDs on or off depending on the Serial byte recieved. You'll notice that the LEDs being written to LOW and HIGH seem to contradict what is actually happening to the LEDs, and you'd be correct! I noticed early on that the LEDs seemed to be doing the opposite of what I was writing it to do, and I'll admit I do not know why. However, rather than worrying about the why of what was happening I simply took what I noticed and wrote the code accordingly.

p5SerialSend

let y;
let sliderColor;
//where data comes in and we can parse it
//data comes in as a string
function gotData() {
let currentString = serial.readLine();//reading the code, looking for //commas and line breaks
if(currentString.length > 0) {
let sensorArray = split(currentString, ","); //splits string at //delimiter
slider = Number(sensorArray[0]);
potLeft = Number(sensorArray[1]);
potRight = Number(sensorArray[2]);
button = Number(sensorArray[3]);
}
}
function draw() {
background(255,255,255);
var colorLibrary = ["LavenderBlush","LemonChiffon","Lavender","LightCyan","LightPink","PaleTurquoise","Pink","Plum","PowderBlue"];
sliderColor = floor(map(slider, 30, 1023, 0, 8));
thisColor = colorLibrary[sliderColor];
colorString = (String(thisColor));
fill(colorString);
noStroke();
x = map(potLeft,0,1023,0,windowWidth);
y = map(potRight,0,1023,0,windowHeight);
ellipse(x,y,50,50);
serialSend();
}
function serialSend(){
neoByte = String((sliderColor + 1));
if((y <= height/3) && (y >= 0)){
LEDByte = '1';
}else if((y > height/3) && (y < 2*height/3)){
LEDByte = '2';
}else if ((y > 2*height/3) && (y < height)){
LEDByte = '3';
}
serial.write("<");
serial.write(neoByte);
serial.write(",");
serial.write(LEDByte);
serial.write(">");
}
view rawp5SerialSend hosted with ❤ by GitHub

ArduinoSerialControl

#include
#define NUM_LEDS 5 //number of LEDs in the NeoPixel Strip
#define PIXEL_PIN 8 //Pin location of Neopixel input
//defining html colors used in p5 program
//because of LED limitations, different colors were used to make
//the neopixels appear similar to the screen color and were then
//renamed to match html color names used in p5
#define LAVENDER_BLUSH CRGB::MediumOrchid;
#define LEMON_CHIFFON CRGB::Goldenrod;
#define LAVENDER CRGB(102, 51, 153);
#define LIGHT_CYAN CRGB::CornflowerBlue;
#define LIGHT_PINK CRGB::HotPink;
#define PALE_TURQUOISE CRGB::DarkTurquoise;
#define PINK CRGB::DeepPink;
#define PLUM CRGB::Indigo;
#define POWDER_BLUE CRGB::SteelBlue;
// Define the array of leds
CRGB leds[NUM_LEDS];
const byte numChars = 32;
char receivedChars[numChars];
char tempChars[numChars]; // temporary array for use when parsing
//sets variables to be filled with
int neoByte = 0;
int LEDByte = 0;
boolean newData = false;
void setup() {
Serial.begin(9600);
FastLED.addLeds(leds, NUM_LEDS); // declaring Neopixel strip to FastLED
FastLED.setBrightness(20); //adjusting brightest of LEDs
//declaring outputs
pinMode(PIXEL_PIN, OUTPUT);
}
void loop() {
//calls functions for output values
recvWithStartEndMarkers();
if (newData == true) {
strcpy(tempChars, receivedChars);
// this temporary copy is necessary to protect the original data
// because strtok() used in parseData() replaces the commas with \0
parseData();
LEDController();
neoPixelController();
newData = false;
}
delay(10); // delay in between reads for stability
}
void recvWithStartEndMarkers() {
static boolean recvInProgress = false;
static byte ndx = 0;
char startMarker = '<';
char endMarker = '>';
char rc;
while (Serial.available() > 0 && newData == false) {
rc = Serial.read();
if (recvInProgress == true) {
if (rc != endMarker) {
receivedChars[ndx] = rc;
ndx++;
if (ndx >= numChars) {
ndx = numChars - 1;
}
}
else {
receivedChars[ndx] = '\0'; // terminate the string
recvInProgress = false;
ndx = 0;
newData = true;
}
}
else if (rc == startMarker) {
recvInProgress = true;
}
}
}
void parseData() { // split the data into its parts
char * strtokIndx; // this is used by strtok() as an index
strtokIndx = strtok(tempChars,","); // reads the first char in index
neoByte = atoi(strtokIndx); // convert this part to an integer
strtokIndx = strtok(NULL, ","); // this continues where the previous call left off
LEDByte = atoi(strtokIndx); // convert this part to an integer
}
//sets Neopixel color based on serial input
void neoPixelController(){
// listens for incoming data
if(Serial.available() > 0){
//reads neoByte and sets LEDs to corresponsing color
if (neoByte == 1) { //LavenderBlush
for (int i = 0; i < NUM_LEDS; i++) { //itterates over each LED in array
leds[i] = LAVENDER_BLUSH; //set each LED in array to CRGB Color Defined at top
leds[i].r = dim8_video(leds[i].r); // gamma correction function for more nuanced color output
leds[i].g = dim8_video(leds[i].g);
leds[i].b = dim8_video(leds[i].b);
}
} if (neoByte == 2) { //LemonChiffon
for (int i = 0; i < NUM_LEDS; i++){
leds[i] = LEMON_CHIFFON;
leds[i].r = dim8_video(leds[i].r);
leds[i].g = dim8_video(leds[i].g);
leds[i].b = dim8_video(leds[i].b);
}
} else if (neoByte == 3) { //Lavender
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = LAVENDER;
leds[i].r = dim8_video(leds[i].r);
leds[i].g = dim8_video(leds[i].g);
leds[i].b = dim8_video(leds[i].b);
}
} else if (neoByte == 4) { //LightCyan
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = LIGHT_CYAN;
leds[i].r = dim8_video(leds[i].r);
leds[i].g = dim8_video(leds[i].g);
leds[i].b = dim8_video(leds[i].b);
}
} else if (neoByte == 5) { //LightPink
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = LIGHT_PINK;
leds[i].r = dim8_video(leds[i].r);
leds[i].g = dim8_video(leds[i].g);
leds[i].b = dim8_video(leds[i].b);
}
} else if (neoByte == 6) { //PaleTurquoise
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = PALE_TURQUOISE;
leds[i].r = dim8_video(leds[i].r);
leds[i].g = dim8_video(leds[i].g);
leds[i].b = dim8_video(leds[i].b);
}
} else if (neoByte == 7) { //Pink
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = PINK;
leds[i].r = dim8_video(leds[i].r);
leds[i].g = dim8_video(leds[i].g);
leds[i].b = dim8_video(leds[i].b);
}
} else if (neoByte == 8) { // Plum
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = PLUM;
leds[i].r = dim8_video(leds[i].r);
leds[i].g = dim8_video(leds[i].g);
leds[i].b = dim8_video(leds[i].b);
}
} else if (neoByte == 9) { //PowderBlue
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = POWDER_BLUE;
leds[i].r = dim8_video(leds[i].r);
leds[i].g = dim8_video(leds[i].g);
leds[i].b = dim8_video(leds[i].b);
}
}
//sets neopixels to the assigned values
FastLED.show();
//delay for stability
delay(1);
}
}
//sets which LEDs are on based on serial inputs
void LEDController(){
//listen for incoming data
if(Serial.available() > 0){
//these statements read values that have already been delclared
//in p5 based on on screen information and interpret those values
//for the Arduino
//if p5 ball is high on the screen, turn on top LEDs
if(LEDByte == 1){
digitalWrite(LEDR1,LOW);
digitalWrite(LEDL1,LOW);
digitalWrite(LEDR2,HIGH);
digitalWrite(LEDR3,HIGH);
digitalWrite(LEDL2,HIGH);
digitalWrite(LEDL3, HIGH);
//if p5ball is mid screen, turn on middle LEDs
} else if(LEDByte == 2){
digitalWrite(LEDL2, LOW);
digitalWrite(LEDR2, LOW);
digitalWrite(LEDL3, HIGH);
digitalWrite(LEDR1, HIGH);
digitalWrite(LEDR3, HIGH);
digitalWrite(LEDL1, HIGH);
//if p5b ball is low in the screen, turn on bottom LEDs
} else if(LEDByte == 3){
digitalWrite(LEDR3, LOW);
digitalWrite(LEDL3, LOW);
digitalWrite(LEDR1, HIGH);
digitalWrite(LEDR2, HIGH);
digitalWrite(LEDL1, HIGH);
digitalWrite(LEDL2, HIGH);
}
delay(1); //delay inbetween readings for stability
}
}

Step 4: Bringing the Two Together

With our circuits wired and our code complete, all that's left is to play the game! While this simple game of moving a ball and changing its color isn't all that complex, the methods for making everything work together were more difficult than meets the eye and I'm certainly proud of the outcome. I would encourage you to take these principles and knowledge of Serial communication to make your own game!

Here is a link to the complete repository which includes both the Arduino and p5 code.