Project tutorial
Wheel of Misfortune

Wheel of Misfortune © LGPL

The Wheel magically turns & dispenses the worst fates from all your favourite fairy tales. Complete with sound, lights and mythic automata.

  • 2,168 views
  • 1 comment
  • 18 respects

Components and supplies

About this project

Madame Morrow's Wheel of Misfortune

"A History of the Marvel"

Madame Morrow summoned and/or crafted this contraption in the early Autumn of 1880. Over time, it has, inexplicable altered its appearance and --- shall we say, 'abilities' – apparently by absorbing the powers of various magical objects; tarot decks, runes, casting bones, shattered crystal balls and at least one Fortune Teller of great repute. The last “mishap” was recorded in the Summer of 1952 while the Wheel was being shown at Coney Island as a simple “amusement”. Most of the children were found a few days later, and all but two (seemingly) made full recoveries.

It was only recently found and restored after a long storage in the cellars below the Monastery of St. Dunstan the Damned. This is its first exhibition and we have great hopes for what it may be able to accomplish as it helps you explore your Fate.

~The Management

Inspiration for the Build

This project was built as part of the book launch for my wife's new novel, “The Witches of New York”. Inspired by great Victorian amusements and old penny arcades it draws in patrons who seek to know their fate. However, this fortune teller dispenses the hard truth of the fairy tales of old, before Disney and others sanitized and softened the horrible outcomes that often befell the characters in those stories. Blinded, poisoned, imprisoned and being devoured by a selection of beasts are but a few of the fates that await the user.

By placing a hand on each of the palmistry diagrams, the user invites the Wheel to read their fortune. Once the contraption senses the user, the fun begins. Each “Fate” is determined by a spin of the Wheel and is enhanced by appropriate sounds and the light of illuminated, shattered crystal balls. Certain results cause a wand to pan over a variety of beasts and magical mishaps to further enhance and detail the user's impending doom.

Ami meets the Wheel! Cap Touch sensors need calibrating, but it is still pretty awesome :)

WIP...more videos coming soon!

Madame Morrow makes her debut at the book launch for "Witches of New York"

Guiding Principles

I wanted this build to inspire a sense of wonder and magic in the users. To that end I decided that it should be activated by "unseen" means and spin without being handled. While most people interact with Capacitive Touch devices everyday [smart phones and tablets] they are still amazed when something works simply by touch, or close proximity. The HANDS fill this role and the Wheel then moves without being touched. The Lights and Sounds add to the overall effect.

