diff --git a/ProtonPack.ino b/ProtonPack.ino index ac58be5..e4e791f 100644 --- a/ProtonPack.ino +++ b/ProtonPack.ino @@ -1,142 +1,189 @@ +// Proton Pack with NeoPixels +// Boy howdy do these make everything easy + #include #include -#include "Adafruit_LEDBackpack.h" -#include "Adafruit_GFX.h" +#include +#include +#include +#include +#include -#define LTCH 8 -#define RED 9 -#define GREEN 10 -#define BLUE 11 -#define DEBUG 13 -#define TRIGGER 4 +#define DEBUG 12 + +// Music Player object +#define SHIELD_RESET -1 // VS1053 reset pin (unused!) +#define SHIELD_CS 7 // VS1053 chip select pin (output) +#define SHIELD_DCS 6 // VS1053 Data/command select pin (output) +#define CARDCS 4 // Card chip select +#define DREQ 1 // VS1053 Data request (an interrupt pin) +Adafruit_VS1053_FilePlayer musicPlayer = Adafruit_VS1053_FilePlayer(SHIELD_RESET, SHIELD_CS, SHIELD_DCS, DREQ, CARDCS); + +// NeoPixel: so cool +#define SYNCHROTRON_PIN 5 +#define SYNCHROTRON_PIXELS 24 // I'm using the middle-sized NeoPixel ring +Adafruit_NeoPixel synchrotron = Adafruit_NeoPixel(SYNCHROTRON_PIXELS, SYNCHROTRON_PIN, NEO_GRB | NEO_KHZ800); + +// 7-segment displays +Adafruit_7segment disp1 = Adafruit_7segment(); + +// Inputs +#define TRIGGER 8 + +// Nominal brightness +#define brightness 64 -const byte powerColor[3] = {0xff, 0, 0}; const byte dispBright = 10; unsigned long jiffies = 0; -Adafruit_7segment disp1; void rgbPWM(byte r, byte g, byte b) { - analogWrite(RED, 0xff - r); - analogWrite(GREEN, 0xff - g); - analogWrite(BLUE, 0xff - b); + // XXX: do this } void rgb(byte r, byte g, byte b) { - SPI.transfer(b); - SPI.transfer(g); - SPI.transfer(r); - digitalWrite(LTCH, HIGH); - digitalWrite(LTCH, LOW); + for (int i = 0; i < SYNCHROTRON_PIXELS; i += 1) { + synchrotron.setPixelColor(i, synchrotron.Color(r, g, b)); + } + synchrotron.show(); } void setup() { randomSeed(analogRead(12)); - - SPI.begin(); - SPI.setDataMode(SPI_MODE0); - SPI.setClockDivider(SPI_CLOCK_DIV2); - SPI.setBitOrder(LSBFIRST); - disp1 = Adafruit_7segment(); - disp1.begin(0x70); - - pinMode(LTCH, OUTPUT); - pinMode(RED, OUTPUT); - pinMode(GREEN, OUTPUT); - pinMode(BLUE, OUTPUT); - pinMode(DEBUG, OUTPUT); + // synchrotron + synchrotron.begin(); + synchrotron.show(); // Turn everything off + + // inputs pinMode(TRIGGER, INPUT_PULLUP); + + // music player, this sets up SPI for us + SD.begin(CARDCS); + musicPlayer.begin(); + musicPlayer.setVolume(20, 20); // lower = louder + // We don't set useInterrupt, since we do our own polling for smoother operations + + // 7-segment displays. + // These also use SPI, in i2c mode. + // Since the music player has a CS line, + // and we're unlikely to send the right i2c command to the 7-segment to wake it up, + // it's okay to use the same SPI bus for both. + disp1.begin(0x70); } -// Cycle through colors, one spoke at a time. -// Since we can only control brightness by color component for all spokes, -// we can't do a fancier trick per-spoke. -// But this one isn't that bad, really. -bool doStartup() { - static int count = 0; - static byte cur[3] = {0, 0, 0}; - - // Run this every 12 jiffies - if (jiffies % 6 != 0) { - return false; +// Synchrotron needs to "spin up" +// We start slow, with red, then work our way through the rainbow to blue +bool charge() { + static uint32_t count = 0; + static int every = 9; + static int reps = 0; + uint32_t color_count; + byte r, g, b; + static int whichout = 0; + + // Play startup sound at the start + if (count == 0) { + musicPlayer.startPlayingFile("track001.mp3"); } - - int weight = 0; - int pos = count % 8; - int color = 6 - (count / 8); + + // Make the animation play out a little more slowly, + // while still allowing a nice fast rotation + color_count = count / 4; + + // Give the illusion of something spinning up + if (every == 1) { + whichout = (whichout + 1) % SYNCHROTRON_PIXELS; + } else if (count % every == 0) { + whichout = (whichout + 1) % SYNCHROTRON_PIXELS; + reps += 1; + if (reps == 20 - every) { + every -= 1; + reps = 0; + } + } + + // Start at blue, go through hue to red + switch (color_count / brightness) { + case 0: + r = color_count % brightness; + g = 0; + b = 0; + break; + case 1: + r = brightness - (color_count % brightness) - 1; + g = color_count % brightness; + b = 0; + break; + case 2: + r = 0; + g = brightness - (color_count % brightness) - 1; + b = color_count % brightness; + break; + default: + rgb(brightness, 0, 0); + return true; + } + + // Set 'em up pixels + for (int i = 0; i < SYNCHROTRON_PIXELS; i += 1) { + if (whichout == i) { + synchrotron.setPixelColor(i, 0); + } else if ((whichout == (i+1) % SYNCHROTRON_PIXELS) || ((whichout+1) % SYNCHROTRON_PIXELS == i)) { + synchrotron.setPixelColor(i, synchrotron.Color(r/4, g/4, b/4)); + } else { + synchrotron.setPixelColor(i, synchrotron.Color(r, g, b)); + } + } + synchrotron.show(); + + disp1.clear(); + disp1.printNumber(0xb00, HEX); + disp1.setBrightness(dispBright); + disp1.writeDisplay(); count += 1; - for (int i = 0; i < 3; i += 1) { - int bit = (color & (1 << i))?1:0; - weight += bit; - // Shift the current color in from the LSB to the MSB - cur[i] = (cur[i] << 1) | bit; - } - rgb(cur[0], cur[1], cur[2]); - rgbPWM(32 * weight, 32 * weight, 32 * weight); - - for (int i = 0; i < 5; i += 1) { - disp1.writeDigitRaw(i, random(256)); - } - disp1.setBrightness(random(16)); - disp1.writeDisplay(); - - if ((color == 1) && (pos == 7)) { - rgb(powerColor[0], powerColor[1], powerColor[2]); - disp1.clear(); - disp1.printNumber(0xb00, HEX); - disp1.setBrightness(dispBright); - disp1.writeDisplay(); - return true; - } - return false; } -// Pulse to an extreme, then back -bool pulse(byte initial, int pct) { - static int prev = 0; - static int state = 0; - static int val = 0; - int cur = (pct << 8) | initial; - int newval = initial; - - // Reset if called with new values - if (prev != cur) { - state = 0; - prev = cur; +// Do a sort of mirrored KITT effect +bool kitt() { + static int count = 0; + int out = count % (SYNCHROTRON_PIXELS/2); + + if (jiffies % 12 != 0) { + return false; } - switch (state) { - case 0: - state = 1; - val = initial; - break; - case 1: - val = (val * pct) / 100; - if ((val <= 1) || (val >= 255)) { - state = 2; + for (int i = 0; i < SYNCHROTRON_PIXELS; i += 1) { + int pixnum = (SYNCHROTRON_PIXELS/2) - abs(i - (SYNCHROTRON_PIXELS / 2)); + int intensity; + + if (count < SYNCHROTRON_PIXELS/2) { + intensity = 100; + if (pixnum == out) { + intensity = 50; + } else if (pixnum < out) { + intensity = 10; + } + } else { + intensity = 10; + if (pixnum == out) { + intensity = 50; + } else if (pixnum < out) { + intensity = 100; + } } - break; - case 2: - // discrete exponentiation, woo woo - while ((newval * pct) / 100 != val) { - newval = (newval * pct) / 100; - } - val = newval; - if (val == initial) { - state = 3; - } - break; - case 3: - state = 0; - val = 0; + synchrotron.setPixelColor(i, synchrotron.Color(brightness * intensity / 100, 0, 0)); + } + synchrotron.show(); + + count += 1; + if (count > SYNCHROTRON_PIXELS) { + rgb(brightness, 0, 0); + count = 0; return true; } - - newval = min(val, 255); - rgbPWM(newval, newval, newval); return false; } @@ -144,17 +191,16 @@ bool glitch(int r, int g, int b) { static int state = 0; int i; - if (jiffies % 5 != 0) { + if (jiffies % 10 != 0) { return false; } switch (state) { case 0: - // pick a random bit and clear it - i = random(8); - r &= ~(1 << i); - g &= ~(1 << i); - b &= ~(1 << i); + // glitch to a random color + r = random(brightness / 6); + g = random(brightness / 6); + b = random(brightness / 6); rgb(r, g, b); state = 1; break; @@ -169,22 +215,26 @@ bool glitch(int r, int g, int b) { } void fire() { - rgb(0, 0xff, 0xff); - pulse(32, 160); + rgb(0, brightness, brightness); } void fireDone() { - rgb(powerColor[0], powerColor[1], powerColor[2]); - rgbPWM(64, 64, 64); + rgb(brightness, 0, 0); } +void flashDebug() { + if (jiffies % 50 == 0) { + int val = digitalRead(DEBUG); + digitalWrite(DEBUG, (val==HIGH)?LOW:HIGH); + } +} -int doPowered() { +void tick() { static int doing = 0; static float val1 = 584.2; static bool firing = false; bool trigger; - + trigger = (digitalRead(TRIGGER) == LOW); if (trigger) { @@ -194,26 +244,24 @@ int doPowered() { switch (doing) { case 0: // doing nothing - if (jiffies % 200 == 0) { - doing = 1; // pulse + if (jiffies % 300 == 0) { + doing = 1; // KITT } else if (random(350) == 0) { doing = 2; // surge - } else if (random(200) == 0) { + } else if (random(400) == 0) { doing = 3; // glitch } break; case 1: - if (pulse(64, 80)) { + if (kitt()) { doing = 0; } break; case 2: - if (pulse(64, 120)) { - doing = 0; - } + doing = 0; break; case 3: - if (glitch(powerColor[0], powerColor[1], powerColor[2])) { + if (glitch(brightness, 0, 0)) { doing = 0; } break; @@ -248,38 +296,51 @@ int doPowered() { disp1.print(someNumber); disp1.writeDisplay(); } - - - return 1; -} - -void flashDebug() { - if (jiffies % 50 == 0) { - int val = digitalRead(DEBUG); - digitalWrite(DEBUG, (val==HIGH)?LOW:HIGH); - } + + flashDebug(); } void loop() { - static int state = 0; + // 6 seems to be about what my overly-critical brain needs to buffer out + // any music player delays so that they're unnoticeable + unsigned long new_jiffies = millis() / 6; - // state machine - // The delay is *outside* the state machine, you'll notice. - // So don't call sleep in your state function. - switch (state) { - case 0: - if (doStartup()) { - state = 1; - } - break; - case 1: - state = doPowered(); - break; + if (new_jiffies > jiffies) { + jiffies = new_jiffies; + tick(); } - - flashDebug(); - delay(12); - jiffies += 1; + /* Cleverness ensues + * + * The Adafruit library is written to let you go off and do whatever you need, + * hooking into an interrupt to sort of act like a multitasking operating system, + * interrupting your program periodically. + * + * That's smart, since it makes it easy to use, + * but we want this to be responsive, and can't handle something barging in and taking up lots of time: + * it makes things look really uneven as our display code pauses to fill the buffer. + * Fortunately, we don't have to fill the entire buffer at once, we can trickle data in. + * That's what this does. + * + * Since the entire program is polling, without ever calling delay, + * and hopefully doing what needs to be done quickly, + * we check to see if the music chip wants more data. + * If it does, we give it one chunk, and only one chunk, + * rather than filling its buffer back up completely. + * + * There is still some weirdness with this loop, + * possibly because the SPI routines are masking interrupts used to increment millis. + * But it's remarkably more fluid than the other way. + */ + + if (musicPlayer.playingMusic && musicPlayer.readyForData()) { + int bytesread = musicPlayer.currentTrack.read(musicPlayer.mp3buffer, VS1053_DATABUFFERLEN); + if (bytesread == 0) { + musicPlayer.playingMusic = false; + musicPlayer.currentTrack.close(); + } else { + musicPlayer.playData(musicPlayer.mp3buffer, bytesread); + } + } }