Monsters Inc.
Scare Factory
Monster Headquarters Mission Control
By: Kurt Blancaflor & Joanna Bailet
Project Description
For our site, we created an interface over processing to replicate the leaderboard in the first "Monster's Inc." movie. We coordinated with the separate remote sites to figure out what kind of information they would be collecting for us, how we would process this information in our programs, and how to communicate and update the data in our interface. As a result, we created a vertical leaderboard containing a list of scarers and their corresponding scare totals. Like in the movie, we programmed this section to automatically organize the scarers from highest to lowest scare totals, and we made it so that the scare totals update according to the scarer chosen in the Employee Check-In Station, and the screams collected in the Scare Canister. Additionally, we added in a section to show the "Last Scarer's" name and an image of their scare captured in the Child's Room site.
​
Finally, we also created a set of statistics to keep track of over a series of scares. These stats include:
​
-
Energy Collected (in SUVs)
-
Kids Scared
-
Number of Accident-Free Days
​
We used an LCD to cycle through these stats, and updated them accordingly based on the codes and information sent to us by the remote sites.
​
​
​
Additional Info
Gallery
Arduino Code:
​
// xBeeSendReceiveMsg
// demo to send and receive data from the xBee
// sending a message is, for this example, simply triggered by pressing a button.
// the program also receives any incoming messages and provesses them.
// a message is any sequence of characters, terminated by a newLine character.
// this demo shows how to parse a message into independent fields and convert
// them to integers, if the field represents a numeric value. the input message is
// assumed to have fields that are delimnited by TAB characters.
#include <SoftwareSerial.h>
#include <Button.h>
// include the library code:
#include "Wire.h"
#include "Adafruit_LiquidCrystal.h"
const String myNodeName = "Monster Control";
const int ledPin = 13;
const int beepPin = 12;
const int scarePin = 8;
const int warningPin = 7;
long energyNum = 799527;
int accidentFree = 33;
int scareNum = 1000;
int displayCount = 0;
bool changeLCD = false;
bool signedIn = false;
// Connect via i2c, default address #0 (A0-A2 not jumpered)
Adafruit_LiquidCrystal lcd(0);
const byte TAB_CHAR = 0x09;
const byte NEWLINE_CHAR = 0x0A;
// information for extracting the fields from the larger message
// maximum number of fields to extract from a single message
// can be made larger -- only limited by memory available
const int MAX_FIELDS = 4;
// initialize the serial port for the xBee with whatever pins are used for it
SoftwareSerial xBee(2, 3); // (TX, RX) : pins on XBee adapter
// declare a button to be used for input, without a pulldown resistor on the board
Button alert(warningPin, INPUT_PULLUP);
Button scare(scarePin, INPUT_PULLUP);
int butter = 0; // counts the number of times the button has been clicked
int messages = 0; // counts the number of messages that have been received
String scarers[] = { "SULLIVAN", "RANDALL", "RANFT", "LUCKEY", "RIVERA", "PETERSON", "JONES", "SANDERSON", "PLESUSKI", "SCHMIDT", "MIKE"};
bool contamination;
bool warning;
void setup() {
// initialize our own serial monitor window
Serial.begin(38400);
//Serial.println("\n\nxBeeSendReceiveMsg");
// set pin modes
pinMode(ledPin, OUTPUT);
pinMode(beepPin, OUTPUT);
// initialize and set the data rate for the SoftwareSerial port -- to send/receive messages via the xBee
xBee.begin(38400);
contamination = false;
warning = true;
// set up the LCD's number of rows and columns:
lcd.begin(16, 2);
}
void loop() {
if (millis() % 4000 == 0) {
lcdLoop();
}
// look for button happenings
int alertPress = alert.checkButtonAction();
int readyPress = scare.checkButtonAction();
buttonAction(warningPin, alertPress);
buttonAction(scarePin, readyPress);
String msg = checkMessageReceived();
readMessages(msg);
if (contamination) {
if (warning) {
digitalWrite(ledPin, HIGH);
} else {
digitalWrite(ledPin, LOW);
}
if ((millis()/1000)%2 == 0) {
warning = !warning;
} else {
digitalWrite(ledPin, LOW);
}
} else {
digitalWrite(ledPin, LOW);
}
// check to see if any complete incoming messages are ready
}
// checks to see if a complete message (one terminated by a newLine) has been received
// if it has, the message will be returned to the caller (without the newLine)
// if not, it will keep accumulating characters from the xBee and just return a null string.
String checkMessageReceived () {
static String msgBuffer = ""; // buffer to collect incoming message: static instead of global !
String returnMsg = ""; // the result to return to the caller
if (xBee.available()) {
// there is at least one character in the input queue of the XBee,
// so fetch it. to prevent blocking the main loop, only one byte
// is fetched on each call. add it to the nsg buffer accumulating the byutes
byte ch = xBee.read();
msgBuffer += char(ch);
// now check to see if this is the message terminator
if (ch == NEWLINE_CHAR) {
// if so, then return the completed message
returnMsg = msgBuffer;
// and clear out the buffer for the next message
msgBuffer = "";
}
else {
// the message isn't complete yet, so just return a null string to the caller
}
}
else {
// nothing has been received, so
// return a null string to the caller
}
return returnMsg;
}
void buttonAction (int buttonPin, int action) {
// count the number of times it is clicked, and
// send a message on the XBee each time
switch (buttonPin) {
case warningPin:
if (action == Button::CLICKED) {
butter++; // increment the counter
// format the message to be sent:
// this example uses a tab to delimit fields and ends it with a newLine termminator
// you can do whatever you want in your message
// AS LONG AS YOU END IT WITH THE NEWLINE CHARACTER
// that is needed for the receiver to detect the end of a message
// String msg = myNodeName + "\t" + 2319 + "\n";
int code;
// blink the LED, beep the piezo, and send the message (also to our serial monitor window)
digitalWrite(ledPin, HIGH);
tone(beepPin, 440, 150);
digitalWrite(ledPin, LOW);
/*
contamination = !contamination;
if (contamination) {
code = 2319;
} else {
code = 1111;
}
*/
code = 1111;
String msg = myNodeName + "\t" + code + "\n";
String processingMsg = myNodeName + "\t" + code + "\n";
contamination = false;
Serial.print(processingMsg);
xBee.print(msg);
//Serial.println(contamination);
}
break;
case scarePin:
if (!signedIn) {
break;
} else {
if (action == Button::CLICKED) {
butter++; // increment the counter
// format the message to be sent:
// this example uses a tab to delimit fields and ends it with a newLine termminator
// you can do whatever you want in your message
// AS LONG AS YOU END IT WITH THE NEWLINE CHARACTER
// that is needed for the receiver to detect the end of a message
// String msg = myNodeName + "\t" + 2319 + "\n";
int code;
// blink the LED, beep the piezo, and send the message (also to our serial monitor window)
digitalWrite(ledPin, HIGH);
tone(beepPin, 440, 150);
digitalWrite(ledPin, LOW);
code = 1112;
String msg = myNodeName + "\t" + code + "\n";
String processingMsg = myNodeName + "\t" + code + "\n";
Serial.print(processingMsg);
xBee.print(msg);
//Serial.println(contamination);
}
break;
}
default:
// if nothing else matches, do the default
Serial.println("code unknown");
break;
}
}
void readMessages(String msg) {
if (msg.length() > 0) {
// if the result is a null string, then there is not a complete message ready
// otherwise we have received a complete message and can process it
// now that we have the message you just recieved in one big string ("msg"),
// it is very likely you might want to parse it using String object methods:
// https://www.arduino.cc/en/Reference/StringObject
// Below is some code that will split out the tab-delimited fields in that big string
// into an array of strings ("msgFields[]"), each entry representing a different field
// (in the order they were received)
String msgFields[MAX_FIELDS];
// intialize them all to null strings each time we start processing a new message
for (int i = 0; i < MAX_FIELDS; i++) {
msgFields[i] = "";
}
// initialize temporary variables used to process each message
int fieldsFound = 0;
String buf = "";
// now, loop through the big single string, character by character, looking for the
// field delimiter (a TAB, or a NEWLINE if it is the last field). accumulate non-delimiters
// in a small buffer, and then use that to transfer to the array of fields.
// once we find a delimiter, we know we've just finished getting a field, so
// store it in the next available element of the array that is accumulating the
// individual fields, and reset the field buffer. also increment the counter tracking
// how many fields we found. if there are more fields in the source string than we have room for,
// toss any extra ones out so we don't overrun the array limits and cause nasty bugs!
for (int i = 0; i < msg.length(); i++) {
if (((msg.charAt(i) == TAB_CHAR) ||
(msg.charAt(i) == NEWLINE_CHAR)) &&
(fieldsFound < MAX_FIELDS)) {
msgFields[fieldsFound] = buf;
buf = "";
fieldsFound++;
}
else {
buf += msg.charAt(i);
}
}
// finally, these fields are all strings, so what if you know that the second field, e.g., is a string
// that really represents a number and you want to use it as an integer? here's what you do:
// -- remember that the index of field #2 is really [1].
String node = msgFields[0];
int code = msgFields[1].toInt();
int scarerIndex;
int scareLevel;
int score;
// and then you could do something like this, if that 2nd field is your message type code:
switch (code) {
case 1111:
Serial.print(node + "\t" + code + "\n");
contamination = false;
signedIn = false;
break;
case 1234:
scareLevel = msgFields[2].toInt();
score = scareLevel * 5;
switch(scareLevel) {
case 1:
case 2:
case 3:
case 4:
Serial.print(node + "\t" + code + "\t" + score + "\n");
energyNum += score;
scareNum++;
break;
default:
break;
}
case 4444:
Serial.print(node + "\t" + code + "\n");
break;
case 2319:
Serial.print(node + "\t" + code + "\n");
contamination = true;
accidentFree = 0;
break;
case 5555:
scarerIndex = msgFields[2].toInt();
switch(scarerIndex) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
// message code = 1 action
Serial.print(node + "\t" + code + "\t" + scarerIndex + "\n");
signedIn = true;
break;
default:
// if nothing else matches, do the default
Serial.println("code unknown");
break;
}
default:
// if nothing else matches, do the default
Serial.print(node + "\t" + code + "\n");
Serial.println("code unknown");
break;
}
}
}
void lcdLoop() {
displayCount++;
if ((displayCount) % 3 == 0) {
lcd.clear();
lcd.print("Energy Collected");
lcd.setCursor(0, 1);
lcd.print(energyNum);
lcd.print(" SUV");
} else if ((displayCount) % 3 == 1) {
lcd.clear();
lcd.print("Accident Free");
lcd.setCursor(0, 1);
lcd.print(accidentFree);
lcd.print(" Days");
} else if ((displayCount) % 3 == 2) {
lcd.clear();
lcd.print("Kids Scared");
lcd.setCursor(0, 1);
lcd.print(scareNum);
} else {
lcd.clear();
lcd.print("Energy Collected");
lcd.setCursor(0, 1);
lcd.print(energyNum);
lcd.print(" SUV");
}
}
​
​
Helpful Processing Links:
​
Processing Code:
​
PImage scareBoard; // Declare variable "a" of type PImage
PImage leaderboardBG2;
PImage warning;
PImage lastScarer;
PFont futura;
// The font "andalemo.ttf" must be located in the
// current sketch's "data" directory to load successfully
//
// jelly.resize(0, 70);
String scarers[] = { "SULLIVAN", "RANDALL", "RANFT", "LUCKEY", "RIVERA", "PETERSON", "JONES", "SANDERSON", "PLESUSKI", "SCHMIDT", "MIKE"};
int scores[] = { 99479, 99351, 79012, 68245, 67922, 67236, 66101, 58986, 55735, 44421, 41918, 38620, 12431 };
HashMap<Integer, String> scareIDs = new HashMap<Integer, String>();
HashMap<Integer, String> scareScores = new HashMap<Integer, String>();
HashMap<Integer, Integer> rankings = new HashMap<Integer, Integer>();
int fontSize;
boolean contamination = false;
float transparency = 255;
int time;
int energyNum = 799527;
int accidentFree = 33;
int scareNum = 1000;
int currentScarer = -1;
String lastScarerName = "SULLIVAN";
String lastNode;
int code;
byte[] byteBuffer = new byte[10];
int bits = 0;
String[] messageParts = {};
import processing.serial.*;
Serial myPort;
int message;
void setup() {
size(1920, 1080);
// The image file must be in the data folder of the current sketch
// to load successfully
scareBoard = loadImage("scareBoard.png"); // Load the image into the program
warning = loadImage("redAlert.gif");
leaderboardBG2 = loadImage("leaderboardBG2.png");
lastScarer = loadImage("SULLIVAN.jpeg");
lastScarer.resize(480, 360);
fontSize = 50;
futura = createFont("FuturaExtended.ttf", fontSize);
for (int i = 0; i < scarers.length; i = i + 1) {
scareIDs.put(i, scarers[i]);
rankings.put(i, scores[i]);
scareScores.put(scores[i], scarers[i]);
}
for (int i = 0; i < messageParts.length; i++) {
messageParts[i] = "" + i;
}
printArray(Serial.list());
myPort = new Serial(this, Serial.list()[0], 38400);
myPort.bufferUntil('\n');
}
void draw() {
// Displays the image at its actual size at point (0,0)
image(scareBoard, 0, 0);
image(leaderboardBG2, 0, 0);
// Displays the image at point (0, height/2) at half of its size
//image(scareBoard, 0, height/2, scareBoard.width/2, scareBoard.height/2);
int [] sortedScores = sort(scores);
sortedScores = reverse(sortedScores);
String [] sortedScarers = {};
textFont(futura);
for (int s = 0; s < sortedScores.length; s = s + 1) {
sortedScarers = append(sortedScarers, scareScores.get(sortedScores[s]));
}
if (contamination) {
background(0);
image(warning, 0, 0);
fill(222, 22, 65);
/*
if (transparency > 0) {
transparency = transparency - 2.5;
} else {
transparency = 255;
}
tint(255, transparency);
*/
rect(0, 0, 1920, 1080);
textSize(200);
fill(0);
textAlign(CENTER);
text(" WARNING", 0, 270, 1920, 270);
textSize(150);
fill(255);
textAlign(CENTER);
text(" CONTAMINATION ALERT", 0, 540, 1920, 270);
} else {
image(leaderboardBG2, 0, 0);
image(lastScarer, 1040, 600);
fill(255, 255, 255);
text("Last Scarer:", 1160, 1030);
text(lastScarerName, 1400, 1030);
updateLeaderboard(sortedScores, sortedScarers);
}
if (contamination && (millis()/1000) % 2 == 0) {
background(0);
image(warning, 0, 0);
fill(222, 22, 65);
/*
if (transparency > 0) {
transparency = transparency - 2.5;
} else {
transparency = 255;
}
tint(255, transparency);
*/
rect(0, 0, 1920, 1080);
textSize(200);
fill(0);
textAlign(CENTER);
text(" WARNING", 0, 270, 1920, 270);
textSize(150);
fill(255);
textAlign(CENTER);
text(" CONTAMINATION ALERT", 0, 540, 1920, 270);
} else {
image(leaderboardBG2, 0, 0);
image(lastScarer, 1040, 600);
fill(255, 255, 255);
text("Last Scarer:", 1160, 1030);
text(lastScarerName, 1400, 1030);
updateLeaderboard(sortedScores, sortedScarers);
}
scarers = sortedScarers;
scores = sortedScores;
}
void serialEvent (Serial myPort) {
// got a new message
// msgCount++;
String myString = myPort.readStringUntil('\n').trim();
println("got input: " + myString);
// Convert the byte array to a String
// String myString = new String(byteBuffer);
// messageParts = split(myString, "*");
messageParts = splitTokens(myString, "\t");
printArray(messageParts);
lastNode = messageParts[0];
code = int(messageParts[1]);
println(lastNode + "\t" + code + "\n");
int scarerIndex;
int scareLevel;
int score;
switch (code) {
case 2319:
contamination = true;
break;
case 1111:
contamination = false;
break;
case 5555:
scarerIndex = int(messageParts[2]);
switch(scarerIndex) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
// message code = 1 action
println(lastNode + "\t" + code + "\t" + scarerIndex);
currentScarer = scarerIndex;
lastScarerName = scarers[scarerIndex];
String lastScarerImage = lastScarerName + ".jpeg";
lastScarer = loadImage(lastScarerImage);
break;
default:
// if nothing else matches, do the default
println("code unknown");
break;
}
case 1234:
scareLevel = int(messageParts[2]);
score = scareLevel * 5;
switch(scareLevel) {
case 1:
case 2:
case 3:
case 4:
println(lastNode + "\t" + code + "\t" + score);
energyNum += score;
scareNum++;
updateScore(currentScarer, score);
break;
default:
break;
}
case 4444:
lastScarer = loadImage("last_scarer.png");
image(lastScarer, 1040, 600);
break;
default:
// if nothing else matches, do the default
println("code unknown");
contamination = contamination;
break;
}
}
void addScarer(String name) {
scarers = append(scarers, name);
scores = append(scores, 0);
}
void mouseClicked() {
//updateScore((int)random(11), (int)random(100000));
updateScore((int)random(11), (int)random(1000));
}
void keyPressed() {
if (key == ' ') {
contamination = !contamination;
}
}
void updateScore(int id, int score) {
//scores[id] = score;
int originalScore = scores[id];
int increasingScore;
for (int i = 0; i <= score; i = i + 1) {
increasingScore = originalScore + i;
scores[id] = increasingScore;
}
// scores[id] = scores[id] + score;
scareScores.put(scores[id], scarers[id]);
}
void updateLeaderboard(int[] sortedScores, String[] sortedScarers) {
for (int i = 0; i < 4; i = i+1) {
for (int k = 1; k < 3; k = k+1) {
fill(255);
rect((k * 320), (i * 270), 320, 270);
for (int n = 0; n < 3; n = n + 1) {
fill(255);
rect((k * 320), ((i * 270) + (n * 90)), 320, 90);
if (i == 0 && n == 0) {
fill(0, 0, 77);
rect((k * 320), ((i * 270) + (n * 90)), 320, 90);
if (k == 1) {
textSize(fontSize);
fill(255);
textAlign(CENTER);
text("SCARE", 320, 0, 320, 90);
} else {
textSize(fontSize);
fill(255);
textAlign(CENTER);
text("TOTALS", 640, 0, 320, 90);
}
} else {
if (k == 1) {
textSize(fontSize);
fill(50);
textAlign(LEFT);
text(" " + sortedScarers[(i * 3) + n - 1], (k * 320), ((i * 270) + (n * 90)), 320, 90);
fill(69, 165, 91);
rect((k * 320), ((i * 270) + (n * 90)) + 80, 320, 10);
} else {
textSize(fontSize);
fill(50);
textAlign(CENTER);
text("" + sortedScores[(i * 3) + n - 1], (k * 320), ((i * 270) + (n * 90)), 320, 90);
fill(69, 165, 91);
rect((k * 320), ((i * 270) + (n * 90)) + 80, 320, 10);
}
}
}
}
}
}
/* Resources Used:
https://processing.org/reference/
https://processing.org/tutorials/2darray/
https://forum.processing.org/one/topic/what-is-the-best-way-to-fade-an-image.html
https://processing.org/examples/loaddisplayimage.html
https://processing.org/tutorials/drawing/
*/
Process and Implementation
​
Software
​
To program Mission Control, we started out by learning how to shape and size elements in a canvas in processing. We then set out to create a 1920 x 1080 canvas inspired by the disaplys shown early on in the first Monsters Inc. movie:
​
We paid careful attention to creating separate rectangles for each individual name and score, so that we can reprint what's written in these rectangles when necessary. After getting these rectangles in place we had the basis for our leaderboard, and we created a background to place behind this section to replicate the design in the movie.
​
Next, we put the scarers and and the scare totals in separate arrays, and created several HashMaps to associate a scarer's "ID" (their index in the original scarer array) to their score. Then we created a method to update scores, sort the arrays according to these scores, and redraw the canvas after these sorts. This allowed us to update the position of the scarers and their scores in order from highest to lowest down the list.
​
After programming this, we also found a way to display a statistic on an LCD, and to have these statistics loop over time. After figuring out the codes to send out to and receive from the remote sites, we found areas in the code to update appropriate stats (for example, after receiving a scare score from the scare canister, we not only updated the scarer's scare total but we also updated the energy collected by the same amount).
​
​
Sending/Receiving Messages to the Remote Sites:
​
We processed the codes using a format that included:
​
(Site Name) + (4-digit Code) + (Information Relevant for the Specific Code)
​
We delineated these segments using the "\t" tab character and denoted the end of a message using the "\n" new line character.
​
Then, once we figured out how to receive and read messages in arduino, we sent them out to processing and processed them in the same way to keep both programs in line with each other.
​
Some of the codes we received included:
​
-
Code 1234: Sent from the Scare Canister followed by a number indicating scare level
-
Code 5555: Sent from the Check-In Station followed by a scarer index
-
Code 2319: Sent from the Child's Room to denote a contamination alert.
​
In the case of a 2319 our processing interface was programmed to blink with a warning alert:
​
​
​
Some of the codes we send included:
​
-
Code 1111: Sent to the Child's Room to initiate the Scare Game
-
Code 1112: Sent to all of the remote sites as an "all clear" message in case of contamination
​
​
Successes & Challenges:
​
While we took care of some of the more complex parts of our project early on (i.e. the self-sorting leaderboard), we did struggle to implement certain features in coordination with the remote site teams. For example, while we knew that our leaderboard would work by using methods to increase scores ourselves, it took us a while to figure out how to increase the scores based on a signal from the remote teams. As mission control, a large part of our job was to communicate and bridge our program with the the other teams, but we approached this fairly late in the process. Of course, adding together so many complex devices resulted in frustrating and often mysterious moments of troubleshooting, but since we didn't work the flow of our project out with the other teams early on we had to scramble to get all of the parts working together.
​
​
Schematics
Below is the schematic of each part of the LCD circuit.
Breadboard Layout
Below is the breadboard layout for the LCD display for the leadership board. Our team used an Arduino Uno, an LCD display with an i2c backpack, an XBee, a piezo buzzer, an LED light, and two buttons. One button gave the all clear signal to the scare room so the scarer knew when they could enter the child's bedroom and the other button sent a contamination warning.