PE
Force Fielder
Videography By Charles Elmer
Intro
This interactive experience creates force simulations based on inertial motion data planted in the hilt of a light saber. The system requires two users, one that holds the light saber and another that controls a laser beam. When the light saber successfully blocks the laser beam the motion data generatates a particle field online. Force Fielder is a multi faceted submission that uses a wide range of technology in this prototype. This work was inspired by Episode 4 where Luke is first channeling the force with Obi Wan Kenobi on the Millenium Falcon.I highly encourage reusing parts of my experience to build your own iteration as this project wouldn't have been possible without open source software.
Best,
Paul
Technology>
The light emmited from the laser triggers a motion data event that an event listener interprets into Force Fields. This system has three main applications using different software platforms. The nodes are connected to each other through physical and digital means.
*Originally the laser controller was a Parrot Transporter Drone but the execution didn't reflect the original concept. See details below..
Hardware
- Light Saber
- Particle Photon
- Sparkfun Photon IMU Shield
- Sparkfun Photon Battery Shield
- 3.3v Lithium Ion Battery 1000mah
- Adafruit Perma-Proto Quarter-Sized Breadboard PCB
- Solid Core 22 Gauge Wire
- 3 photoresistors
- 3 10k resistors
- 1 Neo Pixel Strip
- Elastic Strip
- Needle
- Thread
- Electrical Tape
- Various PVC Pipes
- Fluroscent Light Tubing
- Soldering Iron
- Solder
- Laser Beam Controller
- LEAP Motion Controller
- 2 Micro Servo Motors
- Particle Photon
- USB Micro Cable
- Small 3.3v Laser Pointer
- Solid Core 22 Gauge Wire
- Breadboard
- Zip ties
- Clamp
- Square Wooden Dowel Rod
- Fog Machine
- Laser Beam Drone
- Parrot Transporter Drone
- Small 3.3v Laser Pointer
- Mini Lipo battery
Software
- Particle Build IDE
- Light Saber Dependencies
- Sparkfun LSM9DS1
- Neopixel
- Application
- Laser Beam Dependency
- Voodoo Firmware
- Server Side Javascript (Node.js) in Sublime IDE
- Cylon.js (Node Package Dependencies)
- cylon-spark
- cylon-leapmotion
- cylon-gpio
- cylon-i2c
- cylon
- Client Side Javascript and HTML in Sublime IDE
- P5 Processing for js library
- Particle (Formerly Spark) JS Library For The Particle Photon
- Laser Beam Drone
- Particle Build IDE
Light Saber>
If you're unfamiliar with using the Photon please read the Getting Started Guide.
Hardware
- Build the shaft of the saber by connecting various pieces of pvc plumbing tubing ensuring that the base is large enough to contain the embedded electronics and that the top is a snug fit with one end of the fluroscent tubing. Cover the hilt in black tape and set aside.
- Stack a Photon on top of the imu shield, on top of a battery shield with the male headers on the battery shield cut off.
- Solder and heatshrink 3 photoresistors to stranded core wire of increasing length to space out the light sensors at the bottom, middle, and top of the saber.
- Solder the wire from the photo resistors to one side of the proto board with 10k resistors arranged like so.
- Solder the associated lines from the Fritzing diagram below to the pcb through holes on the back side of the battery shield at pins a0-a5 with the resistor lines to a3-a5 and the direct lines from a0-a2.
- Measure and cut a desired length of a Neopixel strip (try to stay under 60 pixels to still work with the 3.3v power source) dependent on the length of the fluroscent tube.
- Solder wire from the Neo Pixel din side also to the back of the battery shield. The connections are Neo DIN to Shield D2, ground to ground, and Neo VCC to Shield 3.3v. It's important that you connect the Neo Pixel direct line to D2 so not to conflict with the digital io pins the imu shield is occupying.
- Measure and cut an elastic band the same length as the Neo Pixel Strip that will be placed directly over the LEDs
- Cut three holes in the elastic that match the distance from the photoresistors to the hilt.
- Insert the photoresistors through the holes with the light sensing side not directly facing the LEDs.
- Sew the strips together with a needle and thread by pushing the needle through the Neo Pixel strip's plastic encasing at the sides to avoid damaging the inner circuitry. I taped the end of the strip with blue electrical tape to ensure the two strips stay alligned.
- Insert the circuit through the pvc tubing, gently pulling the light strip through the saber end of the hilt.
- Put the light strip inside of the fluorescent tubing. Secure the tubing to the hilt with electrical tape.
- Connect the battery to the JST plugin on the battery shield.
- Place the cover on the bottom of the hilt to secure the photon in place.
Software
In the Build IDE code we'll use the example file from the Sparkfun LSM9DS1 combined with a few tidbits from the Neo Pixel library example and the Particle publish example. This laser listener creates an average threshold from on and off values of 3 (photoresistors) then publishes the IMU data in one large string when the threshold is crossed.
//include libraries
#include "neopixel/neopixel.h"
#include "application.h"
#include "SparkFunLSM9DS1/SparkFunLSM9DS1.h"
#include "math.h"
//default behavior
SYSTEM_MODE(AUTOMATIC);
/*****************************************************************
LSM9DS1_Basic_I2C.ino
SFE_LSM9DS1 Library Simple Example Code - I2C Interface
Jim Lindblom @ SparkFun Electronics
Original Creation Date: April 30, 2015
https://github.com/sparkfun/SparkFun_LSM9DS1_Particle_Library
This code is released under the MIT license.
Distributed as-is; no warranty is given.
*****************************************************************/
//////////////////////////
// LSM9DS1 Library Init //
//////////////////////////
// Use the LSM9DS1 class to create an object. [imu] can be
// named anything, we'll refer to that throught the sketch.
LSM9DS1 imu;
///////////////////////
// Example I2C Setup //
///////////////////////
// SDO_XM and SDO_G are both pulled high, so our addresses are:
#define LSM9DS1_M 0x1E // Would be 0x1C if SDO_M is LOW
#define LSM9DS1_AG 0x6B // Would be 0x6A if SDO_AG is LOW
////////////////////////////
// Sketch Output Settings //
////////////////////////////
#define PRINT_CALCULATED
//#define PRINT_RAW
#define PRINT_SPEED 250 // 250 ms between prints
// Earth's magnetic field varies by location. Add or subtract
// a declination to get a more accurate heading. Calculate
// your's here:
// http://www.ngdc.noaa.gov/geomag-web/#declination
#define DECLINATION -8.58 // Declination (degrees) in Denver, CO.
int boardLed = D7; // This is the LED that is already on your device.
//three photoresistor input pins
int photoresistor1 = A0;
int photoresistor2 = A1;
int photoresistor3 = A2;
// This is the other end of your photoresistor. The other side is plugged into the "photoresistor" pin (above).
// The reason we have plugged one side into an analog pin instead of to "power" is because we want a very steady voltage to be sent to the photoresistor.
// That way, when we read the value from the other side of the photoresistor, we can accurately calculate a voltage drop.
int power1 = A3;
int power2 = A4;
int power3 = A5;
int intactValue; // This is the average value that the photoresistor reads when the beam is intact.
int brokenValue; // This is the average value that the photoresistor reads when the beam is broken.
int beamThreshold; // This is a value halfway between ledOnValue and ledOffValue, above which we will assume the led is on and below which we will assume it is off.
int analogvalue; // Here we are declaring the integer variable analogvalue, which we will use later to store the combined value of the photoresistors
int aVal1,aVal2,aVal3 = 0;//initiate values for each photoresistor as well
bool contactMade = false;//ensure that after crossing the threshold the saber must go back below the threshold before again publishing an event
bool one,two,three =false;//keep track of which sensor crosses the threshold for the feedback system
float roll;//z
float pitch;//x
float heading;//y
//establish neo pixel info
#define PIXEL_PIN D2
#define PIXEL_COUNT 43
#define PIXEL_TYPE WS2812B
//create neo pixel object
Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);
int _PIXEL_COUNT = PIXEL_COUNT; // pretty code work around
void setup()
{
//test imu settings in the particle cli with the photon connected by usb to your computer
//IMU
Serial.begin(115200);
// Before initializing the IMU, there are a few settings
// we may need to adjust. Use the settings struct to set
// the device's communication mode and addresses:
imu.settings.device.commInterface = IMU_MODE_I2C;
imu.settings.device.mAddress = LSM9DS1_M;
imu.settings.device.agAddress = LSM9DS1_AG;
// The above lines will only take effect AFTER calling
// imu.begin(), which verifies communication with the IMU
// and turns it on.
if (!imu.begin())
{
Serial.println("Failed to communicate with LSM9DS1.");
Serial.println("Double-check wiring.");
Serial.println("Default settings in this sketch will " \
"work for an out of the box LSM9DS1 " \
"Breakout, but may need to be modified " \
"if the board jumpers are.");
while (1)
;
}
//Light
// First, declare all of our pins. This lets our device know which ones will be used for outputting voltage, and which ones will read incoming voltage.
pinMode(photoresistor1,INPUT);
pinMode(photoresistor2,INPUT);
pinMode(photoresistor3,INPUT);
// The pin powering the photoresistor is output (sending out consistent power)
pinMode(power1,OUTPUT);
pinMode(power2,OUTPUT);
pinMode(power3,OUTPUT);
//initiate and clear the neo pixels
strip.begin();
strip.clear();
strip.show();
// Write the power of the photoresistor to be the maximum possible, so that we can use this for power.
digitalWrite(power1,HIGH);
digitalWrite(power2,HIGH);
digitalWrite(power3,HIGH);
// We are going to declare a Particle.variable() here so that we can access the value of the photoresistor and the beam threshold from the cloud.
Particle.variable("analogvalue", &analogvalue, INT);
Particle.variable("beamThresh", &beamThreshold, INT);
delay(500);
//turn the strip on with a brightness of 100 to establish the broken value
colorWipe(strip.Color(0, 0, 255), 5); // Blue
// Now we'll take some readings...
int off_1 = (analogRead(photoresistor1) + analogRead(photoresistor2) + analogRead(photoresistor3)/3); // read photoresistors
delay(200); // wait 200 milliseconds
int off_2 = (analogRead(photoresistor1) + analogRead(photoresistor2) + analogRead(photoresistor3)/3); // read photoresistors
delay(300); // wait 300 milliseconds
delay(500);//delay half second
//turn the strip to full brightness for the intact value
brightColor(strip.Color(0, 0, 255), 5,255);
delay(500);//delay half second
// ...And we will take two more readings.
int on_1 = (analogRead(photoresistor1) + analogRead(photoresistor2) + analogRead(photoresistor3)/3); // read photoresistors
delay(500); // wait 500 milliseconds
int on_2 = (analogRead(photoresistor1) + analogRead(photoresistor2) + analogRead(photoresistor3)/3); // read photoresistors
// Now we average the "on" and "off" values to get an idea of what the resistance will be when the laser strikes
intactValue = (on_1+on_2)/2;
brokenValue = (off_1+off_2)/2;
// Let's also calculate the value between laser and no laser to find the crossing point that triggers an event.
beamThreshold = (intactValue+brokenValue)/2;
//turn the saber back to the low brightness (off) setting to prepare for sensing laser contact
brightColor(strip.Color(0, 0, 255), 5,100);
}
void loop()
{
//store the values of each light sensor
aVal1 = analogRead(photoresistor1);
aVal2 = analogRead(photoresistor2);
aVal3 = analogRead(photoresistor3);
// check to see what the average value of the photoresistors are and store it in the int variable analogvalue
analogvalue = ((aVal1 + aVal2 + aVal3)/3);
//if any of the sensors have crossed the threshold set the associated boolean to true
if(aVal1 > beamThreshold){
one = true;
}
if(aVal2 > beamThreshold){
two = true;
}
if(aVal3 > beamThreshold){
three = true;
}
//if any of the sensors have crossed the threshhold and contact hasn't been made
if(one||two||three)
{
if(!contactMade){
imu.readAccel();
imu.readMag();
imu.readGyro();
//update the pitch, roll and heading
// Call print attitude. The LSM9DS1's magnetometer x and y
// axes are opposite to the accelerometer, so my and mx are
// substituted for each other.
printAttitude(imu.ax, imu.ay, imu.az, -imu.my, -imu.mx, imu.mz);
String allData = getData();//put the imu data in a string
Particle.publish("contact", allData);//publish a contact event with the sensor data
//flash the section of the saber that made contact
if(one){redFlash(1);}
if(two){redFlash(2);}
if(three){redFlash(3);}
delay(500);//wait half a second
brightColor(strip.Color(0, 0, 255), 5,100);//turn the saber back to the low brightness (off) setting
contactMade = true;//contact!
one,two,three = false;//set sensor threshold booleans back to false after successful post
}
}
else
{
contactMade = false;//the saber exited or didn't intersect the beam
}
}
/********************
* IMU Functions
* ******************/
String getData()
{
// To read from the accelerometer, you must first call the
// readAccel() function. When this exits, it'll update the
// ax, ay, and az variables with the most current data.
imu.readAccel();
imu.readMag();
imu.readGyro();
//package all the data in a string with spaces to make seperating values on the event listener end easy
String data = String(imu.calcAccel(imu.ax)) + " " + String(imu.calcAccel(imu.ay)) + " " + String(imu.calcAccel(imu.az)) + " "
+ String(imu.calcGyro(imu.gx)) + " " + String(imu.calcGyro(imu.gy)) + " " + String(imu.calcGyro(imu.gz)) + " "
+ String(imu.calcMag(imu.mx)) + " " + String(imu.calcMag(imu.my)) + " " + String(imu.calcMag(imu.mz)) + " "
+ String(roll) + " " + String(pitch) + " " + String(heading) ;
return data;
}
// Calculate pitch, roll, and heading.
// Pitch/roll calculations take from this app note:
// http://cache.freescale.com/files/sensors/doc/app_note/AN3461.pdf?fpsp=1
// Heading calculations taken from this app note:
// http://www51.honeywell.com/aero/common/documents/myaerospacecatalog-documents/Defense_Brochures-documents/Magnetic__Literature_Application_notes-documents/AN203_Compass_Heading_Using_Magnetometers.pdf
void printAttitude(
float ax, float ay, float az, float mx, float my, float mz)
{
roll = atan2(ay, az);
pitch = atan2(-ax, sqrt(ay * ay + az * az));
heading;
if (my == 0)
heading = (mx M_PI) heading -= (2 * M_PI);
else if (heading
Laser Controller>
- Follow James Bruce's Laser Turret Tutorial while substituting a Photon for the Arduino.
- Secure the laser and servos together with zip ties then tie the module to a wooden dowel rod that is clamped to a surface.
- Load Voodoospark Firmware onto said Photon by copying the Voodoo firmware into the Build IDE and flashing said firmware to the laser controller
- Download and install Node.js on your computer.
- In the terminal (I'm on a mac), install Cylon.js and all library dependencies by running the following commands either globally (using the -g command after the word install) to access the libraries from anywhere on your computer or navigate to your project directory and run the npm installs locally as I did to upload a github project that includes all the dependencies necessary to run the program.
$ npm install cylon $ npm install cylon cylon-spark $ npm install cylon cylon-leapmotion $ npm install cylon-i2c $ npm install cylon-gpio
- Connect your Leap to to the computer.
- Open Sublime Text and create a Javascript file in the same parent directory that contains your node_modules folder.
- Insert said code and your Particle credentials.
var Cylon = require('cylon'); // Initialize the robot Cylon.robot({ //create a connection for each piece of hardware connections: { //replace accessToken and deviceId with your own credentials voodoospark: { adaptor: 'voodoospark', accessToken: 'YOUR_ACCESS_TOKEN', deviceId: 'YOUR_DEVICE_ID', module: 'cylon-spark' }, leapmotion: { adaptor: 'leapmotion' } }, //load the drivers for the objects associated with our components devices: { //link the motors and laser to the photon board laser: { driver: 'led', pin: 'D2', connection: 'voodoospark' }, servoX: { driver: 'servo', pin: 'D1', connection: 'voodoospark' }, servoY: { driver: 'servo', pin: 'D0', connection: 'voodoospark' }, leapmotion: { driver: 'leapmotion', connection: 'leapmotion'} }, //work is automatically initiated in the Cylon structure work: function(my) { //toggle the laser on and off every((.2).second(), function() {my.laser.turnOn()}); //create variables for hand tilt var roll, pitch; //leap processes a large JSON file every frame my.leapmotion.on('frame', function(frame) { //extract the hand from the frame info frame.hands.forEach(function(hand, index) { //select the roll and pitch of the hand while mapping and constraining the data with the range of the servo motor roll = Math.min(Math.max(((hand.roll()*-1)+2.5)*30,10),170); pitch = Math.min(Math.max((hand.pitch()+1)*60,10),170); //change the angle of each servo based on either the roll or pitch my.servoX.angle(roll); my.servoY.angle(pitch); }); }); } }).start();
- In your terminal cd to the parent directory containing your js file and node modules then run the command
$ node YOUR_FILE_NAME.js
- The program should connect to the devices then run. If there is any error, double check that your devices are connected and that the voodoo firmware was uploaded to the correct Photon. You can exit the program in the terminal by pressing control + c.
Drone Test>
I originally intended to mount a photon on top of a Parrot Transporter Drone that would act as the laser controller. Unfortunately there were multiple problems that I would further iterate upon moving forwards. The stack of electronics I attempted to mount on the drone weighed 12.9 grams while the toy action figure provided with the drone weighed 4.6 grams. This additional weight made the drone difficult to control and decreased the time airborne. Future iterations would use a nicer drone that can take a larger load including a camera running openCV to automatically position the laser in the direction of the light saber user.
Hardware
- Solder two male headers to the VIN and Ground in the top left corner of a headerless Photon
- (Optional) cut off the jst on the lipo and replace with two female jumper cable heads.
- Hot glue together the almost headerless photon, small lipo battery and the 3.3v laser pointer. Glue the stack onto a flat Lego piece and attach the Lego to the drone.
Software
In the Build IDE create a laser listener which subscribes to a fireLaser event.
//laser power control pin
int laser = D6;
void setup() {
pinMode(laser,OUTPUT); // Our laser pin is output (lighting up the laser)
// Here we are going to subscribe to a buttonFire event that calls myHandler when buttonFire is published
Particle.subscribe("buttonFire", myHandler);
}
void loop() {
}
// Fire the laser for half a second
void myHandler(const char *event,const char *data)
{
digitalWrite(laser,HIGH);
delay(500);
digitalWrite(laser,LOW);
}
At IFTTT create a recipe using any trigger/channel you like for the 'if' and publish a buttonFire event using the Particle Channel for the 'that'. Ensure that the name of your event published is the same name as the one subscribed to in the Build IDE.
Web Visualization>
When contact is made the event containing the imu data is published through the cloud. Using the spark.js library we create an event listener that unpackages the string data tied to the event instance. We then use a P5 sketch to visualize said data like so..
Software
*This code is mixture of two examples found online. Harrison Jones from Particle posted js examples using local storage and temporary access tokens here . Also, Martin Schneider posted this OpenProcessing doodle which I've converted to a P5 sketch and connected to the event data. If you have a Particle account you can sing in and test if your system is working using the code pen application below.
See the Pen Force Fielder by Paul Elsberg (@paulelsberg) on CodePen.
If you don't have a Particle account here's a sample of what the final output might look like.
Comments