DescriptionA fidget device to help me focus by giving my hands a mindless task to do.

Process:

Image result for snes controller

My visual inspiration: a Super Nintendo Entertainment System controller

Some of the original sketches that detailed some of the form issues I would have to solve.

Designing this project was mostly a pain because of how many electronics had to be fit into such a small space. I consistently had to change the dimensions of the SPES to fit in new components like a power switch or an LED ring.

This is the ATTiny84A, a microcontroller small enough to control the SPES!

The original print of the SPES case. It is very small, and I had to make a hole in the back for a set of batteries.

 

 

 

 

 

 

 

The project was very difficult to power effectively since I was using a NeoPixel LED Ring, which uses a lot of current. My solution was to simply push the electronics as close to the front of the SPES as possible, and sneak two AA batteries in behind them.

In terms of software, I did not experience any real challenges except implementing an event-loop system to make the SPES able to change modes very quickly and reliably, as well as flash its LED’s consistently while switching game modes.

An extremely short video of the first time the event loop for flashing the LED ring worked!

The SPES in its normal place on my desk. It fits in quite well amongst the other pieces.

Discussion:

Some feedback from my classmates included:

“Some light sequences seem a bit jarring.”
I agree with this feedback, in regards to other users. However, I do not find them jarring personally, and this is a very personalized device. If I were to market this kind of product to others, I would certainly dim down any extreme color shifts and perhaps make a more cohesive user interface.

“…it would be cool to have labels.”
I also agree with this. The original plan was to have colorful buttons with letters on them, much like a Super Nintendo console controller, but acquiring the buttons would have been expensive relative to the rest of the project. I could have 3D-printed them, but their quality may have been questionable.

Overall, I am very happy with how this project turned out. It was hard work to manufacture the case for my electronics, but it looks as polished as I would have liked. The software works as was intended, but my color-blending “game” is very hard since my concept of color comes from pigmented colors like paint and colored pencils, not mixing light together. This is somewhat annoying, but helps my mind stay occupied when I am not trying to focus on something else.

I certainly did not enjoy spending all night building that case, so I learned to always start (no matter what other progress I have made) building the container earlier. I am very sensitive to the amount of sleep I get, and the day after finishing the case was absolutely terrible. The case was 3D-printed, but I ran out of material and had to remake it, which taught me to always have material handy.

The project certainly sparked a fun train of thought while I was implementing the color-blending mode. My original idea was to somehow make a different, more “traditional” game out the LED ring, but I changed it relatively late in the process. I got the idea from a game on my iPhone called Blendoku, and simply implemented its logic in a simpler format.

I would like to rebuild the SPES with a prettier body (e.g. not white) and I would like to solder the electronics absolutely perfectly so that I do have to worry about breaking hard-to-detect connections.

Schematic:

This schematic has many pushbuttons with pull-down resistors.

Code:

/*============================================================= 
 * SUPER PULANDO ENTERTAINMENT SYSTEM (SPES)
 *
 * By: Rolando Garcia III
 * 
 * This code controls the SPES, a small handheld fidget device
 * inspired by a Super Nintendo Entertainment System controller.
 *
 * There are two "game" modes:
 *
 * • Fun with LED's - Just play around with mixing colors
 *                    and making a light move around!
 *
 * • Blendoku - You are given two colors and you must mix them properly to
 *              win!
 =============================================================*/

/*============================================================= 
 * PIN MAPPING:
 *
 * • A button - 2
 * • B button - 3
 * • X button - 4
 * • Y button - 5
 * • Select button - 6
 * • Start button - 7
 * • NeoPixel Ring - 8
 * • Joystick switch - 9
 * • Joystick X - A0
 * • Joystick Y - A1
 *
 =============================================================*/

#include <Adafruit_NeoPixel.h>
#define APIN 2
#define BPIN 3
#define XPIN 4
#define YPIN 5
#define SELECT 6
#define START 7
#define LEDS 8
#define STICKSW 9 //MUST BE PULLED UP TO WORK PROPERLY
#define STICKX A0
#define STICKY A1


//Flashing event loop variables
uint32_t lastTimeLightsOn;
uint32_t lastTimeLightsOff;
const int flashInterval = 250; //How long to flash on or off

/*=============================================================
 * INPUTS:
 * • Joystick
 * • Joystick switch
 * • Start Button
 * • Select Button
 * • A
 * • B
 * • X
 * • Y
  =============================================================*/
//Struct to hold a button and its properties, including debouncing properties
struct button {

  int pin; //Pin mapping of button
  int lastState; //Previous state of button
  int buttonState; //Current state of button
  unsigned long lastBounceTime; //Last bounce time of button

};

//Initialize buttons
struct button A = { APIN, LOW, LOW, millis() };
struct button B = { BPIN, LOW, LOW, millis() };
struct button X = { XPIN, LOW, LOW, millis() };
struct button Y = { YPIN, LOW, LOW, millis() };
struct button Select = { SELECT, LOW, LOW, millis() };
struct button Start = { START, LOW, LOW, millis() };
struct button StickSwitch = { STICKSW, HIGH, HIGH, millis() };

//Button debounce function
int debounce(struct button *b) {

  //Get the current reading
  int buttonReading = digitalRead(b->pin);
  bool result = HIGH;

  //If the buttonState has changed
  if (buttonReading != b->lastState) {
    //Reset the bounce timer
    b->lastBounceTime = millis();
  }

  //If enough time has passed, check if the state is the same
  if ( (millis() - (b->lastBounceTime)) > 10 ) {

    if (buttonReading != b->buttonState) {

      b->buttonState = buttonReading;


      // only toggle the LED if the new button state is HIGH
      if (b->buttonState == LOW) {
        result = !result;
      }
    }

  }

  b->lastState = buttonReading;

  return result;

}

//=============================================================
//OUTPUT: NeoPixel Ring (16 LEDs)
//=============================================================
//Initialize the ring
Adafruit_NeoPixel ring = Adafruit_NeoPixel(16, LEDS, NEO_RGBW + NEO_KHZ800);

//Function to clear the lights of all their color
void clearLights(){

  //Loop through all the pixels and set their RGBW values to all 0's
  for(int i = 0; i < 16; i++){
    ring.setPixelColor(i, 0, 0, 0, 0);
  }
  //Then show them so that the change is reflected
  ring.show();
  
}

/*=============================================================
 * GAME MODES:
 *  true = Fun with LEDs
 *  false = Blendoku
  =============================================================*/
//Game state variables
bool gameMode = true;
bool lastMode = false;

//Hue changing variables
int color = 0; //Color being changed
int red = 1; //Value of red
int green = 1; //Value of green
int blue = 1; //Value of blue
const int change = 10; //Amount of change in color during increases or decreases

//Increase the saturation of a certain color with no rollover
void increaseColor() {
  //Determine which color is being changed
  //and ensure that it is not already maxed out
  if(color == 0 && red+change <= 255){
    red += change;
  } else if(color == 1 && green+change <= 255) {
    green += change;
  } else if(color == 2 && blue+change <= 255) {
    blue += change;
  }
}

//Decrease the saturation of a certain color with no rollover
void decreaseColor() {
  //Determine which color is being changed
  //and ensure that it is not already minimized
  if(color == 0 && red-change >= 0){
    red -= change;
  } else if(color == 1 && green-change >= 0) {
    green -= change;
  } else if(color == 2 && blue-change >= 0) {
    blue -= change;
  }
}

//Function to randomize a passed-in hue
void randomizeColors(int *redVal, int *greenVal, int *blueVal){

  *redVal = random(0,255);
  *greenVal = random(0,255);
  *blueVal = random(0,255);
  
}

//Resets all user's hue variables
void resetColors(){
  red = 0;
  green = 0;
  blue = 0;
}

