Documentation

Prototyping

In the prototypes, we used:

  • 30 push buttons
  • 2 LED light strips
  • 2 Arduinos
  • 2 insulated wire
  • cardboard
We used these to create two smaller versions to test how the spacing could work, as well as making sure we connect them across the country.

Final Physical Build

For the final installations, we will be using:

  • 12 LED light strips
  • 512 push buttons
  • 2 Raspberry Pi Computers
  • 2 Protoboards
  • 2 Arduinos
  • 2 frosted acrylic boards
  • 2 4x4 wood sheets
  • bolts, spacers, and screws
  • 2 power supplies
  • 2 port expanders
We will continue to update this list as we make progress on the final build.

Code for Arduino

The following is the code we implemented to connect the two boards and synchronize the lights and buttons.

        
  
// NeoPixel Ring simple sketch (c) 2013 Shae Erisson
// Released under the GPLv3 license to match the rest of the
// Adafruit NeoPixel library
#include 

#define LEDPIN 4 // NeoPixel pin on Arduino
#define NUMPIXELS 1024 // 1024 of pixels through the whole strip

Adafruit_NeoPixel pixels(NUMPIXELS, LEDPIN, NEO_GRB + NEO_KHZ800); // set up strip

//preview rbg light pins
#define PRERPIN 7
#define PREGPIN 6 
#define PREBPIN 5 

//bay rbg light pins
#define BAYRPIN 10
#define BAYGPIN 9
#define BAYBPIN 8 

//pgh rbg light pins
#define PGHRPIN 13
#define PGHGPIN 12
#define PGHBPIN 11

#define RESETPIN 2 //reset button pin

const char sliderPin = A0; //pin for slider

// reference for button matrix code https://www.baldengineer.com/arduino-keyboard-matrix-tutorial.html
const byte sideSize = 16; // size of one side of the button matrix
const int buttonSize = sideSize*sideSize; // size of all buttons in the matrix
const byte col[sideSize] = {22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37};
const byte row[sideSize] = {38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53};
                          
struct LightClass {
  bool pressed;
  bool on; 
};
typedef struct LightClass Light;

struct ButtonClass {
  Light left;
  Light right;
}; 
typedef struct ButtonClass Button;

unsigned char lr;
unsigned char lg;
unsigned char lb; 
unsigned char rr;
unsigned char rg;
unsigned char rb;

static Button buttonArray[buttonSize];

void setup() {

  pixels.begin(); // INITIALIZE NeoPixel strip object 
  pixels.clear(); // set all pixel colors to "off"
  pixels.show(); // display cleared board

  //initialize all the buttons
  for (int i=0; i<sideSize; i++){
    pinMode(row[i], INPUT);
    pinMode(col[i], INPUT_PULLUP);
  }
  
  for(int i=0; i<buttonSize; i++){

    Light left = {false, false};  //initialize the left Light struct
    Light right = {false, false}; //initialize the right Light struct

    //assign the lights to the button
    buttonArray[i].left = left;
    buttonArray[i].right = right;
    
  }
  
  pinMode(sliderPin, INPUT_PULLUP); //set pinMode for color slider
  pinMode(PRERPIN, OUTPUT);
  pinMode(PREGPIN, OUTPUT);
  pinMode(PREBPIN, OUTPUT);
  pinMode(BAYRPIN, OUTPUT);
  pinMode(BAYGPIN, OUTPUT);
  pinMode(BAYBPIN, OUTPUT);
  pinMode(PGHRPIN, OUTPUT);
  pinMode(PGHGPIN, OUTPUT);
  pinMode(PGHBPIN, OUTPUT);

  pinMode(RESETPIN, INPUT_PULLUP);


  Serial.begin(115200); // to print to the server

}

void loop() {  

  sliderSetColor(analogRead(sliderPin)); //change right rgb colors based on slider

  if(digitalRead(RESETPIN)==LOW) { //reset button pressed
    resetLights();
  }
  
  for (byte c=0; c<sideSize; c++) {

    byte currCol = col[c]; 

    pinMode(currCol, OUTPUT); //enable a column
    digitalWrite(currCol, LOW); 
    
    for (byte r=0; r<sideSize; r++) {
      
      byte currRow = row[r];
      pinMode(currRow, INPUT_PULLUP);

      int buttonIndex = r+c*16; // get index of button
      
      if (digitalRead(currRow) == LOW){ // button at row r col c pressed
        if (!buttonArray[buttonIndex].left.pressed) { // button previously unpressed
          buttonArray[buttonIndex].left.on = !buttonArray[buttonIndex].left.on; // toggle light on/off
          buttonArray[buttonIndex].left.pressed = true; // set previously pressed to true to prevent multiple reads if the user holds down the button
          
          Serial.print(buttonIndex+1); // print index of button (+1 to avoid misreading with 0)
          Serial.print(",");
          Serial.print(lr);
          Serial.print(",");
          Serial.print(lg);
          Serial.print(",");
          Serial.println(lb);

          //calculate lights to change  
          int rightLights = c*64 + r*2; 
          int leftLights = 63-(2*r+1) + 64*c;
          toggleLights(rightLights, buttonArray[buttonIndex].left.on, lr, lg, lb);
          toggleLights(leftLights, buttonArray[buttonIndex].left.on, lr, lg, lb);
          
        }  

      } else if (digitalRead(currRow) == HIGH) { // button at row r col c not pressed
        if (buttonArray[buttonIndex].left.pressed) { // button previously pressed
          buttonArray[buttonIndex].left.pressed = false; // change previously pressed to false, will read button press only after user lets go
        }
      } 
      pinMode(currRow, INPUT);  //turn off pullup, turn off detection from that row
    }
    
    pinMode(currCol, INPUT_PULLUP); //turn off detection from that column 
    
  }
  
  pixels.show();

}


void sliderSetColor(int sliderVal) {

  int inc = 85; //increment for color range, set so we can have 12 colors 
  
  if( sliderVal < inc ) { setColor( 255, 255, 255 ) ; } //set color to white
  else if( sliderVal < inc*2 ) { setColor( 255, 0, 0 ) ; } //set color to red
  else if( sliderVal < inc*3 ) { setColor( 255, 127, 0 ) ; } //set color to orange
  else if( sliderVal < inc*4 ) { setColor( 255, 255, 0 ) ; } //set color to yellow
  else if( sliderVal < inc*5 ) { setColor( 127, 255, 0 ) ; } //set color to chartreuse
  else if( sliderVal < inc*6 ) { setColor( 0, 255, 0 ) ; } //set color to green
  else if( sliderVal < inc*7 ) { setColor( 0, 255, 127 ) ; } //set color to aquamarine
  else if( sliderVal < inc*8 ) { setColor( 0, 255, 255 ) ; } //set color to cyan
  else if( sliderVal < inc*9 ) { setColor( 0, 127, 255 ) ; } //set color to azure
  else if( sliderVal < inc*10 ) { setColor( 0, 0, 255 ) ; } //set color to blue
  else if( sliderVal < inc*11 ) { setColor( 127, 0, 255 ) ; } //set color to violet
  else if( sliderVal < inc*12 ) { setColor( 255, 0, 255 ) ; } //set color to magenta
  else { setColor( 255, 0, 127 ) ; } //set color to rose
}

void setColor(unsigned short r, unsigned short g, unsigned short b) {
  lr = r;
  lg = g;
  lb = b;
  //invert values since rgb leds are anode
  analogWrite(PRERPIN, 255-r);
  analogWrite(PREGPIN, 255-g);
  analogWrite(PREBPIN, 255-b);
  analogWrite(BAYRPIN, 255-r);
  analogWrite(BAYGPIN, 255-g);
  analogWrite(BAYBPIN, 255-b);
  analogWrite(PGHRPIN, 255-r);
  analogWrite(PGHGPIN, 255-g);
  analogWrite(PGHBPIN, 255-b);
}



void toggleLights(int n, bool on, unsigned short r, unsigned short g, unsigned short b){
  if(on) {
    //turn on lights if lights previously off
    setPixels(n, r, g, b);
    setPixels(n+1, r, g, b);
  } else {
    //turn off lights if lights previously on
    erasePixels(n);
    erasePixels(n+1);
  }
}

void setPixels(int index, unsigned short r, unsigned short g, unsigned short b) {
  pixels.setPixelColor(index, r, g, b); 
}

void erasePixels(int index) {
  pixels.setPixelColor(index, 0, 0, 0); 
}

void resetLights(){
  pixels.clear();
  for (int i=0; i<buttonSize; i++) {
    buttonArray[i].left.on = false;
  }
}


        
      

Code for Unity and MQTT

The following code is what we are using to connect MQTT to Unity to look at our button presses digitally.
We used the HIVEMQ public MQTT broker to send and receive messages. To connect our Arduinos to MQTT, we used CMU IDeATe's MQTT-Arduino bridge.

        
          /*
The MIT License (MIT)

Copyright (c) 2018 Giovanni Paolo Vigano'

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using uPLibrary.Networking.M2Mqtt;
using uPLibrary.Networking.M2Mqtt.Messages;

/// 
/// Adaptation for Unity of the M2MQTT library (https://github.com/eclipse/paho.mqtt.m2mqtt),
/// modified to run on UWP (also tested on Microsoft HoloLens).
/// 
namespace M2MqttUnity
{
    /// 
    /// Generic MonoBehavior wrapping a MQTT client, using a double buffer to postpone message processing in the main thread. 
    /// 
    public class M2MqttUnityClient : MonoBehaviour
    {
        // public GameObject Canvas;
        public cloner cloner;
        public GameObject[] clones = new GameObject[256];
        public button_pressl buttpress;

        [Header("MQTT broker configuration")]
        [Tooltip("IP address or URL of the host running the broker")]
        public string brokerAddress = "localhost";
        [Tooltip("Port where the broker accepts connections")]
        public int brokerPort = 1883;
        [Tooltip("Use encrypted connection")]
        public bool isEncrypted = false;
        [Header("Connection parameters")]
        [Tooltip("Connection to the broker is delayed by the the given milliseconds")]
        public int connectionDelay = 500;
        [Tooltip("Connection timeout in milliseconds")]
        public int timeoutOnConnection = MqttSettings.MQTT_CONNECT_TIMEOUT;
        [Tooltip("Connect on startup")]
        public bool autoConnect = true;
        [Tooltip("UserName for the MQTT broker. Keep blank if no user name is required.")]
        public string mqttUserName = null;
        [Tooltip("Password for the MQTT broker. Keep blank if no password is required.")]
        public string mqttPassword = null;
        
        /// 
        /// Wrapped MQTT client
        /// 
        protected MqttClient client;

        private List messageQueue1 = new List();
        private List messageQueue2 = new List();
        private List frontMessageQueue = null;
        private List backMessageQueue = null;
        private bool mqttClientConnectionClosed = false;
        private bool mqttClientConnected = false;

        /// 
        /// Event fired when a connection is successfully established
        /// 
        public event Action ConnectionSucceeded;
        /// 
        /// Event fired when failing to connect
        /// 
        public event Action ConnectionFailed;

        /// 
        /// Connect to the broker using current settings.
        /// 
        public virtual void Connect()
        {
            if (client == null || !client.IsConnected)
            {
                StartCoroutine(DoConnect());
            }
        }

        /// 
        /// Disconnect from the broker, if connected.
        /// 
        public virtual void Disconnect()
        {
            if (client != null)
            {
                StartCoroutine(DoDisconnect());
            }
        }

        /// 
        /// Override this method to take some actions before connection (e.g. display a message)
        /// 
        protected virtual void OnConnecting()
        {
            Debug.LogFormat("Connecting to broker on {0}:{1}...\n", brokerAddress, brokerPort.ToString());
        }

        /// 
        /// Override this method to take some actions if the connection succeeded.
        /// 
        protected virtual void OnConnected()
        {
            Debug.LogFormat("Connected to {0}:{1}...\n", brokerAddress, brokerPort.ToString());

            SubscribeTopics();

            if (ConnectionSucceeded != null)
            {
                ConnectionSucceeded();
            }
        }

        /// 
        /// Override this method to take some actions if the connection failed.
        /// 
        protected virtual void OnConnectionFailed(string errorMessage)
        {
            Debug.LogWarning("Connection failed.");
            if (ConnectionFailed != null)
            {
                ConnectionFailed();
            }
        }

        /// 
        /// Override this method to subscribe to MQTT topics.
        /// 
        protected virtual void SubscribeTopics()
        {
            client.Subscribe(new string[] { "erica" }, new byte[] { MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE });
            client.Subscribe(new string[] { "michelle" }, new byte[] {MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE});
        }

        /// 
        /// Override this method to unsubscribe to MQTT topics (they should be the same you subscribed to with SubscribeTopics() ).
        /// 
        protected virtual void UnsubscribeTopics()
        {
            client.Unsubscribe(new string[] { "erica" });
            client.Unsubscribe(new string[] { "michelle" });
        }

        /// 
        /// Disconnect before the application quits.
        /// 
        protected virtual void OnApplicationQuit()
        {
            CloseConnection();
        }

        /// 
        /// Initialize MQTT message queue
        /// Remember to call base.Awake() if you override this method.
        /// 
        protected virtual void Awake()
        {
            frontMessageQueue = messageQueue1;
            backMessageQueue = messageQueue2;
        }

        /// 
        /// Connect on startup if autoConnect is set to true.
        /// 
        protected virtual void Start()
        {
            GameObject test = GameObject.Find("Canvas");
            cloner cloner = test.GetComponent();
            clones = cloner.clones;
            if(clones == null) {Debug.Log("clones null");}
            // List clones = cloner.cloneList;


            // GameObject combo = GameObject.Find("Canvas/combo");
            GameObject combo = test.transform.Find("combo").gameObject;
            if(combo == null) { Debug.Log("combo null");}
            buttpress = combo.GetComponent();
            if(buttpress == null) { Debug.Log("pretzel null");}


            if (autoConnect)
            {
                Connect();
            }
        }

        /// 
        /// Override this method for each received message you need to process.
        /// 
        protected virtual void DecodeMessage(string topic, byte[] message)
        {
            string msg = System.Text.Encoding.UTF8.GetString(message);
            int lightnum = int.Parse(msg);
            Debug.LogFormat("Message received on topic: {0} - {1}", topic, lightnum);

            if(lightnum == -1) {
                for(int i = 0; i < 256; i++){
                    buttpress.combo = clones[i];
                    buttpress.reset(buttpress.combo);
                }
                Debug.Log("board reset");
            }
            else if(topic == "erica"){
                if (buttpress == null) {
                    Debug.Log("no butt");
                } else if (clones[lightnum] == null) {
                    Debug.Log("no clones");
                } else {
                    if(lightnum>0){buttpress.combo = clones[lightnum-1];}
                    buttpress.TaskOnClickLeft(buttpress.combo);
                }
            }
            else if(topic == "michelle"){
                if (buttpress == null) {
                    Debug.Log("no butt");
                } else if (clones[lightnum] == null) {
                    Debug.Log("no clones");
                } else {
                    if(lightnum>0){buttpress.combo = clones[lightnum-1];}
                    buttpress.TaskOnClickRight(buttpress.combo);
                }
            }
        }

        /// 
        /// Override this method to take some actions when disconnected.
        /// 
        protected virtual void OnDisconnected()
        {
            Debug.Log("Disconnected.");
        }

        /// 
        /// Override this method to take some actions when the connection is closed.
        /// 
        protected virtual void OnConnectionLost()
        {
            Debug.LogWarning("CONNECTION LOST!");
        }

        /// 
        /// Processing of income messages and events is postponed here in the main thread.
        /// Remember to call ProcessMqttEvents() in Update() method if you override it.
        /// 
        protected virtual void Update()
        {
            ProcessMqttEvents();
        }

        protected virtual void ProcessMqttEvents()
        {
            // process messages in the main queue
            SwapMqttMessageQueues();
            ProcessMqttMessageBackgroundQueue();
            // process messages income in the meanwhile
            SwapMqttMessageQueues();
            ProcessMqttMessageBackgroundQueue();

            if (mqttClientConnectionClosed)
            {
                mqttClientConnectionClosed = false;
                OnConnectionLost();
            }
        }

        private void ProcessMqttMessageBackgroundQueue()
        {
            foreach (MqttMsgPublishEventArgs msg in backMessageQueue)
            {
                DecodeMessage(msg.Topic, msg.Message);
            }
            backMessageQueue.Clear();
        }

        /// 
        /// Swap the message queues to continue receiving message when processing a queue.
        /// 
        private void SwapMqttMessageQueues()
        {
            frontMessageQueue = frontMessageQueue == messageQueue1 ? messageQueue2 : messageQueue1;
            backMessageQueue = backMessageQueue == messageQueue1 ? messageQueue2 : messageQueue1;
        }

        private void OnMqttMessageReceived(object sender, MqttMsgPublishEventArgs msg)
        {
            frontMessageQueue.Add(msg);
        }

        private void OnMqttConnectionClosed(object sender, EventArgs e)
        {
            // Set unexpected connection closed only if connected (avoid event handling in case of controlled disconnection)
            mqttClientConnectionClosed = mqttClientConnected;
            mqttClientConnected = false;
        }

        /// 
        /// Connects to the broker using the current settings.
        /// 
        /// The execution is done in a coroutine.
        private IEnumerator DoConnect()
        {
            // wait for the given delay
            yield return new WaitForSecondsRealtime(connectionDelay / 1000f);
            // leave some time to Unity to refresh the UI
            yield return new WaitForEndOfFrame();

            // create client instance 
            if (client == null)
            {
                try
                {
#if (!UNITY_EDITOR && UNITY_WSA_10_0 && !ENABLE_IL2CPP)
                    client = new MqttClient(brokerAddress,brokerPort,isEncrypted, isEncrypted ? MqttSslProtocols.SSLv3 : MqttSslProtocols.None);
#else
                    client = new MqttClient(brokerAddress, brokerPort, isEncrypted, null, null, isEncrypted ? MqttSslProtocols.SSLv3 : MqttSslProtocols.None);
                    //System.Security.Cryptography.X509Certificates.X509Certificate cert = new System.Security.Cryptography.X509Certificates.X509Certificate();
                    //client = new MqttClient(brokerAddress, brokerPort, isEncrypted, cert, null, MqttSslProtocols.TLSv1_0, MyRemoteCertificateValidationCallback);
#endif
                }
                catch (Exception e)
                {
                    client = null;
                    Debug.LogErrorFormat("CONNECTION FAILED! {0}", e.ToString());
                    OnConnectionFailed(e.Message);
                    yield break;
                }
            }
            else if (client.IsConnected)
            {
                yield break;
            }
            OnConnecting();

            // leave some time to Unity to refresh the UI
            yield return new WaitForEndOfFrame();
            yield return new WaitForEndOfFrame();

            client.Settings.TimeoutOnConnection = timeoutOnConnection;
            string clientId = Guid.NewGuid().ToString();
            try
            {
                client.Connect(clientId, mqttUserName, mqttPassword);
            }
            catch (Exception e)
            {
                client = null;
                Debug.LogErrorFormat("Failed to connect to {0}:{1}:\n{2}", brokerAddress, brokerPort, e.ToString());
                OnConnectionFailed(e.Message);
                yield break;
            }
            if (client.IsConnected)
            {
                client.ConnectionClosed += OnMqttConnectionClosed;
                // register to message received 
                client.MqttMsgPublishReceived += OnMqttMessageReceived;
                mqttClientConnected = true;
                OnConnected();
            }
            else
            {
                OnConnectionFailed("CONNECTION FAILED!");
            }
        }

        private IEnumerator DoDisconnect()
        {
            yield return new WaitForEndOfFrame();
            CloseConnection();
            OnDisconnected();
        }

        private void CloseConnection()
        {
            mqttClientConnected = false;
            if (client != null)
            {
                if (client.IsConnected)
                {
                    UnsubscribeTopics();
                    client.Disconnect();
                }
                client.MqttMsgPublishReceived -= OnMqttMessageReceived;
                client.ConnectionClosed -= OnMqttConnectionClosed;
                client = null;
            }
        }

#if ((!UNITY_EDITOR && UNITY_WSA_10_0))
        private void OnApplicationFocus(bool focus)
        {
            // On UWP 10 (HoloLens) we cannot tell whether the application actually got closed or just minimized.
            // (https://forum.unity.com/threads/onapplicationquit-and-ondestroy-are-not-called-on-uwp-10.462597/)
            if (focus)
            {
                Connect();
            }
            else
            {
                CloseConnection();
            }
        }
#endif
    }
}

        
      

User Testing

Based on user testing from the prototypes, we learned:

  • users could easily get bored
  • we would need a key to explain the left and right sides of the buttons
  • boards did not match when there were a lot of inputs (i.e. button presses at the same time)
  • we could set goals for users:
    • make suggestions for users to draw
    • encourage a game of tic tac toe (we could have a preset that transforms board into a tic tac toe board)
  • lights were a little bright (we will be using acrylic to diffuse the light and are considering having a knob to control brightness)
  • we could consider adding a reset button to clear the board