added arduino, modified build
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// Fidget spinner logic.
|
||||
// This class represents a fidget spinner that can be spun to a specified
|
||||
// velocity and then over time will slow down and stop. The spinner keeps track
|
||||
// of its continuous (floating point) position value within the range 0...<10
|
||||
// so it can map to CircuitPlayground pixel positions easily.// Author: Tony DiCola
|
||||
// License: MIT License (https://opensource.org/licenses/MIT)
|
||||
// Usage:
|
||||
// - Create an instance of the class (specifying a custom decay value between 0 and 1,
|
||||
// the lower the decay the faster the spinner slows down).
|
||||
// - Call spin to start the spinner moving at the specified velocity (in pixels/second).
|
||||
// - Call getPosition periodically to get the current spinner position given an elapsed
|
||||
// amount of seconds since the last getPosition call.
|
||||
#ifndef FIDGETANIMATION_H
|
||||
#define FIDGETANIMATION_H
|
||||
|
||||
class FidgetSpinner {
|
||||
public:
|
||||
FidgetSpinner(float decay=0.5):
|
||||
_position(0.0), _velocity(0.0), _elapsedS(0.0), _decay(decay)
|
||||
{}
|
||||
|
||||
void spin(float velocity) {
|
||||
// Start the fidget spinner moving at the specified velocity (in pixels/second).
|
||||
// Over time the spinner will slow down based on its decay value.
|
||||
_velocity = velocity;
|
||||
_elapsedS = 0.0; // Reset elapsed time to start the decay from the beginning.
|
||||
}
|
||||
|
||||
float getPosition(float deltaS) {
|
||||
// Get the fidget spinner's current position after moving for the specified number
|
||||
// of milliseconds. Returns a continuous value in the range 0...<10.
|
||||
_elapsedS += deltaS;
|
||||
// Compute current velocity based on exponential decay of initial
|
||||
// velocity over the elapsed time (this causes the spinner to
|
||||
// gradually slow down and mimics the effects of friction).
|
||||
float currentVelocity = _velocity*pow(_decay, _elapsedS);
|
||||
// Now move the position based on the current velocity.
|
||||
_position += currentVelocity*deltaS;
|
||||
// Finally make sure position is constrained within 0...<10 range.
|
||||
_position = fmod(_position, 10.0);
|
||||
if (_position < 0.0) {
|
||||
// Ensure position is never negative by wrapping back to the
|
||||
// same pixel position in a positive location.
|
||||
_position += 10.0;
|
||||
}
|
||||
return _position;
|
||||
}
|
||||
|
||||
private:
|
||||
float _position;
|
||||
float _velocity;
|
||||
float _elapsedS;
|
||||
float _decay;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,269 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Signal peak detector using smoothed z-score algorithm.
|
||||
// Detects when a continuous signal has a significant peak in values. Based on
|
||||
// algorithm from the answer here:
|
||||
// https://stackoverflow.com/questions/22583391/peak-signal-detection-in-realtime-timeseries-data/22640362#22640362
|
||||
// Author: Tony DiCola
|
||||
// License: MIT License (https://opensource.org/licenses/MIT)
|
||||
// Usage:
|
||||
// - Create an instance of the PeakDetector class and configure its lag, threshold,
|
||||
// and influence. These likely need to be adjust to fit your dataset. See the
|
||||
// Stack Overflow question above for more details on their meaning.
|
||||
// - Continually call detect and feed it a new sample value. Detect will return 0
|
||||
// if no peak was detected, 1 if a positive peak was detected and -1 if a negative
|
||||
// peak was detected.
|
||||
#ifndef PEAKDETECTOR_H
|
||||
#define PEAKDETECTOR_H
|
||||
|
||||
class PeakDetector {
|
||||
public:
|
||||
PeakDetector(const int lag=5, const float threshold=3.5, const float influence=0.5):
|
||||
_lag(lag), _threshold(threshold), _influence(influence), _avg(0.0), _std(0.0), _primed(false), _index(0)
|
||||
{
|
||||
// Allocate memory for last samples (used during averaging) and set them all to zero.
|
||||
_filtered = new float[lag];
|
||||
for (int i=0; i<lag; ++i) {
|
||||
_filtered[i] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
~PeakDetector() {
|
||||
// Deallocate memory for samples.
|
||||
if (_filtered != NULL) {
|
||||
delete[] _filtered;
|
||||
}
|
||||
}
|
||||
|
||||
int detect(float sample) {
|
||||
// Detect if the provided sample is a positive or negative peak.
|
||||
// Will return 0 if no peak detected, 1 if a positive peak and -1
|
||||
// if a negative peak.
|
||||
int result = 0;
|
||||
// Fill up filtered samples if not yet primed with enough available samples.
|
||||
if (_primed && (abs(sample-_avg) > (_threshold*_std))) {
|
||||
// Detected a peak!
|
||||
// Determine type of peak, positive or negative.
|
||||
if (sample > _avg) {
|
||||
result = 1;
|
||||
}
|
||||
else {
|
||||
result = -1;
|
||||
}
|
||||
// Save this sample but scaled down based on influence.
|
||||
_filtered[_index] = (_influence*sample) + ((1.0-_influence)*_filtered[_previousIndex()]);
|
||||
}
|
||||
else {
|
||||
// Did not detect a peak, or not yet primed with enough samples.
|
||||
// Just record this sample and move on.
|
||||
_filtered[_index] = sample;
|
||||
}
|
||||
// Increment index of next filtered sample.
|
||||
_incrementIndex();
|
||||
// When primed update the average and standard deviation of the most recent
|
||||
// filtered values.
|
||||
if (_primed) {
|
||||
// Compute mean of filtered values.
|
||||
_avg = 0.0;
|
||||
for (int i=0; i<_lag; ++i) {
|
||||
_avg += _filtered[i];
|
||||
}
|
||||
_avg = _avg/float(_lag);
|
||||
// Compute standard deviation of filtered values.
|
||||
_std = 0.0;
|
||||
for (int i=0; i<_lag; ++i) {
|
||||
_std += pow(_filtered[i]-_avg, 2.0);
|
||||
}
|
||||
_std = sqrt(_std/float(_lag));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
float getAvg() {
|
||||
// Return the current signal average, useful for debugging.
|
||||
return _avg;
|
||||
}
|
||||
|
||||
float getStd() {
|
||||
// Return the current signal standard deviation, useful for debugging.
|
||||
return _std;
|
||||
}
|
||||
|
||||
private:
|
||||
float _lag;
|
||||
float _threshold;
|
||||
float _influence;
|
||||
float* _filtered;
|
||||
float _avg;
|
||||
float _std;
|
||||
bool _primed;
|
||||
int _index;
|
||||
|
||||
void _incrementIndex() {
|
||||
// Increment the index of the current sample.
|
||||
_index += 1;
|
||||
if (_index >= _lag) {
|
||||
// Loop back to start of sample buffer when full, but be sure to note
|
||||
// when this happens to indicate we are primed with enough samples.
|
||||
_index = 0;
|
||||
_primed = true;
|
||||
}
|
||||
}
|
||||
|
||||
int _previousIndex() {
|
||||
// Find the index of the last sample.
|
||||
int result = _index-1;
|
||||
if (result < 0) {
|
||||
result = _lag-1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user