Last active
July 10, 2023 22:16
-
-
Save EvanBalster/4ea8642c582752d03ae654206851c580 to your computer and use it in GitHub Desktop.
Tea timer for circuit playground. RB cycles steeps & green/black, LB starts timer. When idle, lights auto-dim.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <Adafruit_CircuitPlayground.h> | |
const long FrameDT = 16; | |
unsigned long frame_index = 0; | |
unsigned long frame_mark = 0; | |
struct Timer | |
{ | |
long started; | |
long length; | |
Timer() | |
{ | |
reset(); | |
} | |
long remaining() | |
{ | |
return length - (millis()-started); | |
} | |
bool done() | |
{ | |
return started && remaining() <= 0; | |
} | |
void begin(long length) | |
{ | |
started = millis(); | |
this->length = length; | |
} | |
void reset() | |
{ | |
started = 0; | |
length = 0; | |
} | |
}; | |
Timer countdown; | |
Timer alarm; | |
long last_moved = 0; | |
enum TeaType | |
{ | |
SENCHA = 0, | |
BLACK = 1, | |
MAX_TEA_TYPE = 2 | |
}; | |
TeaType teaType = SENCHA; | |
long resteeps = 0; | |
struct Accel | |
{ | |
float x, y, z; | |
long time; | |
void readSensor() | |
{ | |
x = CircuitPlayground.motionX(); | |
y = CircuitPlayground.motionY(); | |
z = CircuitPlayground.motionZ(); | |
time = millis(); | |
} | |
}; | |
Accel gravity; | |
static uint16_t NeoPixel_Gamma_x128[256]; | |
static float NeoPixel_BrightnessMod = 1.f; | |
// the setup function runs once when you press reset or power the board | |
void setup() { | |
// initialize digital pin 13 as an output. | |
//pinMode(13, OUTPUT); | |
// Buttons | |
//pinMode(A0, OUTPUT); | |
//pinMode(4, INPUT); pinMode(5, INPUT); | |
//pinMode(PA30, OUTPUT); | |
//digitalWrite(PA30, HIGH); | |
CircuitPlayground.begin(); | |
// Calculate 16-bit gamma table... | |
for (unsigned i = 0; i < 255; ++i) | |
{ | |
NeoPixel_Gamma_x128[i] = uint16_t(pow(i/255.f,2.6f)*32640.f+0.5f); | |
} | |
CircuitPlayground.setBrightness(255); | |
teaType = MAX_TEA_TYPE; | |
resteeps = 3; | |
refreshPixels(); | |
} | |
uint32_t pcg16_state = 0x406832dd; | |
uint16_t pcg16_random(void) { | |
uint32_t oldstate = pcg16_state; | |
pcg16_state = pcg16_state * 747796405U + 1U; | |
uint16_t value = ((oldstate >> 10U) ^ oldstate) >> 12U; | |
uint32_t rot = oldstate >> 28U; | |
return (value >> rot) | (value << ((- rot) & 15)); | |
} | |
void setPixel(uint16_t n, uint16_t r, uint16_t g, uint16_t b, uint16_t w = 0) | |
{ | |
// Convert RGB into linear brightness. | |
r = NeoPixel_Gamma_x128[min(r, uint16_t(255))]; | |
g = NeoPixel_Gamma_x128[min(g, uint16_t(255))]; | |
b = NeoPixel_Gamma_x128[min(b, uint16_t(255))]; | |
w = NeoPixel_Gamma_x128[min(w, uint16_t(255))]; | |
// Brightness scaling. | |
float brightness = min(NeoPixel_BrightnessMod, 2.0f); | |
r = uint16_t(r*brightness + .5f); | |
g = uint16_t(g*brightness + .5f); | |
b = uint16_t(b*brightness + .5f); | |
w = uint16_t(w*brightness + .5f); | |
// Dither randomly within one ULP of brightness (128) | |
unsigned spinDither = 2*frame_index + 3*n + 3*(n>=5); | |
unsigned rand = pcg16_random(); | |
r += 2 + 2*(spinDither++&15) + 2*( rand &15); | |
g += 2 + 2*(spinDither++&15) + 2*((rand>> 4)&15); | |
b += 2 + 2*(spinDither++&15) + 2*((rand>> 8)&15); | |
w += 2 + 2*(spinDither++&15) + 2*((rand>>12)&15); | |
static const uint16_t L_MAX = (128*255), L_MIN = (255); | |
// Excess RGB overflows into white. | |
if (r > 32640) {w += (r-L_MAX)/3; r = L_MAX;} | |
if (g > 32640) {w += (g-L_MAX)/2; g = L_MAX;} | |
if (b > 32640) {w += (g-L_MAX)/4; b = L_MAX;} | |
if (w > 32640) w = L_MAX; | |
CircuitPlayground.setPixelColor(n, uint8_t(r>>7), uint8_t(g>>7), uint8_t(b>>7)); //uint8_t(w>>7) | |
} | |
void setPixel(uint16_t n, uint32_t color) | |
{ | |
setPixel(n, | |
((color>>16)&255), | |
((color>>8)&255), | |
(color&255), | |
(color>>24)); | |
} | |
void refreshPixels() | |
{ | |
// Calculate brightness adjustment. | |
float brightness = 1.f; | |
if (alarm.started) | |
{ | |
float pulse = .5f + .5f * sin(TWO_PI * countdown.remaining()/500.f); | |
brightness = pulse*pulse*2.0f; | |
} | |
else if (countdown.started) | |
{ | |
float pulse = .5f + .5f * sin(TWO_PI * countdown.remaining()/5000.f); | |
pulse = pulse*pulse*pulse; | |
brightness = .07f + .07f * pulse; | |
} | |
else | |
{ | |
brightness = 2.f - (millis() - last_moved) / 10000.f; | |
if (brightness > 1.f) brightness = 1.f; | |
if (brightness < 0.f) brightness = 0.f; | |
brightness *= .1f; | |
} | |
NeoPixel_BrightnessMod = brightness; | |
if (countdown.started) | |
{ | |
float progress = 1.0f - float(countdown.remaining()) / float(countdown.length); | |
for (long i = 0; i < 10; ++i) | |
{ | |
float fill = (progress-.1f*float(i))/.1f; | |
if (fill < 0.f) fill = 0.f; | |
if (fill > 1.f) fill = 1.f; | |
setPixel(i, 192.f * fill, 127.f, 192.f - 192.f*fill); | |
} | |
} | |
else | |
{ | |
switch (teaType) | |
{ | |
case SENCHA: | |
setPixel(0, 0x007f5f); | |
setPixel(1, 0x2b9348); | |
setPixel(2, 0x55a630); | |
setPixel(3, 0xaacc00); | |
setPixel(4, 0x80b918); | |
break; | |
case BLACK: | |
setPixel(0, 0x03071e); | |
setPixel(1, 0x370617); | |
setPixel(2, 0xdc2f02); | |
setPixel(3, 0xf48c06); | |
setPixel(4, 0xe85d04); | |
break; | |
default: | |
setPixel(0, 0x7F007F); | |
setPixel(1, 0x550055); | |
setPixel(2, 0xBB00BB); | |
setPixel(3, 0x550055); | |
setPixel(4, 0x7F007F); | |
break; | |
} | |
for (long i = 0; i < 5; ++i) | |
{ | |
setPixel(5+i, (resteeps > i) ? 0xfdd85d : 0x6798c0); | |
} | |
} | |
} | |
PinStatus b_right_prev = LOW, b_left_prev = LOW; | |
static const long ARP_PATTERN[8] = {440, 550, 660, 1600, 880, 1320, 1100, 880}; | |
long teaPower(long base, long resteeps) | |
{ | |
long value = base << (resteeps>>1); | |
if (resteeps & 1) {value = (17*value) / 12;} | |
return value; | |
} | |
// the loop function runs over and over again forever | |
void loop() { | |
PinStatus b_left = digitalRead(CPLAY_LEFTBUTTON); | |
PinStatus b_right = digitalRead(CPLAY_RIGHTBUTTON); | |
bool p_left = (b_left && !b_left_prev); | |
bool p_right = (b_right && !b_right_prev); | |
if (countdown.started) | |
{ | |
if (p_right) // Cancels the alarm. | |
{ | |
p_left = false; | |
p_right = false; | |
countdown.reset(); | |
alarm.begin(250); | |
} | |
refreshPixels(); | |
} | |
else | |
{ | |
if (teaType == MAX_TEA_TYPE) | |
{ | |
// Just started loop | |
teaType = SENCHA; | |
resteeps = 0; | |
gravity.readSensor(); | |
last_moved = millis(); | |
} | |
if (p_right) | |
{ | |
resteeps += 1; | |
if (resteeps > 5) | |
{ | |
resteeps = 0; | |
teaType = TeaType((long(teaType)+1) % MAX_TEA_TYPE); | |
// Beep! | |
refreshPixels(); | |
tone(A0, 440); delay(50); | |
tone(A0, 330); delay(50); | |
tone(A0, 660); delay(50); | |
tone(A0, 880); delay(100); | |
noTone(A0); | |
} | |
else | |
{ | |
// Beep! | |
refreshPixels(); | |
tone(A0, teaPower(275, 5-resteeps)); delay(50); | |
tone(A0, teaPower(220, 5-resteeps)); delay(50); | |
noTone(A0); | |
} | |
} | |
refreshPixels(); | |
if (p_left) | |
{ | |
long baseTime = 60; | |
switch (teaType) | |
{ | |
case SENCHA: baseTime = 60; break; | |
case BLACK: baseTime = 120; break; | |
} | |
long upscale = resteeps; | |
while (upscale > 1) {baseTime *= 2; upscale -= 2;} | |
if (upscale == 1) {baseTime = (17*baseTime) / 12;} | |
countdown.begin(1000 * baseTime); | |
// Beep! | |
for (int i = 0; i < 10; ++i) setPixel(i, 0x7F00FF00); | |
tone(A0, 880); delay(50); | |
tone(A0, 1320); delay(50); | |
tone(A0, 1760); delay(100); | |
noTone(A0); | |
} | |
} | |
{ | |
Accel accel; | |
accel.readSensor(); | |
float dtf = (accel.time - gravity.time) / 1000.f; | |
float dx = accel.x-gravity.x, dy = accel.y-gravity.y, dz = accel.z-gravity.z; | |
if (sqrt(dx*dx+dy*dy+dz*dz)/dtf > 150.f) | |
{ | |
last_moved = millis(); | |
} | |
float a = pow(.95f, dtf); | |
gravity.x += a*dx; | |
gravity.y += a*dy; | |
gravity.z += a*dz; | |
gravity.time = accel.time; | |
} | |
if (countdown.done()) | |
{ | |
resteeps += 1; | |
alarm.begin(5000); | |
countdown.reset(); | |
} | |
static unsigned long alarm_arp = 0; | |
long delay_this_loop = FrameDT; | |
if (alarm.started) | |
{ | |
if (alarm.done() || p_left || p_right) | |
{ | |
p_left = false; | |
p_right = false; | |
alarm.reset(); | |
noTone(A0); | |
} | |
else | |
{ | |
tone(A0, ARP_PATTERN[alarm_arp&7]); | |
++alarm_arp; | |
delay_this_loop = 40; | |
} | |
} | |
else | |
{ | |
alarm_arp = 0; | |
if (countdown.started) | |
{ | |
// tick, tock... | |
digitalWrite(A0, (countdown.remaining()/1000) & 1); | |
} | |
else | |
{ | |
noTone(A0); | |
} | |
} | |
/*if (alarm.started) | |
{ | |
digitalWrite(CPLAY_REDLED, alarm.remaining() % 250 < 125); | |
} | |
else if (countdown.started) | |
{ | |
digitalWrite(CPLAY_REDLED, countdown.remaining() % 1000 < 100); | |
}*/ | |
// Simple framerate regulator | |
{ | |
unsigned long frame_cur = millis(); | |
unsigned long frame_len = (frame_cur - frame_mark); | |
if (delay_this_loop > frame_len) | |
{ | |
//digitalWrite(CPLAY_REDLED, 0); | |
delay(delay_this_loop - frame_len); | |
} | |
//else digitalWrite(CPLAY_REDLED, 1); | |
++frame_index; | |
frame_mark = frame_cur; | |
} | |
b_right_prev = b_right; | |
b_left_prev = b_left; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment