In the prototypes, we used:
For the final installations, we will be using:
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;
}
}
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
}
}
Based on user testing from the prototypes, we learned: