You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
11 KiB
270 lines
11 KiB
// Circuit Playground Digital Fidget Spinner
|
|
// -----------------------------------------
|
|
// Uses the Circuit Playground accelerometer to detect when the board
|
|
// is flicked from one side or the other and animates the pixels spinning
|
|
// around accordingly (with decay in speed to simulate friction).
|
|
// Press one button to cycle through different color combinations,
|
|
// and another button to cycle through 4 different types of animations
|
|
// (single dot, dual dot, single sine wave, dual sine wave).
|
|
// See the FidgetSpinner.h and PeakDetector.h files as tabs at the top
|
|
// for more of the sketch code!
|
|
// Author: Tony DiCola
|
|
// License: MIT License (https://opensource.org/licenses/MIT)
|
|
#include <Adafruit_CircuitPlayground.h>
|
|
#include "FidgetSpinner.h"
|
|
#include "PeakDetector.h"
|
|
|
|
|
|
// Configure which accelerometer axis to check:
|
|
// This should be one of the following values (uncomment only ONE):
|
|
//#define AXIS CircuitPlayground.motionX() // X axis, good for Circuit Playground express (SAMD21).
|
|
#define AXIS CircuitPlayground.motionY() // Y axis, good for Circuit Playground classic (32u4).
|
|
//#define AXIS CircuitPlayground.motionZ() // Z axis, wacky--try it!
|
|
|
|
// Configure how the axis direction compares to pixel movement direction:
|
|
#define INVERT_AXIS true // Set to true or 1 to invert the axis direction vs.
|
|
// pixel movement direction, or false/0 to disable.
|
|
// If the pixels spin in the opposite direction of your
|
|
// flick all the time then try changing this value.
|
|
|
|
// Configure pixel brightness.
|
|
// Set this to a value from 0 to 255 (minimum to maximum brightness).
|
|
#define BRIGHTNESS 255
|
|
|
|
// Configure peak detector behavior:
|
|
#define LAG 30 // Lag, how large is the buffer of filtered samples.
|
|
// Must be an integer value!
|
|
#define THRESHOLD 20.0 // Number of standard deviations above average a
|
|
// value needs to be to be considered a peak.
|
|
#define INFLUENCE 0.1 // Scale down peak values to this percent influence
|
|
// when storing them back in the filtered values.
|
|
// Should be a value from 0 to 1.0 where smaller
|
|
// values mean peaks have less influence.
|
|
|
|
// Configure spinner decay, i.e. how much it slows down. This should
|
|
// be a value from 0 to 1 where smaller values cause the spinner to
|
|
// slow down faster.
|
|
#define DECAY 0.66
|
|
|
|
// Debounce times for peak detection and button presses.
|
|
// You probably don't need to change these.
|
|
#define PEAK_DEBOUNCE_MS 500
|
|
#define BUTTON_DEBOUNCE_MS 20
|
|
|
|
// Define combinations of colors. Each combo has a primary and secondary color
|
|
// that are defined as 24-bit RGB values (like HTML colors, set them using hex
|
|
// values). First define a struct to specify the types colors. Then define
|
|
// the list of color combos. If you want to change color combos (like add or
|
|
// remove them, skip down to the colors[] array below and change it.
|
|
typedef struct {
|
|
uint32_t primary;
|
|
uint32_t secondary;
|
|
} colorCombo;
|
|
// Add or remove color combos here:
|
|
colorCombo colors[] = {
|
|
// Each color combo has the following form:
|
|
// { .primary = 24-bit RGB color (use hex), .secondary = 24-bit RGB color },
|
|
// Make sure each combo ends in a comma EXCEPT for the last one!
|
|
{ .primary = 0xFF0000, .secondary = 0x000000 }, // Red to black
|
|
{ .primary = 0x00FF00, .secondary = 0x000000 }, // Green to black
|
|
{ .primary = 0x0000FF, .secondary = 0x000000 }, // Blue to black
|
|
{ .primary = 0xFF0000, .secondary = 0x00FF00 }, // Red to green
|
|
{ .primary = 0xFF0000, .secondary = 0x0000FF }, // Red to blue
|
|
{ .primary = 0x00FF00, .secondary = 0x0000FF } // Green to blue
|
|
};
|
|
|
|
// Uncomment this to output a stream of debug information
|
|
// from the accelerometer and peak detector. Use the serial
|
|
// plotter to view the graph of these 4 values:
|
|
// - Accelerometer y axis value (in m/s^2)
|
|
// - Peak detector filtered y axis average
|
|
// - Peak detector filtered y axis standard deviation
|
|
// - Peak detector result, 0=no peak 1=positive peak, -1=negative peak
|
|
#define DEBUG
|
|
|
|
|
|
// Global state used by the sketch (you don't need to change this):
|
|
PeakDetector peakDetector(LAG, THRESHOLD, INFLUENCE);
|
|
FidgetSpinner fidgetSpinner(DECAY);
|
|
uint32_t lastMS = millis();
|
|
uint32_t peakDebounce = 0;
|
|
uint32_t buttonDebounce = 0;
|
|
bool lastButton1;
|
|
bool lastButton2;
|
|
int currentAnimation = 0;
|
|
int currentColor = 0;
|
|
|
|
|
|
void setup() {
|
|
// Initialize serial output for debugging and plotting.
|
|
Serial.begin(115200);
|
|
// Initialize Circuit Playground library and set accelerometer
|
|
// to its widest +/-16G range.
|
|
CircuitPlayground.begin(BRIGHTNESS);
|
|
CircuitPlayground.setAccelRange(LIS3DH_RANGE_16_G);
|
|
// Initialize starting button state.
|
|
lastButton1 = CircuitPlayground.leftButton();
|
|
lastButton2 = CircuitPlayground.rightButton();
|
|
}
|
|
|
|
void loop() {
|
|
// Update time since last loop call.
|
|
uint32_t currentMS = millis();
|
|
uint32_t deltaMS = currentMS - lastMS; // Time in milliseconds.
|
|
float deltaS = deltaMS / 1000.0; // Time in seconds.
|
|
lastMS = currentMS;
|
|
|
|
// Grab the current accelerometer axis value and look for a sudden peak.
|
|
float accel = AXIS;
|
|
int result = peakDetector.detect(accel);
|
|
|
|
// If in debug mode, print out the current acceleration and peak detector
|
|
// state (average, standard deviation, and peak result). Use the serial
|
|
// plotter to view this over time.
|
|
#ifdef DEBUG
|
|
Serial.print(accel);
|
|
Serial.print(",");
|
|
Serial.print(peakDetector.getAvg());
|
|
Serial.print(",");
|
|
Serial.print(peakDetector.getStd());
|
|
Serial.print(",");
|
|
Serial.println(result);
|
|
#endif
|
|
|
|
// If there was a peak and enough time has elapsed since the last peak
|
|
// (i.e. to 'debounce' the noisey peak signal a bit) then start the spinner
|
|
// moving at a velocity proportional to the accelerometer value.
|
|
if ((result != 0) && (currentMS >= peakDebounce)) {
|
|
peakDebounce = currentMS + PEAK_DEBOUNCE_MS;
|
|
// Invert accel because accelerometer axis positive/negative is flipped
|
|
// with respect to pixel positive/negative movement.
|
|
if (INVERT_AXIS) {
|
|
fidgetSpinner.spin(-accel);
|
|
}
|
|
else {
|
|
fidgetSpinner.spin(accel);
|
|
}
|
|
}
|
|
|
|
// Check if enough time has passed to test for button releases.
|
|
if (currentMS >= buttonDebounce) {
|
|
buttonDebounce = currentMS + BUTTON_DEBOUNCE_MS;
|
|
// Check if any button was released. I.e. the last known button
|
|
// state was pressed (true) and the current state is released (false).
|
|
bool button1 = CircuitPlayground.leftButton();
|
|
bool button2 = CircuitPlayground.rightButton();
|
|
if (!button1 && lastButton1) {
|
|
// Button 1 released!
|
|
// Change to the next animation (looping back to start after 3 since
|
|
// there are 4 total animations).
|
|
currentAnimation = (currentAnimation + 1) % 4;
|
|
}
|
|
if (!button2 && lastButton2) {
|
|
// Button 2 released!
|
|
// Change to the next color index.
|
|
currentColor = (currentColor + 1) % (sizeof(colors)/sizeof(colorCombo));
|
|
}
|
|
lastButton1 = button1;
|
|
lastButton2 = button2;
|
|
}
|
|
|
|
// Update the spinner position and draw the current animation frame.
|
|
float pos = fidgetSpinner.getPosition(deltaS);
|
|
switch (currentAnimation) {
|
|
case 0:
|
|
// Single dot.
|
|
animateDots(pos, 1);
|
|
break;
|
|
case 1:
|
|
// Two opposite dots.
|
|
animateDots(pos, 2);
|
|
break;
|
|
case 2:
|
|
// Sine wave with one peak.
|
|
animateSine(pos, 1.0);
|
|
break;
|
|
case 3:
|
|
// Sine wave with two peaks.
|
|
animateSine(pos, 2.0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Helper functions:
|
|
void fillPixels(const uint32_t color) {
|
|
// Set all the pixels on CircuitPlayground to the specified color.
|
|
for (int i=0; i<CircuitPlayground.strip.numPixels();++i) {
|
|
CircuitPlayground.strip.setPixelColor(i, color);
|
|
}
|
|
}
|
|
|
|
float constrainPosition(const float pos) {
|
|
// Take a continuous positive or negative value and map it to its relative positon
|
|
// within the range 0...<10 (so it's valid as an index to CircuitPlayground pixel
|
|
// position).
|
|
float result = fmod(pos, CircuitPlayground.strip.numPixels());
|
|
if (result < 0.0) {
|
|
result += CircuitPlayground.strip.numPixels();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
float lerp(const float x, const float x0, const float x1, const float y0, const float y1) {
|
|
// Linear interpolation of value y within y0...y1 given x and range x0...x1.
|
|
return y0 + (x-x0)*((y1-y0)/(x1-x0));
|
|
}
|
|
|
|
uint32_t primaryColor() {
|
|
return colors[currentColor].primary;
|
|
}
|
|
|
|
uint32_t secondaryColor() {
|
|
return colors[currentColor].secondary;
|
|
}
|
|
|
|
uint32_t colorLerp(const float x, const float x0, const float x1, const uint32_t c0, const uint32_t c1) {
|
|
// Perform linear interpolation of 24-bit RGB color values.
|
|
// Will return a color within the range of c0...c1 proportional to the value x within x0...x1.
|
|
uint8_t r0 = (c0 >> 16) & 0xFF;
|
|
uint8_t g0 = (c0 >> 8) & 0xFF;
|
|
uint8_t b0 = c0 & 0xFF;
|
|
uint8_t r1 = (c1 >> 16) & 0xFF;
|
|
uint8_t g1 = (c1 >> 8) & 0xFF;
|
|
uint8_t b1 = c1 & 0xFF;
|
|
uint32_t r = int(lerp(x, x0, x1, r0, r1));
|
|
uint32_t g = int(lerp(x, x0, x1, g0, g1));
|
|
uint32_t b = int(lerp(x, x0, x1, b0, b1));
|
|
return (r << 16) | (g << 8) | b;
|
|
}
|
|
|
|
// Animation functions:
|
|
void animateDots(float pos, int count) {
|
|
// Simple discrete dot animation. Spins dots around the board based on the specified
|
|
// spinner position. Count specifies how many dots to display, each one equally spaced
|
|
// around the pixels (in practice any count that 10 isn't evenly divisible by will look odd).
|
|
// Count should be from 1 to 10 (inclusive)!
|
|
fillPixels(secondaryColor());
|
|
// Compute each dot's position and turn on the appropriate pixel.
|
|
for (int i=0; i<count; ++i) {
|
|
float dotPos = constrainPosition(pos + i*(float(CircuitPlayground.strip.numPixels())/float(count)));
|
|
CircuitPlayground.strip.setPixelColor(int(dotPos), primaryColor());
|
|
}
|
|
CircuitPlayground.strip.show();
|
|
}
|
|
|
|
void animateSine(float pos, float frequency) {
|
|
// Smooth sine wave animation. Sweeps a sine wave of primary to secondary color around
|
|
// the board pixels based on the specified spinner position.
|
|
// Compute phase based on spinner position. As the spinner position changes the phase will
|
|
// move the sine wave around the pixels.
|
|
float phase = 2.0*PI*(constrainPosition(pos)/10.0);
|
|
for (int i=0; i<CircuitPlayground.strip.numPixels();++i) {
|
|
// Use a sine wave to compute the value of each pixel based on its position for time
|
|
// (and offset by the global phase that depends on fidget spinner position).
|
|
float x = sin(2.0*PI*frequency*(i/10.0)+phase);
|
|
CircuitPlayground.strip.setPixelColor(i, colorLerp(x, -1.0, 1.0, primaryColor(), secondaryColor()));
|
|
}
|
|
CircuitPlayground.strip.show();
|
|
}
|