Mike Kushnerik
Published © CC BY-SA

Harry Potter Wand with RP2040 Prop-Maker Feather

Using my Ravenclaw skills, I decided to conjure up a custom electronic wand using the Adafruit RP2040 Prop-Maker Feather!

IntermediateWork in progress2 hours702

Things used in this project

Hardware components

Adafruit RP2040 Prop-Maker Feather with I2S Audio Amplifier
×1
Adafruit Lithium Ion Cylindrical Battery - 3.7v 2200mAh
×1
Adafruit NeoPixel Diffused 5mm Through-Hole LED - 5 Pack
×1
Slide Switch
Slide Switch
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Custom parts and enclosures

Feather Wand - FreeCAD Assembly

Feather Wand - Wand STL

Feather Wand - Carrier STL

Schematics

Fritzing Schematic

Code

Feather Wand Sketch V2

Arduino
//feather wand sketch v2
//author: mike kushnerik
//github.com/miekush
//date: 10/25/2023

#include <Wire.h>
#include <Adafruit_LIS3DH.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_NeoPixel.h>

//define pins
#define RGB_PIN               21
#define LED_PIN               13

//define parameters
#define MOTION_THRES_SPELL          18
#define SPELL_ORIENTATION_THRES     0
#define LUMOS_ORIENTATION_THRES     2
#define SAMPLE_NUM                  50
#define SPELL_TIMEOUT_COUNT         2

