Laser sword on an Arduino Uno
A light saber style sword on an Uno: a WS2812 blade that ignites and retracts, swing and clash from an MPU6050, a DFPlayer sound font and a charge gauge. The whole build, an Arduino Uno and 16 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced Runs in your browser. Free, and no account needed.
Build it as the Laser Sword Kit: the parts in numbered bags and a booklet that checks every step. Retails for about $50, free on Mokxi.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Laser sword: a light saber style blade you can swing.
//
// pin 2 the activation button, other side to GND (internal pull-up)
// pin 3 the DFPlayer's BUSY line, LOW while a sound plays
// pin 6 the blade: 24 WS2812 LEDs, through 330 ohm on the data line
// pin 10 DFPlayer TX pin 11 -> 1 k -> DFPlayer RX
// A4, A5 the MPU6050 (SDA, SCL)
// A0 the battery pack, through a 10 k / 10 k divider
// 7, 8, 9 the charge gauge: green, yellow, red
//
// Click the button to ignite the blade, click again to retract it. Hold it
// for a second while the blade is lit to pick the next color; the sword
// remembers it in EEPROM. Drag the MPU6050 to swing, flick it hard to clash.
//
// # The sound font
//
// The microSD card holds five files, in this order, the way most hobby
// saber cards are laid out:
//
// 0001 ignite 0002 hum 0003 swing 0004 clash 0005 retract
//
// The hum loops whenever nothing else is playing. A swing or a clash plays
// over it, and when BUSY goes high again the hum comes back.
//
// # Swing and clash
//
// A gyroscope measures how fast the hilt turns, so a swing is simply a turn
// rate above SWING_DPS. A clash is the blade stopping dead against something,
// which an accelerometer sees as a sudden jump: the change between two
// readings, summed over the three axes, above CLASH_JUMP.
#include <Wire.h>
#include <EEPROM.h>
#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>
#include <Adafruit_NeoPixel.h>
const int BUTTON = 2;
const int BUSY = 3;
const int BLADE_PIN = 6;
const int GAUGE_GREEN = 7;
const int GAUGE_YELLOW = 8;
const int GAUGE_RED = 9;
const int BATTERY = A0;
const int LEDS = 24;
const int MPU = 0x68;
// Turn rate for a swing, in degrees a second, and the accelerometer jump
// for a clash, in counts at +/-2 g (16384 counts is 1 g, so this is
// about three quarters of a g, arriving between two readings).
const long SWING_DPS = 150;
const long CLASH_JUMP = 12000;
// The pack is two Li-ion cells: 6.0 V empty, 8.4 V full. Halved by the
// divider that is 3.0 V to 4.2 V, which the ADC reads as about 614 to 860.
const int ADC_EMPTY = 614;
const int ADC_FULL = 860;
enum { SND_IGNITE = 1, SND_HUM, SND_SWING, SND_CLASH, SND_RETRACT };
const uint8_t COLORS[][3] = {
{0, 90, 255}, // blue
{0, 255, 40}, // green
{255, 0, 0}, // red
{170, 0, 255}, // purple
{255, 120, 0}, // amber
{255, 255, 255} // white
};
const char *const NAMES[] = {"blue", "green", "red", "purple", "amber", "white"};
const int COLOR_COUNT = 6;
Adafruit_NeoPixel blade(LEDS, BLADE_PIN, NEO_GRB + NEO_KHZ800);
SoftwareSerial link(10, 11);
DFRobotDFPlayerMini player;
bool lit = false;
int color = 0;
int battery = 100;
bool playerOk = false;
// The button: when it went down, and whether this press already did something.
bool wasDown = false;
unsigned long downAt = 0;
bool held = false;
int16_t lastA[3];
unsigned long readAt = 0;
unsigned long effectUntil = 0;
bool clashing = false;
unsigned long batteryAt = 0;
unsigned long humAt = 0;
unsigned long frameAt = 0;
long mag(long v) { return v < 0 ? -v : v; }
void mpuWrite(uint8_t reg, uint8_t value) {
Wire.beginTransmission(MPU);
Wire.write(reg);
Wire.write(value);
Wire.endTransmission();
}
// Accelerometer and gyroscope in one burst: fourteen bytes from 0x3B,
// high byte first, with the temperature in the middle.
bool mpuRead(int16_t a[3], int16_t g[3]) {
Wire.beginTransmission(MPU);
Wire.write(0x3B);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom(MPU, 14) != 14) return false;
for (int i = 0; i < 3; i++) a[i] = (Wire.read() << 8) | Wire.read();
Wire.read();
Wire.read();
for (int i = 0; i < 3; i++) g[i] = (Wire.read() << 8) | Wire.read();
return true;
}
void play(int file) {
if (playerOk) player.play(file);
}
// Paint the blade in the current color at `level` of full brightness, up to
// `length` LEDs from the hilt.
void paint(int length, int level) {
const uint8_t *c = COLORS[color];
for (int i = 0; i < LEDS; i++) {
if (i < length) {
blade.setPixelColor(i, c[0] * level / 255, c[1] * level / 255, c[2] * level / 255);
} else {
blade.setPixelColor(i, 0);
}
}
blade.show();
}
void ignite() {
if (battery < 5) {
Serial.println("battery too low");
return;
}
Serial.print("ignite ");
Serial.println(NAMES[color]);
play(SND_IGNITE);
for (int n = 1; n <= LEDS; n++) {
paint(n, 255);
delay(12);
}
lit = true;
effectUntil = millis() + 300;
}
void retract() {
Serial.println("retract");
play(SND_RETRACT);
for (int n = LEDS - 1; n >= 0; n--) {
paint(n, 255);
delay(16);
}
lit = false;
}
void nextColor() {
color = (color + 1) % COLOR_COUNT;
EEPROM.update(0, color);
Serial.print("color ");
Serial.println(NAMES[color]);
// Sweep the new color up the blade, one LED at a time.
const uint8_t *c = COLORS[color];
for (int i = 0; i < LEDS; i++) {
blade.setPixelColor(i, c[0], c[1], c[2]);
blade.show();
delay(8);
}
}
void readBattery() {
long sum = 0;
for (int i = 0; i < 4; i++) sum += analogRead(BATTERY);
int level = map(sum / 4, ADC_EMPTY, ADC_FULL, 0, 100);
battery = constrain(level, 0, 100);
// The gauge: green above 60 percent, yellow above 25, red below that,
// and a blinking red under 15 percent, which is when to charge it.
bool blink = (millis() / 250) % 2;
digitalWrite(GAUGE_GREEN, battery > 60);
digitalWrite(GAUGE_YELLOW, battery > 25 && battery <= 60);
digitalWrite(GAUGE_RED, battery <= 25 && (battery >= 15 || blink));
}
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
pinMode(BUSY, INPUT);
pinMode(GAUGE_GREEN, OUTPUT);
pinMode(GAUGE_YELLOW, OUTPUT);
pinMode(GAUGE_RED, OUTPUT);
Serial.begin(115200);
blade.begin();
blade.show();
Wire.begin();
mpuWrite(0x6B, 0x00); // wake it: it powers up asleep
mpuWrite(0x1B, 0x10); // gyroscope at +/-1000 degrees a second
mpuWrite(0x1C, 0x00); // accelerometer at +/-2 g
link.begin(9600);
// Acknowledgments off: a sword cannot stop and wait for the sound board
// to say "got it" in the middle of a swing.
playerOk = player.begin(link, false);
if (playerOk) player.volume(24);
uint8_t saved = EEPROM.read(0);
if (saved < COLOR_COUNT) color = saved;
int16_t g[3];
mpuRead(lastA, g);
readBattery();
Serial.print("Laser sword ready. Battery ");
Serial.print(battery);
Serial.println("%");
if (!playerOk) Serial.println("no sound card");
}
void loop() {
unsigned long now = millis();
// The button: a click toggles the blade, a one second hold changes color.
bool down = digitalRead(BUTTON) == LOW;
if (down && !wasDown) {
downAt = now;
held = false;
}
if (down && !held && lit && now - downAt > 1000) {
held = true;
nextColor();
}
if (!down && wasDown && !held && now - downAt > 30) {
if (lit) {
retract();
} else {
ignite();
}
}
wasDown = down;
if (now - batteryAt > 200) {
batteryAt = now;
readBattery();
}
if (!lit) return;
// Read the motion every 20 ms.
if (now - readAt >= 20) {
readAt = now;
int16_t a[3], g[3];
if (mpuRead(a, g)) {
long jump = 0;
for (int i = 0; i < 3; i++) jump += mag((long)a[i] - lastA[i]);
long turn = mag(g[0]) + mag(g[1]) + mag(g[2]);
for (int i = 0; i < 3; i++) lastA[i] = a[i];
// 32.8 counts a degree a second at +/-1000.
long dps = turn * 10 / 328;
if (jump > CLASH_JUMP && now > effectUntil) {
Serial.println("clash");
play(SND_CLASH);
clashing = true;
effectUntil = now + 300;
} else if (dps > SWING_DPS && now > effectUntil) {
Serial.println("swing");
play(SND_SWING);
clashing = false;
effectUntil = now + 400;
}
}
}
// Back to the hum whenever nothing else is playing. BUSY takes a moment
// to fall after a command, so ask again at most every quarter second.
if (playerOk && now > effectUntil && now - humAt > 250 && digitalRead(BUSY) == HIGH) {
humAt = now;
player.loop(SND_HUM);
}
// The blade, fifty frames a second: a white flash for a clash, full
// brightness for a swing, and otherwise a slow shimmer, the way a real one
// never looks quite still.
if (now - frameAt < 20) return;
frameAt = now;
if (clashing && now + 200 < effectUntil) {
blade.fill(blade.Color(255, 255, 255));
blade.show();
} else if (now < effectUntil) {
paint(LEDS, 255);
} else {
clashing = false;
paint(LEDS, 200 + random(40));
}
}
Parts list
18 parts, plus the jumper wires. They retail for about $44; on Mokxi they're free.
- 1 × Arduino Uno R3
- 1 × Full-size breadboard
- 1 × Addressable RGB LEDs
- 1 × Accelerometer and gyroscope, MPU6050
- 3 × Resistor, 220 Ω
- 1 × LED, green
- 1 × LED, yellow
- 1 × LED, red
- 1 × Pushbutton
- 1 × DFPlayer Mini MP3 module
- 1 × Resistor, 1k Ω
- 1 × Speaker, 8 ohm
- 1 × Resistor, 330 Ω
- 1 × Battery: AA, coin cell, LiPo
- 2 × Resistor, 10k Ω
How it is wired
22 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Arduino Uno R3 pin SCL; Accelerometer and gyroscope, MPU6050 pin SCL
- Arduino Uno R3 pin SDA; Accelerometer and gyroscope, MPU6050 pin SDA
- Ground: Arduino Uno R3 pin GND; Accelerometer and gyroscope, MPU6050 pin GND
- Arduino Uno R3 pin 11; Resistor, 1k Ω pin 1
- Arduino Uno R3 pin 10; DFPlayer Mini MP3 module pin TX
- Arduino Uno R3 pin 9; Resistor, 220 Ω (3) pin 1
- Arduino Uno R3 pin 8; Resistor, 220 Ω (2) pin 1
- Arduino Uno R3 pin 7; Resistor, 220 Ω (1) pin 1
- Arduino Uno R3 pin 6; Resistor, 330 Ω pin 1
- Arduino Uno R3 pin 3; DFPlayer Mini MP3 module pin BUSY
- Arduino Uno R3 pin 2; Pushbutton pin 1a
- Arduino Uno R3 pin 5V; Addressable RGB LEDs pin VDD; Accelerometer and gyroscope, MPU6050 pin VCC; DFPlayer Mini MP3 module pin VCC
- Ground: Arduino Uno R3 pin GND; Addressable RGB LEDs pin VSS; LED, green pin C; LED, yellow pin C; LED, red pin C; Pushbutton pin 2a; DFPlayer Mini MP3 module pin GND; Battery: AA, coin cell, LiPo pin V-; Resistor, 10k Ω (2) pin 2
- Arduino Uno R3 pin A0; Resistor, 10k Ω (1) pin 2; Resistor, 10k Ω (2) pin 1
- Addressable RGB LEDs pin DIN; Resistor, 330 Ω pin 2
- Resistor, 220 Ω (1) pin 2; LED, green pin A
- Resistor, 220 Ω (2) pin 2; LED, yellow pin A
- Resistor, 220 Ω (3) pin 2; LED, red pin A
- DFPlayer Mini MP3 module pin RX; Resistor, 1k Ω pin 2
- DFPlayer Mini MP3 module pin SPK_1; Speaker, 8 ohm pin 1
- DFPlayer Mini MP3 module pin SPK_2; Speaker, 8 ohm pin 2
- Battery: AA, coin cell, LiPo pin V+; Resistor, 10k Ω (1) pin 1
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.