Hackster is hosting Hackster Holidays, Ep. 7: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Friday!Stream Hackster Holidays, Ep. 7 on Friday!
Lucas Ainsworth
Published © LGPL

Wizard's Walking Staff

Make your own gesture-controlled Wizard's Walking Staff that lights up to different spells when you perform gestures. You shall not pass!

BeginnerFull instructions provided2 hours11,234

Things used in this project

Hardware components

Arduino 101
Arduino 101
×1
$4 Pololu Voltage Regulator
×1
3x AA battery holder w/switch
×1
AA Batteries
AA Batteries
Any type is fine. You need 3.
×1
M Barrel Jack Adapter
$1.95 These make it easy to connect & disconnect power
×1
F Barrel Jack Adapter
$1.95 These make it easy to connect & disconnect power
×1
Addressable LED Strip
The example is written for any NeoPixel compatible LEDs, 60 pixel strip.
×1
Sports Tape
Any kind of cloth tape. This is used to hold the electronics on the staff and make a nice grip.
×1
Rubber Bands
To help hold parts in place on the staff until you can use tape.
×1
A Staff. (broomstick, sprinkler pipe, natural walking stick, etc)
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Code

Wizard Walking Staff Example Code

Arduino
Count impulses on the x, y, and z axis, certain impulse combinations summon "spells" (light displays) on a neopixel strip.
/*
   Copyright (c) 2016 Intel Corporation.  All rights reserved.
   See license notice at end of file.
*/

/*
  Arduino 101 "Wizard's Walking Staff." 
  This code uses the Adafruit Neopixel library. Library installation instructions here: https://learn.adafruit.com/adafruit-neopixel-uberguide/arduino-library-installation
  Make sure you have the latest Intel Curie Core installed.  For more info, visit https://www.arduino.cc/en/Guide/Arduino101
*/

#include "CurieIMU.h"
#include <Adafruit_NeoPixel.h>
#define PIN 6  //// what pin are the NeoPixels connected to?
Adafruit_NeoPixel strip = Adafruit_NeoPixel(60 , PIN, NEO_GRB + NEO_KHZ800);  ///  the strip is 60 pixels long.

int tr = 0;  //Some variables to hold color target and color current for smoothing...
int tg = 0;
int tb = 0;
int r = 0;
int g = 0;
int b = 0;

long int globaltimer = 0;  /// timers to keep track of gestures vs time...for things like "if timer hasn't passed 0.5sec, AND there's two taps, then..."
long int gesturetimer = 0;
long int ledtimer = 0;

int fade = 10;  /// how quickly lights fade.  Used for smoothing

int tap = 0;  //couter for vertical impulses

int lr = 0;   //couter for left/right impulses

int fb = 0;   //couter for forward/back impulses

int gesture = 0;  // 
int state = 0;  /// for our switch case...  this will keep track of each movement in a series.





void setup() {
  // put your setup code here, to run once:
  //Serial.begin(9600);

  globaltimer = millis();  // start timekeepers at current time
  gesturetimer = millis();
  ledtimer = millis();

  /* Initialise the IMU */
  CurieIMU.begin();
  CurieIMU.attachInterrupt(eventCallback);

  /* Enable Shock Detection */
  CurieIMU.setDetectionThreshold(CURIE_IMU_SHOCK, 1500); // 1.5g = 1500 mg
  CurieIMU.setDetectionDuration(CURIE_IMU_SHOCK, 50);   // milliseconds of spike required to call interupt
  CurieIMU.interrupts(CURIE_IMU_SHOCK);

  strip.begin();  //  intialize neopixel strip
  strip.show();   // Initialize all pixels to 'off'
}

void loop() {
  // put your main code here, to run repeatedly:


  ////basic filter- the IMU registers multiple shocks from rebound and counteraction.  This tries to capture the dominant shock in each gesture.

  if (millis() - globaltimer > 170) {  ///  this tries to find the dominant axis of movement for each shock..  Compares the sum of tap, left-right, and front-back movements, and picks the largest.
    if ((tap > lr) && (tap > fb)) {
      Serial.println("tap");
      gesture = 1;
      tap = 0; lr = 0; fb = 0; ////  reset values after a movement is classified.
    }
    else if ((lr > fb) && (lr > tap)) {
      Serial.println("lr");
      gesture = 2;
      tap = 0; lr = 0; fb = 0;
    }
    else if ((fb > lr) || (fb > tap)) {
      Serial.println("fb");
      gesture = 3;
      tap = 0;  lr = 0; fb = 0;
    }
  }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  if (millis() - globaltimer > 1000) {  /////timeoutreset
    globaltimer = millis() - 170;
tr = 0; tg=0; tb = 0;
    state = 0;
    //gesture=0;
  }
  if (millis() - gesturetimer > 1000) {
    gesturetimer = millis() - 350;
tr = 0; tg = 0; tb = 0;
    state = 0;
     //gesture=0;
  }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  switch (state) {   // This tracks gestures

    case 0: {  /// no gestures recorded yet...  listen for tap.  If there's one, go to case 1.
        if (millis() - gesturetimer > 350) {  
          if (gesture == 1) {
            state = 1;
            gesture = 0;
            gesturetimer = millis();
          }
        }

        break;
      }
    case 1: {  ///// one tap recorded.  If a second tap happens, play a quick flash then go to step 2.
        if (millis() - gesturetimer > 350) {  
          if (gesture == 1) {
            r=10;g=10;b=10;  ///feedback flash
            state = 2;
            gesture = 0;
            gesturetimer = millis();
            
          }
        }

        break;
      }
    case 2: {  ///////Switch point -  two taps recorded.  The three spells diverge here based on the next gesture.  If it's a tap, go to case 3.  If it's L/R, go to case 4. If it's Front/Back, go to case 5.
        
        if (millis() - gesturetimer > 350) {
          if (gesture == 1) {
            state = 3;
            gesture = 0;
            gesturetimer = millis();
          }
          if (gesture == 2) {
            state = 4;
            gesture = 0;
            gesturetimer = millis();
          }
          if (gesture == 3) {
            state = 5;
            gesture = 0;
            gesturetimer = millis();
          }
        }

        break;
      }
    case 3: {  ////  three taps recorded...  we're in the tap spell, turn the staff red and listen for the final "tap" to set off the spell.
      tr = 20; tg = 0; tb = 0;
      globaltimer = millis()-250;
      if (millis() - gesturetimer > 350) {
        if (gesture == 1) {
          state = 0;
          Serial.println("tapspell!");
          tapspell();
        }
      }
        break;
      }
    case 4: {  /////  two taps and a L or R recorded...  we're in the leftspell, turn the staff blue and listen for the final "tap" to set off the spell.
      tr = 0; tg = 0; tb = 20;
      globaltimer = millis()-250;
      if (millis() - gesturetimer > 350) {;
        if (gesture == 1) {
          state = 0;
          Serial.println("leftspell!");
          leftspell();
        }
      }

        break;
      }
    case 5: {  /////  ////  two taps and a forward or back recorded...  we're in the forwardspell, turn the staff green and listen for the final "tap" to set off the spell.
      tr = 0; tg = 20; tb = 0;
      globaltimer = millis()-250;
      if (millis() - gesturetimer > 350) {
        if (gesture == 1) {
          state = 0;
          Serial.println("forwardspell!");
          forwardspell();
        }
      }

        break;
      }
defaut: {
        break;
      }
  }
  //Serial.println(tr);
  
if (millis()-ledtimer > fade){   /// only do this next step periodically every (fade value) milliseconds.  Unlike the "delay()" function, this allows other things to happen in the program in between updates.

   /// color smoothing.  Actual color moves toward target color...  If target is more than curent, move up, if less, move down.
  if (tr > r + 1) {
    r++;
  }
  if (tg > g + 1) {
    g++;
  }
  if (tb > b + 1) {
    b++;
  }
  if (tr < r) {
    r--;
  }
  if (tg < g) {
    g--;
  }
  if (tb < b) {
    b--;
  }
  //turn all the LEDS to the current r, g, b values.
  for (int i = 0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, r, g, b);
  }
  strip.show();
  ledtimer=millis();
}

}

///////////When a shock is detected, the following code interrupts the loop.

static void eventCallback(void)
{
  if (CurieIMU.getInterruptStatus(CURIE_IMU_SHOCK)) {
    if (CurieIMU.shockDetected(X_AXIS, POSITIVE)) {
      tap++;
      globaltimer = millis();
    }

    if (CurieIMU.shockDetected(X_AXIS, NEGATIVE)) {   /// for now, just classifying shocks based on their axis.  positive and negative shocks both are the same.
      tap++;
      globaltimer = millis();
    }

    if (CurieIMU.shockDetected(Y_AXIS, POSITIVE)) {
      lr++;
      globaltimer = millis();
    }

    if (CurieIMU.shockDetected(Y_AXIS, NEGATIVE)) {
      lr++;
      globaltimer = millis();
    }

    if (CurieIMU.shockDetected(Z_AXIS, POSITIVE)) {
      fb++;
      globaltimer = millis();
    }

    if (CurieIMU.shockDetected(Z_AXIS, NEGATIVE)) {

      fb++;
      globaltimer = millis();
    }
  }
}

/////////////////////////////////////////////////spells//////////////////////
void tapspell() {
  ///red theatre lights
  theaterChase(strip.Color(127, 20, 0), 20);
  theaterChase(strip.Color(127, 20, 50), 55);
  strip.show();
}

void leftspell() {
  theaterChase(strip.Color(0, 0, 100), 20);
  theaterChase(strip.Color(0, 30, 120), 75);
  strip.show();
}

void forwardspell() {
  theaterChase(strip.Color(0, 127, 0), 30);
  theaterChase(strip.Color(0, 127, 90), 55);
  strip.show();
}


////////////////Spell functions for lights////////// From the Adafruit Neopixel Strandtest Example
//Theatre-style crawling lights.
void theaterChase(uint32_t c, uint8_t wait) {
  for (int j = 0; j < 10; j++) { //do 10 cycles of chasing
    for (int q = 0; q < 3; q++) {
      for (uint16_t i = 0; i < strip.numPixels(); i = i + 3) {
        strip.setPixelColor(i + q, c);  //turn every third pixel on
      }
      strip.show();

      delay(wait);

      for (uint16_t i = 0; i < strip.numPixels(); i = i + 3) {
        strip.setPixelColor(i + q, 0);      //turn every third pixel off
      }
    }
  }
}

  ////////////////////////////////////////////////////////////////////////////////////////////
/*
   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

Credits

Lucas Ainsworth
2 projects • 41 followers
I'm a designer working in the Modular Innovation Group at Intel.

Comments