Adafruit_NeoPixel rgb(1, RGB_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_LIS3DH lis = Adafruit_LIS3DH();

uint8_t spell=0;

//acc motion sampling variables
uint16_t motionSum=0;
uint8_t averageMotion=0;
uint8_t sampleCount=0;
bool dataReady=false;

//spell variables
bool spellActive=false;
bool spellComplete=false;
uint8_t spellDuration=0;
uint8_t spellActiveCount=0;
uint8_t spellInactiveCount=0;
uint16_t spellMotionSum=0;
uint8_t spellAverageMotion=0;
uint8_t spellMaxMotion=0;

//debug flags
bool serialDebugSimple=true;      //print spells only
bool serialDebugAdvanced=false;   //print spells + raw acc data

//spell format:
//{min motion, max motion, min duration, max duration}
uint8_t spells[4][4]=
{
  {18, 25, 3, 6},   //lumos
  {18, 25, 6, 20},  //
  {30, 50, 3, 6},   //
  {30, 50, 6, 10}   //
};
String spell_names[5]=
{
  "Lumos",
  "Spell 2",
  "Spell 3",
  "Spell 4",
  "Unknown"
};

void setup()
{
  Serial.begin(115200);
  lis.begin(0x18);

  // lis.setRange(LIS3DH_RANGE_4_G);   // 2, 4, 8 or 16 G!
  // lis.setDataRate(LIS3DH_DATARATE_50_HZ);

  //init rgb
  rgb.begin();
  rgb.clear();

  //init external power pin
  pinMode(PIN_EXTERNAL_POWER, OUTPUT);
  digitalWrite(PIN_EXTERNAL_POWER, HIGH);

  //init led
  pinMode(LED_PIN, OUTPUT);
}

void loop()
{
  //poll acc
  updateAccelerometer();

  //check if sample is ready
  if(dataReady)
  {
    digitalWrite(LED_PIN, spellActive);

    if(averageMotion > MOTION_THRES_SPELL)
    {
      //new spell started
      if(spellActive == false)
      {
        spellActive=true;
        spellActiveCount=0;
        spellInactiveCount=0;
        spellDuration=0;
        spellMotionSum=0;
        spellMaxMotion=0;
      }
      spellDuration++;
      spellActiveCount++;
      spellInactiveCount=0;
      spellMotionSum+=averageMotion;

      if(averageMotion > spellMaxMotion)
      {
        spellMaxMotion=averageMotion;
      }
    }
    else
    {
      if(spellActive)
      {
        spellInactiveCount++;
        spellDuration++;

        if(spellInactiveCount > SPELL_TIMEOUT_COUNT)
        {
          spellActive=false;
          spellComplete=true;
          spellAverageMotion=spellMotionSum/spellActiveCount;
        }
      }
    }

    if(serialDebugAdvanced)
    {
      Serial.print("SPELL:");
      Serial.println(spellActive);
    }

    dataReady=false;
  }

  if(spellComplete)
  {
    //make sure wand is facing up
    if(wandOrientation(SPELL_ORIENTATION_THRES))
    {
      //classify spell
      //uint8_t spell = processSpell(spellAverageMotion, spellDuration);
      uint8_t spell = processSpell(spellMaxMotion, spellDuration);

      if(serialDebugSimple || serialDebugAdvanced)
      {
        Serial.println("Spell complete!");
        Serial.print("Duration:"); Serial.print(spellDuration);
        Serial.print(",AverageMotion:"); Serial.print(spellAverageMotion);
        Serial.print(",MaxMotion:"); Serial.print(spellMaxMotion);
        Serial.print(",Spell:"); Serial.println(spell_names[spell]);
      }

      spellAnimation(spell);
    }

    spellComplete=false;
  }
}

//function to poll accelerometer
void updateAccelerometer(void)
{
  if(lis.haveNewData())
  {
    sensors_event_t event;
    lis.getEvent(&event);

    if(serialDebugAdvanced)
    {
      Serial.print("X:"); Serial.print(event.acceleration.x);
      Serial.print(",Y:"); Serial.print(event.acceleration.y);
      Serial.print(",Z:"); Serial.print(event.acceleration.z);
      Serial.print(",MOTION:"); Serial.println(abs(event.acceleration.x)+abs(event.acceleration.y)+abs(event.acceleration.z));
      delay(100);
    }

    //add sample to sum
    motionSum += (abs(event.acceleration.x)+abs(event.acceleration.y)+abs(event.acceleration.z));

    sampleCount++;

    if(sampleCount >= SAMPLE_NUM)
    {
      averageMotion = motionSum / SAMPLE_NUM;
      sampleCount=0;
      motionSum=0;
      dataReady=true;
    }
  }
}

//wand orientation (based on X acc)
//true -> facing up
//false -> facing down
bool wandOrientation(uint8_t threshold)
{
  sensors_event_t event;
  lis.getEvent(&event);
  return (event.acceleration.y < -(threshold));
}

//function to process a spell event
//returns 4 if spell could not be classified
uint8_t processSpell(uint8_t motion, uint8_t duration)
{
  uint8_t spell=4;

  //loop through list of possible spells
  for(int i=0; i<4; i++)
  {
    //check if motion in range
    if(motion >= spells[i][0] && motion < spells[i][1])
    {
      //check if duration in range
      if(duration >= spells[i][2] && duration < spells[i][3])
      {
        spell=i;
      }
    }
  }
  return spell;
}

void spellAnimation(uint8_t spell)
{
  //select spell
  switch(spell)
  {
    //Lumos
    case 0:
      while(wandOrientation(LUMOS_ORIENTATION_THRES))
      {
        rgb.setPixelColor(0, 255, 255, 255);
        rgb.show();
      }
      rgb.setPixelColor(0, 0, 0, 0);
      rgb.show();
      delay(1000);
      break;
    //blinking
    case 1:
      rgb.setPixelColor(0, 0, 0, 255);
      rgb.show();
      delay(1000);
      rgb.setPixelColor(0, 0, 0, 0);
      rgb.show();   
      break;
    case 2:
      for(int i=0; i<5; i++)
      {
        rgb.setPixelColor(0, 255, 0, 0);
        rgb.show();
        delay(100);
        rgb.setPixelColor(0, 0, 0, 0);
        rgb.show();
        delay(100);
      }
      break;
    case 3:
      rgb.setPixelColor(0, 0, 255, 0);
      rgb.show();
      delay(1000);
      rgb.setPixelColor(0, 0, 0, 0);
      rgb.show();    
      break;
    case 4:
      break;
    default:
      break;
  }
}

Credits

Mike Kushnerik

Mike Kushnerik

4 projects • 7 followers
Electrical Engineer

Comments