Simulating phaser fire with NeoPixels

The above is a prototype of a circuit to simulate a phaser array on a starship such as the Enterprise-D or Voyager using an addressable strip of LEDs such as the NeoPixel. Most people simulate phaser fire by wiring individual LEDs close together and triggering them with multiple ICs like the 74hc595 or decade counter, using a timing pulse from an Arduino.

But NeoPixels are directly addressable and only require a single Arduino pin and magically that pin need not be PWM. In this example, I’ve written a sketch that begins the phaser pulse from either end of the NeoPixel ring (LEDs 1 and 24) and spreads outward until the two pulses meet at a random spot.

There’s a variable that controls how much of a trail the pulses leave behind and the sketch also flips between firing from a single spot and drifting as if the phaser is trying to maintain a lock on a target. There are bumpers at either end to prevent the pulses meeting at the end. Also, the sketch ajdusts the speed at which the pulses travel so as to meet neatly.

This example really is a prototype because I’ve never used an addressable LED strip before and because I’m not sure how to physically install a strip. I’m using a 24-pixel ring here for convenience, but in reality you’d probably want to use a strip you cut to length. It would, however, be difficult to bend a strip to match the curve of a phaser array. The LEDs are pretty close to one another. I would probably try to mount the strip on edge and then use a mirrored surface angled at 45° (like a periscope) to project the effect outward.

It’s also possible to buy single NeoPixels, wire them together and address them individually. You could even have several NeoPixel phaser arrays and address each array separately while still using only one pin on the Arduino.

The code below doesn’t have an include to a timer library (Tinkercad doesn’t support such a library), so I’m just checking milliseconds to get the timing. If I ever get a NeoPixel or similarly addressable LED strip, I’ll post a working example.

/*
Firing phaser strips on starships like Voyager and the Enterprise D and E
Jennifer Petkus
January 21, 2021
*/

#include <Adafruit_NeoPixel.h>

const int PHASER_DATA_PIN = 2;
const int NUM_PHASER_PIXELS = 24;
const int PHASER_BTN_PIN = 8;

const int PHASER_R_VAL = 255;
const int PHASER_G_VAL = 127;
const int PHASER_B_VAL = 0;
const int TRAIL_STEPS = 4;

Adafruit_NeoPixel phaserStrip = Adafruit_NeoPixel(NUM_PHASER_PIXELS, PHASER_DATA_PIN, NEO_GRB + NEO_KHZ800);

bool phaserRamp = 0; // phaser lighting up from ends of strip
bool phaserPulse = 0; // phaser beam changing brightness after ramp
bool phaserFiring = 0; // flag to prevent overlapping phasers
bool phaserDrift = 1; // flag to allow phaser to drift after ramp
int phaserDriftCount = 0; // which LED to fire next during phaserDrift
bool phaserDriftDirection = 1; // whether to drift clock- or anti-clockwise
int rampStopLED = 0; // to which LED ramps builds toward

// all timing variables are unsigned long, good for 52 days of operation
unsigned long phaserRampDuration = 250; // how long until ramp meets in middle
unsigned long phaserATime = 0; // ramp starting from first LED (anti-clockwise)
unsigned long phaserBTime = 0; // ramp starting from last LED (clockwise)
unsigned long prevPhaserATime = 0;
unsigned long prevPhaserBTime = 0;
unsigned long phaserPulseDuration = 500; // how long pulse/drift effect lasts
unsigned long phaserPulseTime = 0;
unsigned long prevPhaserPulseTime = 0;
int brightness = 192; // set to less than 255 if you want to see the phaser pulse
int currALED = 0;
int currBLED = NUM_PHASER_PIXELS;
const int phaserLEDBumper = 6;
const int phaserALEDBumper = phaserLEDBumper; // these limits ensure phaser ramp travels at least X pixels
const int phaserBLEDBumper = NUM_PHASER_PIXELS - (phaserLEDBumper + 1);
const int phaserDriftDivider = 10; // how quickly you want phaser to drift after firing