//Flash the center lights to alert the user of the game mode
void flashLights(bool currentMode) {

  //For the funwithLEDs mode, simply flash white lights
  if (currentMode == true) {

    if(millis() - lastTimeLightsOff >= flashInterval){
      clearLights();
      lastTimeLightsOff = millis();
    }

    if(millis() - lastTimeLightsOn >= flashInterval*2){
      for (int j = 0; j < 16; j++) {
  
        ring.setPixelColor(j, 0, 0, 0, 255);
  
      }
      ring.show();
      lastTimeLightsOn = millis();
    }

  } else { //For the blendoku mode, flash colorful lights

    if(millis() - lastTimeLightsOff >= flashInterval){
      clearLights();
      lastTimeLightsOff = millis();
    }
    
    if(millis() - lastTimeLightsOn >= flashInterval*2){
      //For the blendoku game mode, flash colorful lights
      for (int j = 0; j < 16; j++) {
  
        ring.setPixelColor(j, random(0,255), random(0,255), random(0,255), 0);
  
      }
      ring.show();
      lastTimeLightsOn = millis();
    }

  }

}

//=============================================================
//VARIABLES/FUNCTIONS FOR BLENDOKU MODE
//=============================================================
//Random Color 1
int red1;
int green1;
int blue1;
//Random Color 2;
int red2;
int green2;
int blue2;

//New blendoku game flag variable
bool newBlendoku = true;

//Randomizes the colors that the person has to blend
void pick2RandomColors() {

  //Use the randomizeColors() function to do the work for you
  randomizeColors(&red1, &green1, &blue1);
  randomizeColors(&red2, &green2, &blue2);
  
}

//Shows player the two colors of the game
void showPlayerProblem(){

  //Flash the first color
  for (int i = 0; i < 16; i++) {

    ring.setPixelColor(i, green1, red1, blue1, 0);

  }
  ring.show();
  delay(500);

  clearLights();
  delay(500);

  //Flash the second color
  for (int i = 0; i < 16; i++) {

    ring.setPixelColor(i, green2, red2, blue2, 0);

  }
  ring.show();
  delay(500);

  clearLights();
  delay(500);
  
}

//Function to show player's current color combination
void showPlayerColors() {

  for (int i = 0; i < 16; i++) {

    ring.setPixelColor(i, green, red, blue, 0);

  }
  ring.show();
  
}

//Checks the user's solution 
bool checkBlendoku(){

  //Set the threshold for verifying two colors
  const int threshold = 200;

  //Determine the solution
  int solveRed = min(red2,red1) + (int)(abs(red1-red2)/2);
  int solveGreen = min(green2,green1) + (int)(abs(green1-green2)/2);
  int solveBlue = min(blue2,blue1) + (int)(abs(blue1-blue2)/2);
  
  //If the RGB value is between those of the random colors, they solved it
  if( (sq(red-solveRed) + sq(green-solveGreen) + sq(blue-solveBlue)) <= sq(threshold) ) {
    return true;
  }
  
  return false;
  
}

//Flash a green circle when a player solves a blendoku
void success(){

  for(int j = 0; j < 2; j++){
    for (int i = 0; i < 16; i++) {
  
      ring.setPixelColor(i, 255, 0, 0, 0);
  
    }
    ring.show();
    delay(500);
  
    clearLights();
    delay(500);
  }
  
}

//Flash a red circle for a failure
void failure(){

  for(int j = 0; j < 2; j++){
    for (int i = 0; i < 16; i++) {
  
      ring.setPixelColor(i, 0, 255, 0, 0);
  
    }
    ring.show();
    delay(500);
  
    clearLights();
    delay(500);
  }
  
}

//Game mode where the player must blend two colors
void blendoku() {

  //If a new game is initiated, pick two new colors and show them
  if(newBlendoku || debounce(&Start) == LOW){
    clearLights();
    pick2RandomColors();
    resetColors();
    showPlayerProblem();
    newBlendoku = false;
  }

  //Allow the user to recheck the colors they were given
  //by moving the stick up or down
  if(analogRead(STICKY) >= 900 || analogRead(STICKY) <= 100){
    showPlayerProblem();
  }

  //Allow the user to switch the hue they are changing
  if(debounce(&StickSwitch) == LOW){
    color += 1;
    color %= 3;
  }

  if(debounce(&X) == LOW){
    increaseColor();
  }

  if(debounce(&B) == LOW){
    decreaseColor();
  }

  //Also allow them to reset their color selections and start over
  if(debounce(&Y) == LOW){
    resetColors();
  }

  showPlayerColors();

  //If the user confirms their solution, check it and start a new game
  if(debounce(&A) == LOW){
    
    if(checkBlendoku() == true){
      success();
    } else {
      failure();
    }
    newBlendoku = true;
    
  }


}

//VARIABLES/FUNCTIONS FOR FUNWITHLEDS MODE

//Index of the light that is currently on
int lightOn = 0;

//Function to determine light index from joystick position
float lightIndexFromJoystick(int stickX, int stickY){

  //Arctangent magics!
  float angle = atan2(stickY, stickX);
  int lightIndex = 15 - round((angle/1.57)*15);

  return lightIndex;
}

//Fun mode where the user can simply play with colors on the wheel
void funWithLEDs() {

  clearLights();
  int stickX = analogRead(STICKX);
  int stickY = analogRead(STICKY);

  //Find how far the joystick is from its center
  int xDist = stickX - 512;
  int yDist = stickY - 512;

  double distance = sqrt( (float)xDist*xDist + (float)yDist*yDist );

  //Only change the light that is on if the analog stick is fully pushed
  if(distance > 100){
    ring.setPixelColor(lightOn, 0, 0, 0, 0);
    lightOn = lightIndexFromJoystick( stickX, stickY );
  }

  //Allow the user to switch the hue they are changing
  if(debounce(&A) == LOW){
    color += 1;
    color %= 3;
  }

  if(debounce(&X) == LOW){
    increaseColor();
  }

  if(debounce(&B) == LOW){
    decreaseColor();
  }

  //Also allow them to reset their color selections and start over
  if(debounce(&Y) == LOW){
    randomizeColors(&red, &green, &blue);
  }

  if(debounce(&Start) == LOW){
    resetColors();
  }
  
  ring.setPixelColor(lightOn, green, red, blue, 0);
  ring.show();

}

//Switches game modes when the Select button is pressed
void switchGameMode() {
  //Switch the game mode
  gameMode = !gameMode;

  //Reinitialize blendoku
  if(gameMode == false){
    newBlendoku = true;
  }

  //Blank out the lights and reset the timers
  resetColors();
  lastTimeLightsOff = millis();
  lastTimeLightsOn = millis() + flashInterval;
  delay(10);
}

void setup() {
  // put your setup code here, to run once:
  pinMode(APIN, INPUT);
  pinMode(BPIN, INPUT);
  pinMode(XPIN, INPUT);
  pinMode(YPIN, INPUT);
  pinMode(STICKX, INPUT);
  pinMode(STICKY, INPUT);
  pinMode(STICKSW, INPUT_PULLUP); //Remember to pull up for reliable reading

  ring.setBrightness(30); //Lower the default brightness so as not to 
  ring.begin(); //Initialize the LED ring
  ring.show();

  lastTimeLightsOff = millis();
  lastTimeLightsOn = millis() + flashInterval;
}

void loop() {
  
  //Check the select button
  if (debounce(&Select) == LOW) {
    lastMode = gameMode;
    switchGameMode();
  }

  //If the person has not confirmed the game mode, flash the lights at them
  if(lastMode != gameMode && debounce(&Start) == HIGH ){
    resetColors();
    flashLights(gameMode);
    return;
  } else {
    //Otherwise continue into the gameMode
    lastMode = gameMode;
  }

  //Determine the correct game mode and play it
  if (gameMode == true) {

    funWithLEDs();

  } else {

    blendoku();
    
  }
}