The aesthetics of the piece would be broadly Victorian and give a sense of age. I also wanted it to give the feeling that this was created by someone who was not completely sane [it wasn't that hard] and in touch with mystical forces beyond our keen.

The Hands

The contraption is activated the user placing their hands on the palmsitry art on the front panel. These are CAPACITIVE TOUCH PADS, created by covering much of the back of the hands [paper covered in Gel Medium and mounted on 1/4” plywood] with Copper Tape. The contraption waits for both hands to be activated before beginning. The user is cued how to use it through the instructions mounted on the front panel and LED lights below each touch pad [Marbles glued to brass finials] that illuminate when the pad is reading their palm.

*UPDATE...We discovered it is sensitive enough to detect hands at about 4cm. We are running with this as it is extra creepy and magical.

  • Here is a place to buy Copper Tape. Simple aluminum ducting tape may also be used and can be found in most hardware stores.

The wheel

The Wheel is a collage of art work created in GIMP and adhered `to 1/4” plywood using Gel Medium.

Mounted using HUB and driven by a DC Motor with encoder and secured to the piece with a Motor mount. (UPDATE...encoder broke..maybe...but we didn't need it. Made the whole wheel an encoder using magnets and 2 mag sensors/reed switches. See below)

Nails were sourced at Lee Valley Tools and gave a great "hand made" feel without breaking the bank. They were square nails so I drilled a small hole before inserting them and they held well. A bit too long so I "capped" the pointing end on the back with hot glue to keep it safe and give it a bit more hold. Also used a bit of Gel Super Glue on the front just to be safe.

We had initially planned to have the code select the result before the Wheel spun and turn it, using the encoder, the appropriate amount to land on the selected “fate”.....however......

******UPDATE...Hurray for Failure report ******

So, we had LOTS of trouble with the encoder being precise enough to do what we wished. After multiple attempts and possibly breaking it with a sudden "turn the other way" command as the Wheel was spinning at full speed. We came up with the idea of turning the Wheel itself into an "encoder". We now place Magnets just after the "border" of each slice and counted the interrupts. This will always let us know what section we are in [stored in an Array]. Instead of "aiming" for a particular section, we now spin the wheel a couple times and then simply use the "count" to determine where the Wheel has landed. over The "zeroing" function [next paragraph] acts as a fail safe.

At the end of each use the Wheel returns to the "?" pie slice and waits for the next user. This not only is kinda creepy, it serves a function in the programming. It is a Reset and “zeroing” action utilizing Magnets mounted on the back of the wheel and a second Mag Sensor [positioned lower than the first and matched with a magnet on the back of the Wheel behind the "Happy" and "Start" positions. Before it starts to spin it always checks to make sure it is where it thinks it is. If it doesn't sense the magnet, it turns until it does. From this zero point it can then turn and begin COUNTS to properly report the "slice" and deliver its fortune. It can also count this ZERO point each time it makes a full rotation for added accuracy and to act as a fail safe.

***UPDATE...we actually have it checking each time it passes "happy" and "start" as a double fail safe. After it passes "start" it begins the count again. After it passes "happy" it starts the count at 5. If it has missed detecting a magnet as it turns, this helps reset it. There is also a delay and a fail safe to make sure it can't count "start" twice while it waits to be spun...it only listens for the lower magnet sensor if it passes two upper magnets beforehand.

This is necessary because the Wheel could move between uses. Passers by, wind, or the odd poltergeist could handle the wheel and move it off its start position. This function plans for the failure and corrects it.

  • Burned out one DC motor because I put too much load on it. The Motor with the higher torque and lower RPM I ended up using was just right. I ordered both at the same time because I can never calculate what is needed without some hands on testing.
  • Spend the money on a real motor, hub and mount. I have wasted many hours recycling old motors and kludging together some kind of connector/mount.
  • How to use an Encoder with a DC motor. Great introduction, vids and code to get you started. ****UPDATE...this is HARD. If I had the money and time a powerful stepper would better do the job, but then we couldn't have figured out our awesome solution of turning the Wheel itself into our encoder :)
  • Getting the Wheel to work as an encoder like we wanted was a chore, with many tests, adjustments to the magnets, mounts, sensors and code. However, it was worth it and now we could tackle a similar project with relative ease. Many lessons learned. Glad we did it :)
  • To keep people from messing with the wheel itself we plan to add this sign; "Keep hands away from Wheel. Any fingers it takes, it keeps. -The Managment"

The wands

The wands serve to expand the possible outcomes for the user. If the Wheel lands on "Devoured by Beasts" or "Magickal Mishap" [the two largest pie slices] the appropriate wand will begin to move and stop on one of the possibilities. These are accompanied by lights and a sound effect for each.

Servo driven with mounted brass tube, epoxy and marbles. Paper and Gel Medium.

  • To properly ID the different results we did a slow "sweep" of each wand. I called out when it was in position over each item and my son, who was watching the serial monitor, told me the value which I wrote down. We could now tell the servo where to go when someone was going to be eaten by an Owlbear. How cool is that?:)

The lights

I visited my local salvage yard and looked for brass finials with pass throughs that allowed me to install the LEDs with relative ease. Extra connectors were purchased at the hardware store and affixed with epoxy.

To add to the feel of the overall piece I started with marbles as a nod to crystal balls but hated the way they looked. I found this video on how to fry marbles to create a fractured look and thought it would suit. I think it added quite a bit to the overall look of the piece. The marbles were attached using epoxy or super glue gel.

The 4 large marble/crystals are all backlit by RGB LEDs. These are all driven through a series of transistors...one for each colour. All 4 LEDs have each of their colour leads going to a perf board where the Red, Green and Blue are bundled together and activated by the Arduino through a transistor. This way, all 4 lights will pulse and change colour in synch. There is far too much current to drive these directly from the Arduino and so they are powered through the perf board.

More photos/Videos of the perf board, arduino and other related electronics to follow. This is a WIP and I am on a deadline :)

The Automata

This will have two servo driven automata mounted on the top cross bar. One is a Skull that turns into a Medusa if "Turned to Stone" is your fate. The other, a ship being attacked by a Kraken type beast if the Wheel selects "Lost at Sea". These are WIP, but I have some pics of what I have so far. (UPDATE...ran out of time to make all the Kraken arms and ship work together. See pics...soon... for my Amor Fati Frame solution that runs if the Wheel lands on "orphaned". A quick and simple one, but I like the effect.) While some may rightly claim these are not true automata, I am using the term because I love the art form and am doing my best to learn more about it with this build given my time and ability.

The ART

The artwork and collages were all created using GIMP and an inkject printer. I used some card stock and some parchment-type paper, but just plain printer paper works as well.

I adhered it all to the wood using Gel Medium, which is essentially acrylic paint without any tint. It works far better than Hodge Podge.

  • Simply put a thin coat on the surface you wish to attach the paper to.
  • Carefully place the paper and smooth out, slow and steady to remove all the air bubbles. You have lots of time. Don't rush. Fingers or the pinkie side of your hand in a fist works well.
  • Once it is smooth, apply another thin coat with a good brush or foam brush.
  • Wait for it to be dry [or close to it] and apply another...and another...repeat at least 3 times.
  • USE A CLEAN CLEAN CLEAN brush. Better yet, use one you only use for Gel Medium. ANY tint or colour still in/on the brush will transfer over.
  • You could use a spray varnish on top, or other acrylics if you wish.

Sounds

My son, who is also the primary programmer, has collected and noted the details for a sound library to use with the Wheel. This will be stored on a microSD card and installed in the Adafruit MP3 Music Maker Shield. More details on that once we get o the full testing phase. There will be video!

  • We only will be using one speaker. It seems that running the 2 that come in the package needs too much power. No biggie. One works great.
  • WAVs are distorted. Will be using all MP3s.
  • Each section has multiple sounds and these are selected randomly once it stops. This is to increase the fun and suspense.

He also created a few "soundscapes" using Audacity.

Credits

My wife Ami McKay inspired this piece, both through her writing and her love and ongoing support for my Makerish lifestyle. My son, Jonah, is a CodeMonkey extraordinaire and has done all the heavy programming related lifting for this and other projects. My eldest, Ian [a talented artist], taught me the wonders of Gel medium and is a Sun who warms every soul with his goofy heart. A family that creates together will always build a loving home.

Ami's newest novel, the reason for this piece, launches Oct 25th, 2016 [just in time for Halloween!] in Canada and the UK. It will be published by Harper Collins in the US July of 2017. The Wheel will be on the stage with her during her book launch and her fans can use it while waiting for her to sign their copies of her book. I hope to add videos of people enjoying the piece after the event.

*Maker's Note

My projects are generally larger scale with multiple systems and some arty components. As such, I rarely do the "full instruction" kind of project share. However, I am very open to answering specific questions left in comments about how/why I may have done something seen in the project posted here. I hope to inspire and possibly assist with these project showcases, so please feel free to post your comments/questions and I will do my best to address them.

This would not have been possible if not for the efforts of this CodeMonkey.

Code

Wheel of Misfortune --- Arduino sketchArduino
Still a WIP, but mostly tuning and calibration.
// Source code for the Wheel of Misfortune (2016)
// Written by Jonah McKay
//
// A Monkey Dream Monkey Do project
// http://www.monkeydreammonkeydo.com
// Contact e-mail: ian@amimckay.com

//LIBRARIES START

#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>

//SOUND LIBRAIRIES START

#include <SPI.h>
#include <Adafruit_VS1053.h>
#include <SD.h>

//SOUND LIBRAIRIES END

//CAP TOUCH LIBRARY START

#include <CapacitiveSensor.h>

//CAP TOUCH LIBRARY END

//LIBRARIES END

//VARIABLES START

//SERVO VARIABLES BEGIN

// called this way, it uses the default address 0x40
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();

#define MEDUSASERVOMIN  150 // this is the 'minimum' pulse length count (out of 4096)
#define MEDUSASERVOMAX  570 // this is the 'maximum' pulse length count (out of 4096)

#define SUBFATELEFTSERVOMIN  280 // this is the 'minimum' pulse length count (out of 4096)
#define SUBFATELEFTSERVOMAX  580 // this is the 'maximum' pulse length count (out of 4096)
#define SUBFATERIGHTSERVOMIN  200 // this is the 'minimum' pulse length count (out of 4096)
#define SUBFATERIGHTSERVOMAX  580 // this is the 'maximum' pulse length count (out of 4096)

#define ENDPOINTRIGHT 385 //CORRECT
#define ENDPOINTLEFT 455 //CORRECT

//SERVO VARIABLES END

//SOUND VARIABLES START

// These are the pins used for the music maker shield
#define SHIELD_RESET  -1      // VS1053 reset pin (unused!)
#define SHIELD_CS     7      // VS1053 chip select pin (output)
#define SHIELD_DCS    6      // VS1053 Data/command select pin (output)

// These are common pins between breakout and shield
#define CARDCS 4     // Card chip select pin
// DREQ should be an Int pin, see http://arduino.cc/en/Reference/attachInterrupt
#define DREQ 3       // VS1053 Data request, ideally an Interrupt pin

Adafruit_VS1053_FilePlayer musicPlayer = Adafruit_VS1053_FilePlayer(SHIELD_RESET, SHIELD_CS, SHIELD_DCS, DREQ, CARDCS);

//SOUND VARIABLES END

//WHEEL VARIABLES START

int motorTicks = 0;
int motorTickLimit = 4;

const int enableA = 36;
const int motorA1 = 38;
const int motorA2 = 40;

long timeDestinationReached;

long timeRunUntilStarted = 0;

long timeZeroOutStarted = 0;

int motorStatus = 0;

//WHEEL VARIABLES END

//SENSOR PINS START

//MAG

const int magPin=30;

const int zeroMagPin=31;

boolean zeroOutStarted = false;

boolean lastMagSensorState = LOW;
boolean lastZeroMagSensorState = LOW;

boolean lastZeroWasStart = false;

long timeMagLastTriggered = 0;
long timeZeroMagLastTriggered = 0;

boolean zeroMagFound = false;
long timeZeroMagFound = 0;

int segmentsSinceLastZero = 0;

//CAP

CapacitiveSensor capacitiveTouchLeft = CapacitiveSensor(A10, A11);

CapacitiveSensor capacitiveTouchRight = CapacitiveSensor(A8, A9);

int leftCapNegativeIncrement = 0;
int rightCapNegativeIncrement = 0;

boolean leftCapActive = false;
boolean rightCapActive = false;

long timeLeftCapSet = 0;
long timeRightCapSet = 0;

const long requiredCapActiveTime = 1000;

const int capSensitivity = 55;

const int capIncrementLimit = 10;

//SENSOR PINS END

//LIGHT PINS START

const int redLEDS = 41;
const int greenLEDS = 37;
const int blueLEDS = 39;

const int whiteLeftLED = 8; //BREAKOUT
const int whiteRightLED = 9; //BREAKOUT

//LIGHT PINS END

//SUBFATE VARIABLES START

//Dragon, Troll, Witch, Owlbear, Wolves, Giant Spider, Goblins, Dogs, Rats, Insects
int eatenServoLocations[] = {216, 249, 280, 307, 342, 423, 458, 486, 516, 550};

//Lycanthrope, Zombie, Stone, Scarecrow, Raven, Frog, Amnesia, Madness, Sleep, Haunted
int mishapServoLocations[] = {282, 305, 345, 372, 398, 422, 495, 519, 543, 568};

int currentSubFate = 0;

boolean subFatePicked = false;

boolean subFateWandReturned = false;

boolean subFateWandAtDestination = false;

boolean subFateSoundPlayed = false;

long timeSubFateSoundEnded = 0;

//MEDUSA --

long timeMedusaStarted = 0;

boolean medusaDone = false;

//SUBFATE VARIABLES END

//SYSTEM VARIABLES START

boolean totalSilence = false; //disable ALL sounds if true

boolean wheelRunning = false;

boolean wheelActive = false;

boolean goingToFate = false;

boolean fateRunning = false;
boolean fateFinished = false;

long timeFateFinished = 0;

boolean wheelReturned = false;

long timeFateSoundEnded = 0;

boolean bootedUp = false;

int destinationResult;

bool destinationReached = false;

int fateLocations[] = {5000, 5500, 4500, 4250, 5250, 4000}; //miliseconds

bool fateSoundPlayed = false;

int zeroingOutForFate = false;

int fateDegreeLocations[] = {3, 44, 85, 126, 177, 183, 224, 265, 307, 357, 361}; //Last is to set off origin OBSOLETE?

int currentSegment = 0;

long lastWheelPositionTime = 0;

int segmentZeroStartedOn = 0;

long currentRunTime = 0;

bool foundStartDuringZero = false;

bool runningFate = false;

//Some Degree Location IDS:
//0: ? - beginning, or origin
//1: eaten
//2: blinded
//3: orphaned
//4: cused
//5: happy
//6: mishap
//7: poisoned
//8: imprisoned
//9: lost at sea
//10: While this should technically not be given to some functions, treat it like origin

//SYSTEM VARIABLES END

//IDLE VARIABLES START

boolean silentIdle = false; //disable sounds during idle if true

long timeOfLastIdleLightBlink = 0;

long timeOfLastTaunt = 0;

int waitForNextBlink = 200;

boolean currentIdleLightState;

//IDLE VARIABLES END
//DEBUG VARIABLES START

long timeSinceLastSerialDump = 0;

//DEBUG VARIABLES END

//VARIABLES END

void setup()
{
  //setup, runs once
  Serial.begin(9600);

  //SERVOS BEGIN --
  
  pwm.begin();
  
  pwm.setPWMFreq(60);  // Analog servos run at ~60 Hz updates

  //SERVOS END --

  //SOUND BOARD START --

   if (! musicPlayer.begin()) { // initialise the music player
     Serial.println(F("Couldn't find VS1053, do you have the right pins defined?"));
     while (1);
  }
  Serial.println(F("VS1053 found"));
  
  SD.begin(CARDCS);    // initialise the SD card
  
  // Set volume for left, right channels. lower numbers == louder volume!
  musicPlayer.setVolume(20,20);

  // Timer interrupts are not suggested, better to use DREQ interrupt!
  //musicPlayer.useInterrupt(VS1053_FILEPLAYER_TIMER0_INT); // timer int

  // If DREQ is on an interrupt pin (on uno, #2 or #3) we can do background
  // audio playing
  musicPlayer.useInterrupt(VS1053_FILEPLAYER_PIN_INT);  // DREQ int
  
  //SOUND BOARD END --

  //WHEEL START --

  pinMode(enableA, OUTPUT);
  pinMode(motorA1, OUTPUT);
  pinMode(motorA2, OUTPUT);
  
  //WHEEL END --

  //SENSOR PINS START --

  //MAG
  
  pinMode(magPin, INPUT);
  pinMode(zeroMagPin, INPUT);
  
  //SENSOR PINS END --

  //LIGHT PINS START --

  pinMode(redLEDS, OUTPUT);
  pinMode(greenLEDS, OUTPUT);
  pinMode(blueLEDS, OUTPUT);
 
  //LIGHT PINS END --

  //SET RANDOM
  pinMode(A15, OUTPUT);
  randomSeed(analogRead(A15));
  
  yield();
}

void loop()
{
  //Start off by reading the Capacitive Touch sensors
  readCapTouch();
  updateCapTouchLights();


  //If they're both active...
  if (leftCapActive && rightCapActive)
  {
    //Check that they have:
    //    1: both been on for requiredCapActiveTime, or 1000 ms usually
    //    2: we are not already running the wheel
    //    3: and that we are reading the zeroMagPin, which means we're at Start 
    //       (could also mean we're at Happy, watch out!)
    if (timeLeftCapSet < millis()-requiredCapActiveTime && timeRightCapSet < millis()-requiredCapActiveTime && 
    !runningFate && digitalRead(zeroMagPin) == HIGH)
    {
      //If all that is true, then let's start the wheel!
      randomSeed(millis());
      setWhiteLights();
      chooseRandomDestination();
     // zeroingOutForFate = true;
      startWheelZeroOut();
      bootedUp = true;
      
    }
  }

  //If we're running the fate spin, then let's runCurrentDestination(), which handles
  //the nitty-gritty of all the spinning, loop() only needs to care about this function
  //for running the fate
  if (runningFate)
  {
    if (runCurrentDestination())
    {
      //If we're done, then say we're done, reset
      //variables, especially runningFate to false so we're not running
      //runCurrentDestination() more than once: see above if statement
      runningFate = false;
      fateFinished = false;
      destinationReached = false;
      timeFateFinished = millis();

      //If the zeroMagPin is high, then we can confidently say that
      //we're at start, the zero out should of handled this already,
      //but we can doubly-say that now.
      if (digitalRead(zeroMagPin) == HIGH)
          currentSegment = 0;
    }
  }
  //If we're not running a fate, then we're IDLING. Do some fancy light blinks,
  //and occasionally a taunt. IDLE LOOP CODE START --
  else
  {
    //Flicker the lights.
    if (millis()-timeOfLastIdleLightBlink > waitForNextBlink)
    {
      //If the light is currently on, then turn it off, and vice-versa.
      currentIdleLightState = !currentIdleLightState;
      
      if (currentIdleLightState) //on
          //Most of the time, set it to white lights...
          //Occasionally, (1 in 5) set it to either blue, or red.
          if (random(0, 5) <= 3)
              setWhiteLights();
          else
          {
              if (random(0, 2) == 0)
                  setBlueLights();
              else
                  setRedLights();
          }
                  
      else                   //off
          setOffLights();
      waitForNextBlink = random(100, 2000); //large variation keeps things interesting
      timeOfLastIdleLightBlink = millis();
    }

    //wait 30 seconds since both:
    //  1. the last taunt
    //  2. the wheel of fate stopped spinning
    //to taunt again
    if (millis()-timeOfLastTaunt > 30000 && millis() > timeFateFinished+30000)
    {
      tauntUser(); //also sets timeOfLastTaunt
    }
  }

  //END IDLE LOOP CODE --
  
  //Update/Read the magSensors every loop, so that we don't miss a segment
  //going by, remember the wheel is spinning _fast_
  updateMagSensors();

  
  //START DEBUG


  //Just print stuff to serial, specifically where it thinks it
  //is on the wheel and sub-fates every second
  if (timeSinceLastSerialDump < millis()-1000)
  {
    Serial.println("BEGIN SERIAL DUMP");
    printLocationName();
    printEatenSubFate();
    printMishapSubFate();
    timeSinceLastSerialDump = millis();
  }
  
  //END DEBUG
  
}


//START SOUND FUNCTIONS

void playSound(char path[])
{
  //playSound is basically just a wrapper function that takes a path name
  //and feeds it into the musicPlayer. If the totalSilence variable is true,
  //then it won't play. While the sound is running it also holds up everything
  //else.
  if (!totalSilence)
      musicPlayer.startPlayingFile(path);
  while (musicPlayer.playingMusic)
  {
    delay(10); //just wait
  }
}

//END SOUND FUNCTIONS


//START PRIMARY MOTOR FUNCTIONS --

void runMotor()
{
  //Runs the motor forwards (clockwise)
  digitalWrite(enableA, HIGH);
  digitalWrite(motorA1, HIGH);
  digitalWrite(motorA2, LOW);
}

void reverseMotor()
{
  //Runs the motor backwards (counter-clockwise)
  digitalWrite(enableA, HIGH);
  digitalWrite(motorA1, LOW);
  digitalWrite(motorA2, HIGH);
}

void stopMotor()
{
  //Stops the motor. Note that this doesn't automatically freeze the
  //wheel in place, it just makes it coast.
  digitalWrite(enableA, LOW);
  digitalWrite(motorA1, LOW);
  digitalWrite(motorA2, LOW);
}

void runWheelMotor(int motorSpeed)
{
  //Runs the wheel motor, sets the speed based on motorSpeed.
  //For best effect, run as many times as possible.
  //motorSpeed works like this: 1 is 50% forward, -1 is 50% backward,
  // 2 is 33% forward, so higher numbers are actually slower.
  //
  if (motorSpeed < 0)
  {
    if (motorTicks > motorTickLimit)
    {
      if (!wheelRunning && motorTicks > motorTickLimit*abs(motorSpeed) || wheelRunning)
      {
      wheelRunning = !wheelRunning;
      motorTicks = 0;
      }
    }
  
    if (wheelRunning)
    {
      reverseMotor();
    }
    else
    {
      stopMotor();
    }
    
    motorTicks++;
    motorStatus = -1;
  }
  else if (motorSpeed == 0)
  {
    stopMotor();
    motorStatus = 0;
  }
  else
  {
    if (motorTicks > motorTickLimit)
    {
      if (!wheelRunning && motorTicks > motorTickLimit*abs(motorSpeed) || wheelRunning)
      {
      wheelRunning = !wheelRunning;
      motorTicks = 0;
      }
    }
  
    if (wheelRunning)
    {
      runMotor();
    }
    else
    {
      stopMotor();
    }
    motorTicks++;
    motorStatus = 1;
  }
}

bool startWheelZeroOut()
{
  //Sets up variables for a wheel zero out. If the wheel is already
  //on Start, then it sets everything as though the wheel zero out
  //already happened, then returns true.
  segmentZeroStartedOn = currentSegment;
  timeZeroOutStarted = millis();
  
  if (digitalRead(zeroMagPin) == LOW || currentSegment != 0) //not already there
  {
    Serial.println("Wheel Zero Out - Not here aleady");
    zeroMagFound = false;
    zeroOutStarted = true;
    wheelReturned = false;
    foundStartDuringZero = false;
    timeZeroMagFound = 0;
    return false;
  }
  else //if we're already there
  {
    Serial.println("Wheel Zero Out - already here!");
    zeroMagFound = true;
    zeroOutStarted = false;
    wheelReturned = true;
    currentSegment = 0;
    zeroingOutForFate = false;
    lastZeroWasStart = true;
    foundStartDuringZero = true;
    timeZeroMagFound = 0;
  }
  return true;
}

boolean zeroOutWheel()
{
  //zero out wheel function, basically keep running it in a loop, and it should
  //update to try to reach start. If it starts in Lost at Sea or Imprisoned, then
  //every segment has the same speed. If not, then it slows down the wheel upon reaching
  //Lost at Sea. When it reaches start, depending on where it started, it gives it a jolt
  //back to try to keep it in Start, as opposed to keep coasting on.
  if (currentSegment != 0 && !foundStartDuringZero)
  {
    if (currentSegment < 7 || segmentZeroStartedOn > 7) //not lost at sea - not right next to it
            if (segmentZeroStartedOn <= 3)
                runWheelMotor(10);
            else if (segmentZeroStartedOn <= 4)
                runWheelMotor(9);
            else if (segmentZeroStartedOn <= 5)
                runWheelMotor(8);
            else if (segmentZeroStartedOn <= 6)
                runWheelMotor(9);
            else if (segmentZeroStartedOn >= 8) //special slowness for imprisoned and lost
                runWheelMotor(10);
            else
                runWheelMotor(7);
    else
        if (segmentZeroStartedOn <= 4 && currentSegment >= 7)
            if (segmentZeroStartedOn <= 2)
                 runWheelMotor(25);
            else
                 runWheelMotor(25);
        else if (segmentZeroStartedOn <= 6 && currentSegment >= 8) //If mishap or less, then start slow at imprisoned, else, lost at sea
            runWheelMotor(20);
        else if (segmentZeroStartedOn > 6 && currentSegment > 8)
            runWheelMotor(20);
        else
            runWheelMotor(14);
  }
  
  if (currentSegment == 0 || foundStartDuringZero)
  {
    foundStartDuringZero = true;
      if (!zeroMagFound)
      {
      zeroMagFound = true;
      timeZeroMagFound = millis();
      }
    if (timeZeroMagFound+230 > millis())
    {
        //give it a jolt back, power depending on where the
        //zero out started.
        if (segmentZeroStartedOn <= 3)
            runWheelMotor(-2);
        else if (segmentZeroStartedOn <= 6)
            runWheelMotor(-2);
        else if (segmentZeroStartedOn <= 8)
            runWheelMotor(-4);
        else
            runWheelMotor(-8);
    }
    else
    {
      //We've reached our destination, and jolted back. Shut down the motor,
      //set variables accordingly, and return true to let the calling function
      //know we're done.
      runWheelMotor(0);
          zeroMagFound = true;
          zeroOutStarted = false;
          wheelReturned = true;
          currentSegment = 0;
          zeroingOutForFate = false;
          lastZeroWasStart = true;
          return true;
          
    }
  }
  return false;
}

boolean spinWheelUntil(long timeStarted, int timeLimit)
{
  //Spins the wheel for a random amount of time (set by
  //chooseRandomDestination(), and put in through timeLimit
  //and timeStarted (timeStarted being when it started running,
  //timeLimit being how long it should run after that.
  //After it does that, then coast. Wait until it comes to a
  //full stop, then return true to let the caller function
  //know we're done. If it's ever on Start, then turn on the
  //motor so that it won't end on Start, giving it enough of
  //a boost to land on Eaten instead.
  //This function should be called frequently
  //in the runCurrentDestination() function itself, so that it
  //can also do things with lights and such if it
  //wishes.

  long currentTimePast = millis()-timeStarted;
  
  if (wheelActive)
  {
    if (currentTimePast < timeLimit/3)
    {
      runWheelMotor(2);
    }
    else if (currentTimePast < timeLimit)
    {
      runWheelMotor(4); //slow down when approaching the end
    }
    else
    {
      if (currentSegment != 0)
          runWheelMotor(0); //full coast
      else
          runWheelMotor(4); //If we're still on start, then give it some more boost to get out
    }

    if (currentTimePast > timeLimit && lastWheelPositionTime < millis()-1500)
    {
      //If we haven't changed our wheel position in a second and a half, then we're
      //at a complete stop, hopefully. Return true.
      return true;
    }
  }
  else
  {
    //DO SOME WAITING STUFF HERE TODO
  }

  return false;
}

void chooseRandomDestination()
{
  //Pick a random destination, and prepare everything for the runCurrentDestination()
  //function that will be called repeatedly in loop().
  wheelActive = true;
  goingToFate = true;
  runningFate = true;
  destinationReached = false;
  destinationResult = random(0, 6);
  timeRunUntilStarted = millis();
  currentRunTime = fateLocations[destinationResult]+random(-750, 750);
  lastWheelPositionTime = 0;

  //Reset to 0 HACK FIX TODO, because this function can only be called
  //while the zeroMag is HIGH, so this makes sense in context
  currentSegment = 0;
  lastZeroWasStart = true;
  timeZeroMagLastTriggered = millis();
  lastWheelPositionTime = millis();
  segmentsSinceLastZero = 0;
}

bool runCurrentDestination()
{
  //First, it zeroes out. Then it runs using spinWheelUntil for a random
  //amount of time (determined by chooseRandomDestination). Then, run
  //the right fate function, let that do it's thing until that returns
  //true. Then, after the fate is finished running, zero out again,
  //returning to start. After that, we can finally say we're done and
  //return true.
  
  if (!zeroingOutForFate)
  {
    if (goingToFate)
    {
      if (spinWheelUntil(timeRunUntilStarted, currentRunTime))
      {
        //We're done the main spin, spinWheelUntil returned true,
        //so let's start the fate now.
        Serial.println("NO MORE!");
        goingToFate = false;
        wheelActive = false;
        destinationReached = true;
        timeDestinationReached = millis();
      }
    }
    else if (!fateFinished)
    {
      //If the fate isn't finished, and we're not going to the fate, then
      //let's run the fate. Once it's returned true, we can finally say
      //the fate is done, and fateFinished = true;
      if (runFate(currentSegment, millis()-timeDestinationReached))
          fateFinished = true;
    }

    else
    {
      //If we're neither going to the fate, running the fate, or
      //zeroing out, then start the return zero out.
      Serial.println("Starting return back");
      startWheelZeroOut();
      zeroingOutForFate = true;
    }
  }
  else
  {
    //Zero out the wheel, returning it to start, both to prepare
    //and for returning...
    if (zeroOutWheel())
    {
      //If we're done zeroing out...
      Serial.println("Zero out finished");
      //Set the timeRunUntilStarted here, so spinWheelUntil() doesn't count the time spent zeroing out
      //against it's time limit.
      timeRunUntilStarted = millis();
      zeroingOutForFate = false;
      if (fateFinished)
      {
        //If the fate is finished, then that means that this was the return zero out,
        //meaning that this is the last thing that this function needs to do was
        //completed. Return true, our job is done.
        return true;
      }
    }
  }

  return false;
}

//END PRIMARY MOTOR FUNCTIONS --


//START SERVO FUNCTIONS --

boolean medusaServos(int spot)
{
  //Medusa servo function. Uses the spot function (takes in a variable based on a start
  //time and millis() to determine a timeline)
  int servoPosArr[] = {0, 0, 0, 0};
  if (spot > 3000)
  {
      return true;
  }
  else if (spot < 500)
  {
    servoPosArr[1] = map(spot, 0, 500, MEDUSASERVOMIN, MEDUSASERVOMAX-40);
    servoPosArr[2] = map(spot, 0, 500, MEDUSASERVOMIN, MEDUSASERVOMAX-280);
    servoPosArr[0] = map(abs(spot-500), 0, 500, MEDUSASERVOMIN+40, MEDUSASERVOMAX);
    servoPosArr[3] = map(abs(spot-500), 0, 500, MEDUSASERVOMIN+230, MEDUSASERVOMAX);
  }
  else if (spot > 500 && spot < 2000)
  {
    servoPosArr[1] = (MEDUSASERVOMAX-40)+random(-20-((spot-500)/3), 20); //top left
    servoPosArr[2] = (MEDUSASERVOMAX-280)+random(-20-((spot-500)/3), 20); //bottom left
    servoPosArr[0] = (MEDUSASERVOMIN+40)+random(-20, 20+((spot-500)/3)); //top right
    servoPosArr[3] = (MEDUSASERVOMIN+230)+random(-20, 20+((spot-500)/3)); //Bottom right
  }
  else if (spot > 2000 && spot < 2500)
  {
    servoPosArr[1] = map(abs((spot-2000)-500), 0, 500, MEDUSASERVOMIN, MEDUSASERVOMAX-40);
    servoPosArr[2] = map(abs((spot-2000)-500), 0, 500, MEDUSASERVOMIN, MEDUSASERVOMAX-280);
    servoPosArr[0] = map(abs(spot-2000), 0, 500, MEDUSASERVOMIN+40, MEDUSASERVOMAX);
    servoPosArr[3] = map(abs(spot-2000), 0, 500, MEDUSASERVOMIN+230, MEDUSASERVOMAX);
  }
  else if (spot > 2500)
  {
    servoPosArr[1] = MEDUSASERVOMIN;
    servoPosArr[2] = MEDUSASERVOMIN;
    servoPosArr[0] = MEDUSASERVOMAX;
    servoPosArr[3] = MEDUSASERVOMAX;
  }

  for (int x = 11; x < 15; x++)
  {
    pwm.setPWM(x, 0, servoPosArr[x-11]);
  }
  return false;
}

//END SERVO FUNCTIONS --

//START CAPACITIVE TOUCH FUNCTIONS --

void readCapTouch()
{
  //Read the capacitive touch sensors, and put set their variables
  //for use in loop()
  long capTotalLeft = capacitiveTouchLeft.capacitiveSensor(10);
  long capTotalRight = capacitiveTouchRight.capacitiveSensor(10);

  if (capTotalLeft > capSensitivity)
  {
    if (!leftCapActive)
        timeLeftCapSet = millis();
    
    leftCapNegativeIncrement = 0;
    leftCapActive = true;
    
  }

  //Basically, we have the capIncrementLimit so that one small value doesn't completely reset
  //the time count used to make sure the hands were pressed down for a second in loop()
  else
  {
    if (leftCapNegativeIncrement < capIncrementLimit)
    {
      leftCapNegativeIncrement++;
    }
    else
    {
      leftCapActive = false;
    }
  }

  if (capTotalRight > capSensitivity)
  {
    if (!rightCapActive)
        timeRightCapSet = millis();
    
    rightCapNegativeIncrement = 0;
    rightCapActive = true;
    
  }
  else
  {
    if (rightCapNegativeIncrement < capIncrementLimit)
    {
      rightCapNegativeIncrement++;
    }
    else
    {
      rightCapActive = false;
    }
  }
}

void updateCapTouchLights()
{
  //Updates the capTouch lights, nothing fancy, just turns on/off LEDS
  //based on true/false variables
  if (leftCapActive)
  {
    //Turns on left LED
    pwm.setPWM(whiteLeftLED, 0, 4095);
  }
  else
  {
    //Turns off left LED
    pwm.setPWM(whiteLeftLED, 0, 0);
  }

  if (rightCapActive)
  {
    //Turns on right LED
    pwm.setPWM(whiteRightLED, 0, 4095);
  }
  else
  {
    //Turns off right LED
    pwm.setPWM(whiteRightLED, 0, 0);
  }
}

//END CAPACITIVE TOUCH FUNCTIONS --

//START FATE FUNCTIONS --

//Fate functions, all of them return false if they're still running, and
//true if they're done. Most just run a sound, turn on a light, then wait
//for their time to run out, then return true, but some do servos, and notably
//Eaten and Mishap do sub-fates, and stand out from the rest.

boolean fate_orphaned(int spot)
{
  if (spot < 6000) //Also do servos for orphaned
  {
    setGreenLights();
    if (!fateSoundPlayed)
    {
      int soundResult = random(0, 2);
      if (soundResult == 0)
          playSound("orphan.mp3");
      else
          playSound("orphan2.mp3");
      fateSoundPlayed = true;
    }
    return false;
  }
  else
  {
    return true; //done
  }
}

boolean fate_lost(int spot)
{
  if (spot < 8000)
  {
    setBlueLights();
    if (!fateSoundPlayed)
    {
      int soundResult = random(0, 2);
      if (soundResult == 0)
          playSound("lost.mp3");
      else
          playSound("lost2.mp3");
      fateSoundPlayed = true;
    }
    return false;
  }
  else
  {
    return true; //done
  }
}

boolean fate_poisoned(int spot)
{
  if (spot < 5000)
  {
    setGreenLights();
    if (!fateSoundPlayed)
    {
      int soundResult = random(0, 2);
      if (soundResult == 0)
          playSound("poison.mp3");
      else
          playSound("poison2.mp3");
      fateSoundPlayed = true;
    }
    
    return false;
  }
  else
  {
    return true; //done
  }
}


boolean fate_cursed(int spot)
{
  if (spot < 5000)
...

This file has been truncated, please download it to see its full contents.

Comments

Similar projects you might like

Remote Lamp

Project tutorial by Kutluhan Aktar

  • 2,044 views
  • 0 comments
  • 7 respects

Interactive Donation Box

Project showcase by Ian McKay

  • 2,132 views
  • 0 comments
  • 6 respects

Use the Force... Or your Brainwaves?

Project tutorial by Tamas Imets

  • 29,220 views
  • 18 comments
  • 101 respects

Ouija Board Access Control

Project tutorial by TheRiverPeople

  • 4,526 views
  • 1 comment
  • 21 respects

Measurino: A Measuring Wheel Proof of Concept

Project tutorial by fmarzocca

  • 1,395 views
  • 2 comments
  • 15 respects

FlightGear Analog Trim Tab Wheel

Project tutorial by dancili

  • 1,004 views
  • 2 comments
  • 6 respects
Add projectSign up / Login