void setup() {
  pinMode(PHASER_BTN_PIN, INPUT);
  // Initialize the NeoPixel library.
  phaserStrip.setBrightness(brightness);
  phaserStrip.clear();
  phaserStrip.begin();
}

void loop() {
  if (digitalRead(PHASER_BTN_PIN) == HIGH && !phaserFiring) {
    phaserFiring = 1;
    phaserRamp = 1;
    rampStopLED = random(phaserALEDBumper,phaserBLEDBumper);
    phaserDriftCount = rampStopLED;
    phaserATime = phaserRampDuration/rampStopLED;
    phaserBTime = phaserRampDuration/(NUM_PHASER_PIXELS - rampStopLED);
  }
  if (phaserRamp) {
    phaserStrip.setBrightness(brightness);
    if (millis() - prevPhaserATime > phaserATime) {
      prevPhaserATime = millis();
      firePhaser(1);
    }
    if (millis() - prevPhaserBTime > phaserBTime) {
      prevPhaserBTime = millis();
      firePhaser(0);
    }
  }
  if (phaserPulse) {
    if (millis() - phaserPulseTime > phaserPulseDuration) {
      phaserPulse = 0;
      phaserStrip.clear();
      phaserStrip.show();
      phaserFiring = 0;
      phaserDriftCount = 0;
      phaserDrift = !phaserDrift;
      phaserDriftDirection = random(0,2);
    } else if (millis() - prevPhaserPulseTime > phaserPulseDuration/phaserDriftDivider) {
      prevPhaserPulseTime = millis();
      firePhaser(2);
    }
  }
}

// picks random values to set for RGB
void firePhaser(int which){
  if (which == 1) {
    phaserStrip.setPixelColor(currALED, PHASER_R_VAL, PHASER_G_VAL, PHASER_B_VAL);
    for (int x = 0; x < TRAIL_STEPS + 1; x++) {
      phaserStrip.setPixelColor(
        currALED - x,
        PHASER_R_VAL - (PHASER_R_VAL * x/TRAIL_STEPS),
        PHASER_G_VAL - (PHASER_G_VAL * x/TRAIL_STEPS),
        PHASER_B_VAL - (PHASER_B_VAL * x/TRAIL_STEPS)
      );
    }
    currALED++;
  } else if (which == 0) {
    phaserStrip.setPixelColor(currBLED, PHASER_R_VAL, PHASER_G_VAL, PHASER_B_VAL);
    for (int x = 0; x < TRAIL_STEPS + 1; x++) {
      phaserStrip.setPixelColor(
        currBLED + x,
        PHASER_R_VAL - (PHASER_R_VAL * x/TRAIL_STEPS),
        PHASER_G_VAL - (PHASER_G_VAL * x/TRAIL_STEPS),
        PHASER_B_VAL - (PHASER_B_VAL * x/TRAIL_STEPS)
      );
    }
    currBLED--;    
  } else if (which == 2) {
    phaserStrip.setBrightness(random(brightness,255));
    if (phaserDrift) {
      phaserStrip.setPixelColor(phaserDriftCount, 0, 0, 0, 0);
      if (phaserDriftDirection) {
        phaserDriftCount++;
      } else {
        phaserDriftCount--;
      }
      phaserStrip.setPixelColor(phaserDriftCount, PHASER_R_VAL, PHASER_G_VAL, PHASER_B_VAL);
    }
  }
  if (currALED > rampStopLED + 1) {
    phaserRamp = 0;
    phaserPulse = 1;
    phaserPulseTime = millis();
    currALED = 0;
    currBLED = NUM_PHASER_PIXELS;
    phaserStrip.clear();
    phaserStrip.setBrightness(255);
    phaserStrip.setPixelColor(rampStopLED, PHASER_R_VAL, PHASER_G_VAL, PHASER_B_VAL);
  }
  phaserStrip.show();
}

And just a reminder that I’m not an electrical engineer. I’m the equivalent of a monkey accidentally writing Shakespeare. And one last note about the sketch above. The drift happens too quickly and probably should have a trail as well. Again, I’ll fix that if I ever get a NeoPixel strip.

Leave a Reply

%d bloggers like this: