Skip to content

Instantly share code, notes, and snippets.

@larsch
Created September 23, 2016 10:28
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save larsch/90173bdc9cfa11518ff175151675f275 to your computer and use it in GitHub Desktop.
Save larsch/90173bdc9cfa11518ff175151675f275 to your computer and use it in GitHub Desktop.
#include <avr/sleep.h>
// const int com[4] = { 10, 13, 14, 4 };
// const int com[4] = { 4, 14, 13, 10 };
// const int dig[8] = { 11, 15, 6, 8, 9, 12, 5, 7 };
// const int dig[8] = { 13, 17, 4, 6, 7, 14, 3, 5 };
// const int num[10] = { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f };
// int nmd[10] = {};
// int nmb[10] = {};
// int nmc[10] = {};
#define debug(x) \
Serial.print(#x " "); Serial.println(x)
#define BUTTON_READ A7
#define BUTTON_PULLUP A5
#define USE_TIMER1 1
int8_t sine[256];
void sineInit() {
uint8_t i = 0;
do {
sine[i] = lround(127.0 * sin(i * 2.0 * M_PI / 256.0));
++i;
} while (i != 0);
}
void checkButton();
void timerReady();
void timerCountDown();
// Wiring:
//
// D12 D13 A0 A1 A2 A3
// | | | | | |
// 12 11 10 9 8 7
// [ 7-Segment LED ]
// 1 2 3 4 5 6
// | | | | | |
// D7 D6 D5 D4 D3 D2
//
// __
// A5---^ ---A7--[20 kΩ]---GND
//
// Port registers:
//
// bit 7 6 5 4 3 2 1 0
// PORT D e d DP c g 4
// PORT B a 1
// PORT C r b 3 2 f
//
// r = button
// 1-4 = anodes
// a-g = cathodes
#define MASK_D 0xFC
#define MASK_B 0x30
#define MASK_C 0x0F
#define CATHODE_MASK_D 0xF8
#define CATHODE_MASK_B 0x20
#define CATHODE_MASK_C 0x09
#define ANODE_MASK_D 0x04
#define ANODE_MASK_B 0x10
#define ANODE_MASK_C 0x06
#define INVMASKD (MASK_D ^ 0xFF)
#define INVMASKB (MASK_B ^ 0xFF)
#define INVMASKC (MASK_C ^ 0xFF)
static unsigned long start;
// 11 10 9 8 7 6 5 4 3 2 1 0
// e d DP c g 4 a 1 b 3 2 f
#define SEG_A 0x020
#define SEG_B 0x008
#define SEG_C 0x100
#define SEG_E 0x800
#define SEG_D 0x400
#define SEG_F 0x001
#define SEG_G 0x080
#define SEG_P 0x200
#define COM_1 0x010
#define COM_2 0x002
#define COM_3 0x004
#define COM_4 0x040
const static uint16_t seg[8] = { SEG_A, SEG_B, SEG_C, SEG_D, SEG_E, SEG_F, SEG_G, SEG_P };
uint16_t decimal[10] = {
SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F,
SEG_B | SEG_C,
SEG_A | SEG_B | SEG_G | SEG_E | SEG_D,
SEG_A | SEG_B | SEG_G | SEG_C | SEG_D,
SEG_B | SEG_C | SEG_F | SEG_G,
SEG_A | SEG_F | SEG_G | SEG_C | SEG_D,
SEG_A | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G,
SEG_A | SEG_B | SEG_C,
SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G,
SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G
};
#define BITS_D(x) ((((x) >> 4) & MASK_D) ^ CATHODE_MASK_D)
#define BITS_B(x) (((x) & MASK_B) ^ CATHODE_MASK_B)
#define BITS_C(x) (((x) & MASK_C) ^ CATHODE_MASK_C)
const uint16_t combits[4] = { COM_1, COM_2, COM_3, COM_4 };
static uint16_t displayReg[4] = {};
static unsigned long nextDigitMicros = 0;
static uint8_t currentDigit = 3;
static volatile bool displayComplete = false;
static uint8_t frameNumber = 0;
#define MAX_ANIM 4
static int animationNumber = 0;
static unsigned long waitTime = 2000;
unsigned long lastFrame = micros();
void showNextDigit() {
// Turn off current anode
switch (currentDigit) {
case 0: DDRB &= 0b11101111; break;
case 1: DDRC &= 0b11111101; break;
case 2: DDRC &= 0b11111011; break;
case 3: DDRD &= 0b11111011; break;
}
// Advance to next digit
currentDigit = (currentDigit + 1) % 4;
// Update cathode output state (high/low)
uint16_t combit = combits[currentDigit];
PORTD = (INVMASKD & PORTD) | BITS_D(displayReg[currentDigit] | combit);
PORTB = (INVMASKB & PORTB) | BITS_B(displayReg[currentDigit] | combit);
PORTC = (INVMASKC & PORTC) | BITS_C(displayReg[currentDigit] | combit);
// Turn on current anode
switch (currentDigit) {
case 0: DDRB |= 0b00010000; break;
case 1: DDRC |= 0b00000010; break;
case 2: DDRC |= 0b00000100; break;
case 3: DDRD |= 0b00000100; break;
}
nextDigitMicros += waitTime;
if (currentDigit == 3) {
displayComplete = true;
frameNumber++;
}
}
inline void checkUpdateDisplay() {
#if !USE_TIMER1
if ((long)(nextDigitMicros - micros()) < 0) {
showNextDigit();
checkButton();
}
#endif
}
void setDisplay(uint16_t digs[4]) {
// Serial.print(currentDigit);
// Serial.print(' ');
// Serial.println(nextDigitMicros - micros());
// waitTime += displayComplete ? 50 : -5;
// if (waitTime < 1000)
// waitTime = 1000;
unsigned long before = micros();
#if USE_TIMER1
set_sleep_mode(SLEEP_MODE_IDLE);
cli();
while (!displayComplete) {
sleep_enable();
sei();
sleep_cpu();
sleep_disable();
}
sei();
#else
while (!displayComplete)
checkUpdateDisplay();
#endif
unsigned long now = micros();
unsigned long frameTime = before - lastFrame + 500;
if (frameTime < 4000)
frameTime = 4000;
#if USE_TIMER1
ICR1 = frameTime * 2;
#else
waitTime = frameTime / 4;
#endif
lastFrame = now;
// Serial.print(DDRC);
// Serial.print(' ');
// Serial.println(PORTC);
unsigned long after = micros();
// Serial.print(currentDigit);
// Serial.print(' ');
// Serial.print(frameNumber);
// Serial.print(' ');
// Serial.println(frameTime);
// Serial.print(after - before);
// Serial.print(' ');
// Serial.println(before - lastFrame);
// Serial.print(' ');
// Serial.print(count);
// Serial.print(' ');
// Serial.println(displayComplete);
displayComplete = false;
displayReg[0] = digs[0];
displayReg[1] = digs[1];
displayReg[2] = digs[2];
displayReg[3] = digs[3];
}
void animateWalkingSquare() {
#define UPBUBBLE (SEG_A|SEG_B|SEG_G|SEG_F)
#define DOWNBUBBLE (SEG_C|SEG_D|SEG_E|SEG_G)
unsigned long n = millis() / 100;
uint16_t digs[4] = { 0, 0, 0, 0 };
unsigned long step = n % 8;
if (step < 4)
digs[step % 4] |= UPBUBBLE;
else
digs[3 - (step % 4)] |= DOWNBUBBLE;
setDisplay(digs);
}
void animateClimbingBars() {
#define ST1 (SEG_D)
#define ST2 (SEG_C|SEG_D|SEG_E|SEG_G)
#define ST3 (SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F|SEG_G)
#define ST4 (SEG_A|SEG_B|SEG_G|SEG_F)
#define ST5 (SEG_A)
uint16_t step[8] = { ST1, ST2, ST3, ST4, ST5, 0, 0, 0 };
unsigned long n = millis() / 100;
uint16_t digs[4] = { };
for (int i = 0; i < 4; ++i)
digs[i] = step[(n+i)%8];
setDisplay(digs);
}
void animateLeftToRightSweep() {
#define LEFT (SEG_E|SEG_F)
#define MID (SEG_A|SEG_G|SEG_D)
#define RIGHT (SEG_B|SEG_C)
#define DOT (SEG_P)
const int dist[16] = { 14, 38, 64,
111, 136, 160, 168,
184, 208, 234, 258,
266, 306, 332, 356,
363 };
const uint16_t p1[16] = { LEFT, MID, RIGHT,
0, 0, 0, DOT };
const uint16_t p2[16] = { 0, 0, 0,
LEFT, MID, RIGHT, 0,
DOT };
const uint16_t p3[16] = { 0, 0, 0,
0, 0, 0, 0,
0, LEFT, MID, RIGHT,
DOT };
const uint16_t p4[16] = { 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, LEFT, MID, RIGHT, DOT };
unsigned long n = millis();
long m = n % 650;
long o = m - 250;
uint16_t digs[4] = {};
for (int i = 0; i < 16; ++i) {
if (m >= dist[i] && o < dist[i]) {
digs[0] |= p1[i];
digs[1] |= p2[i];
digs[2] |= p3[i];
digs[3] |= p4[i];
}
}
setDisplay(digs);
}
const int16_t coordinates[4][8][2] = {
{
{ 139, 62 },
{ 200, 132 },
{ 181, 266 },
{ 101, 324 },
{ 41, 258 },
{ 61, 123 },
{ 122, 193 },
{ 474, 324 },
},
{
{ 398, 61 },
{ 460, 127 },
{ 442, 258 },
{ 363, 324 },
{ 302, 262 },
{ 321, 126 },
{ 385, 192 },
{ 511, 193 },
},
{
{ 661, 61 },
{ 719, 130 },
{ 701, 262 },
{ 622, 324 },
{ 562, 259 },
{ 580, 125 },
{ 640, 194 },
{ 733, 323 },
},
{
{ 920, 60 },
{ 980, 127 },
{ 961, 266 },
{ 883, 323 },
{ 821, 259 },
{ 841, 126 },
{ 901, 193 },
{ 995, 323 }
}
};
double sin1(long t, long w) {
return sin((t % w) / (double)w * 2 * M_PI);
}
void animateFlyingBlob() {
unsigned long time = millis();
// double t = (time % 1000) / 1000.0;
// double t2 = (time % 759) / 759.0;
double ext = 128.0;
double midx = 512.0;
double midy = 193.0;
double pfx = midx + (midx + ext) * (0.3 * sin1(time, 1000) + 0.7 * sin1(time, 2623)); // sin(t * 2 * M_PI);
double pfy = midy + (midy + ext) * (0.3 * sin1(time, 759) + 0.7 * sin1(time, 1728)); // cos(t2 * 2 * M_PI);
int px = pfx;
int py = pfy;
uint16_t digs[4] = {};
int size = 12 + 4.0 * sin1(time, 2873);
for (int d = 0; d < 4; ++d) {
for (int s = 0; s < 8; ++s) {
checkUpdateDisplay();
long x = coordinates[d][s][0];
long y = coordinates[d][s][1];
double dx = x - px;
double dy = y - py;
double dist = sqrt(dx * dx + dy * dy);
int i = size - dist / 32;
if (i > frameNumber % 4)
digs[d] |= seg[s];
}
}
setDisplay(digs);
}
inline long sqa(long a) {
return a * a;
}
void animateRotatingBar() {
unsigned long time = millis();
// double t = (time % 2000) / 2000.0;
double x1 = 512.0 + 512.0 * sin1(time, 1672);
double y1 = 193.0 + 100.0 * sin1(time, 934);
double ta = (time % 2873) / 2873.0;
double tb = (time % 3273) / 3273.0;
double angle = 0.673 * M_PI * sin(ta * 2 * M_PI) + 0.842 * M_PI * sin(tb * 2 * M_PI);
double x2 = x1 + 128.0 * cos(angle);
double y2 = y1 + 128.0 * sin(angle);
double dy = (y2 - y1);
double dx = (x2 - x1);
double d3 = sqa(dx) + sqa(dy);
uint16_t digs[4] = {};
for (int d = 0; d < 4; ++d) {
for (int s = 0; s < 8; ++s) {
checkUpdateDisplay();
long x0 = coordinates[d][s][0];
long y0 = coordinates[d][s][1];
double d1 = dx * (y1 - y0) - (x1 - x0) * dy;
double dsq = (d1 * d1) / d3;
int dist = sqrt(dsq);
int i = 8 - dist / 32;
// 0x0f 0x07 0x0a 0x01
if (i > frameNumber % 4)
digs[d] |= seg[s];
// if (dsq < size2q)
// digs[d] |= seg[s];
// else if (dsq < size2a && (frameNumber % 4))
// digs[d] |= seg[s];
// else if (dsq < size2b && (frameNumber % 2))
// digs[d] |= seg[s];
// else if (dsq < size2c && !(frameNumber % 4))
// digs[d] |= seg[s];
}
}
setDisplay(digs);
}
void animateWiper() {
unsigned long time = millis();
// double t = (time % 2000) / 2000.0;
double x1 = 512.0;
double y1 = 600.0;
double angle = M_PI * (abs((long)(time % 2000) - 1000) / 1000.0);
double x2 = x1 + 128.0 * cos(angle);
double y2 = y1 + 128.0 * sin(angle);
double dy = (y2 - y1);
double dx = (x2 - x1);
double d3 = sqa(dx) + sqa(dy);
uint16_t digs[4] = {};
for (int d = 0; d < 4; ++d) {
for (int s = 0; s < 8; ++s) {
checkUpdateDisplay();
long x0 = coordinates[d][s][0];
long y0 = coordinates[d][s][1];
double d1 = dx * (y1 - y0) - (x1 - x0) * dy;
double dsq = (d1 * d1) / d3;
int dist = sqrt(dsq);
int i = 4 - dist / 64;
// 0x0f 0x07 0x0a 0x01
if (i > frameNumber % 4)
digs[d] |= seg[s];
// if (dsq < size2q)
// digs[d] |= seg[s];
// else if (dsq < size2a && (frameNumber % 4))
// digs[d] |= seg[s];
// else if (dsq < size2b && (frameNumber % 2))
// digs[d] |= seg[s];
// else if (dsq < size2c && !(frameNumber % 4))
// digs[d] |= seg[s];
}
}
setDisplay(digs);
}
void animateRotatingWaves() {
unsigned long time = millis();
// float t = (time % 2000) / 2000.0;
float t2 = (time % 3387) / 3387.0;
float distScale = (0.5 + 0.2 * sin(t2 * 2 * M_PI));
float phase = (time % 2382) / 2382.0 * 2 * M_PI;
float x1 = 512.0;
float y1 = 193.0;
float ta = (time % 2873) / 2873.0;
float angle = ta * 2 * M_PI;
float x2 = x1 + 128.0 * cos(angle);
float y2 = y1 + 128.0 * sin(angle);
float dy = (y2 - y1);
float dx = (x2 - x1);
float d3 = sqa(dx) + sqa(dy);
uint16_t digs[4] = {};
for (int d = 0; d < 4; ++d) {
for (int s = 0; s < 8; ++s) {
checkUpdateDisplay();
float x0 = coordinates[d][s][0];
float y0 = coordinates[d][s][1];
float d1 = dx * (y1 - y0) - (x1 - x0) * dy;
float dsq = (d1 * d1) / d3;
int dist = sqrt(dsq) * distScale;
int i = 1.5 + 3.0 * cos(dist / 64 + phase);
// 0x0f 0x07 0x0a 0x01
if (i > frameNumber % 4)
digs[d] |= seg[s];
// if (dsq < size2q)
// digs[d] |= seg[s];
// else if (dsq < size2a && (frameNumber % 4))
// digs[d] |= seg[s];
// else if (dsq < size2b && (frameNumber % 2))
// digs[d] |= seg[s];
// else if (dsq < size2c && !(frameNumber % 4))
// digs[d] |= seg[s];
}
}
// Serial.print(currentDigit);
// Serial.print(' ');
// Serial.println(nextDigitMicros - micros());
setDisplay(digs);
}
void animateWaves() {
uint16_t digs[4] = {};
uint16_t xoffset = micros() >> 9; // 32768 - sine[(millis()>>3) & 0xFF] << 3;
int16_t midx = 512;
int16_t midy = 193;
int8_t sina = sine[(millis()>>5) & 0xFF];
int8_t cosa = sine[(64 + (millis()>>5)) & 0xFF];
for (int d = 0; d < 4; ++d) {
for (int s = 0; s < 8; ++s) {
checkUpdateDisplay();
long x0 = coordinates[d][s][0] - midx;
long y0 = coordinates[d][s][1] - midy;
long x = midx + (x0 * cosa - y0 * sina) / 130;
// long y = midy + (x0 * sina + y0 * cosa) / 128;
uint16_t c = x + xoffset;
int8_t v = sine[(c/4) & 0xFF];
int8_t i = v / 48 + 2;
if (i > frameNumber % 4)
digs[d] |= seg[s];
}
}
setDisplay(digs);
}
void buttonInit()
{
DDRC |= 0x20; // A5 = output
PORTC |= 0x20; // A5 = high
// pinMode(A5, OUTPUT);
// digitalWrite(A5, HIGH);
Serial.print(DDRC);
Serial.print(' ');
Serial.println(PORTC);
}
void onButtonPress();
void onButtonDoublePress();
void onButtonRelease();
void onButtonHold();
static uint16_t pressRegister = 0;
static uint16_t releaseRegister = 0;
static unsigned long pressTime;
enum ButtonState { Released, PressedWait, ReleasedWait, Pressed };
static ButtonState buttonState = Released;
void checkButton()
{
unsigned long now = millis();
int button = analogRead(BUTTON_READ) > 512;
pressRegister = (pressRegister << 1) | (!button) | 0xe000;
releaseRegister = (releaseRegister << 1) | button | 0xe000;
if (pressRegister == 0xf000) {
if (buttonState == ReleasedWait) {
onButtonDoublePress();
buttonState = Pressed;
// Serial.println("Pressed");
} else {
buttonState = PressedWait;
// Serial.println("PressedWait");
}
pressTime = now;
} else if (releaseRegister == 0xf000) {
if (buttonState == PressedWait) {
buttonState = ReleasedWait;
// Serial.println("ReleasedWait");
} else {
buttonState = Released;
// Serial.println("Released");
}
} else {
unsigned long timePassed = millis() - pressTime;
if (buttonState == PressedWait) {
if (timePassed > 600) {
onButtonHold();
buttonState = Pressed;
// Serial.println("Pressed");
}
} else if (buttonState == ReleasedWait) {
if (timePassed > 400) {
onButtonPress();
buttonState = Released;
// Serial.println("Released");
}
}
}
}
// void loop1()
// {
// static int c = 0;
// static int d = 0;
// pinMode(com[c], INPUT);
// pinMode(dig[d], INPUT);
// d = (d + 1) % 8;
// if (d == 0)
// c = (c + 1) % 4;
// pinMode(com[c], OUTPUT);
// digitalWrite(com[c], 1);
// pinMode(dig[d], OUTPUT);
// digitalWrite(com[c], 1);
// delay(100);
// }
// void loop2() {
// static int n = 0;
// n = (millis() / 50);
// for (int c = 0; c < 4; ++c) {
// int m = n % 10;
// n /= 10;
// pinMode(com[c], OUTPUT);
// digitalWrite(com[c], 1);
// int mask = num[m];
// for (int d = 0; d < 8; ++d) {
// // if (mask & 1) {
// // pinMode(dig[d], INPUT);
// // } else {
// // pinMode(dig[d], OUTPUT);
// // digitalWrite(dig[d], HIGH);
// // }
// pinMode(dig[d], OUTPUT);
// digitalWrite(dig[d], !(mask & 1));
// mask >>= 1;
// }
// delay(5);
// for (int d = 0; d < 8; ++d)
// pinMode(dig[d], INPUT);
// pinMode(com[c], INPUT);
// }
// }
void (*animationFunction[MAX_ANIM])() = {
animateWaves,
animateWiper,
// animateRotatingWaves,
animateFlyingBlob,
// animateLeftToRightSweep,
// animateClimbingBars,
// animateWalkingSquare,
animateRotatingBar,
};
unsigned long endTime;
#define WORK_DURATION (25L * 60L * 1000L)
#define BREAK_DURATION (5L * 60L * 1000L)
// #define WORK_DURATION (25L * 1000L)
// #define BREAK_DURATION (5L * 1000L)
unsigned long runTime = WORK_DURATION;
unsigned long readyTime;
unsigned long screenSaverTime;
uint8_t countDownMode = 0;
void (*state)();
void makeDigs(uint16_t *digs, int val, bool dots) {
int min = (val / 60) % 100;
int sec = val % 60;
digs[0] = decimal[min / 10];
digs[1] = decimal[min % 10];
digs[2] = decimal[sec / 10];
digs[3] = decimal[sec % 10];
if (dots)
digs[1] |= SEG_P;
}
void setTimer(unsigned long duration) {
runTime = duration;
state = timerReady;
readyTime = millis();
}
void startTimer() {
endTime = millis() + runTime;
state = timerCountDown;
countDownMode = 0;
}
void startScreenSaver();
void nextScreenSaver() {
screenSaverTime = millis();
animationNumber = (animationNumber + 1) % MAX_ANIM;
}
void screenSaver() {
if (millis() - screenSaverTime > 15000)
nextScreenSaver();
animationFunction[animationNumber]();
}
void startScreenSaver() {
state = screenSaver;
nextScreenSaver();
}
void timerReady() {
if ((millis() - readyTime) > 30000)
startScreenSaver();
int i = 2 + 3 * sin1(millis(), 1234);
if (i > frameNumber % 4) {
uint16_t digs[4];
makeDigs(digs, runTime / 1000, true);
setDisplay(digs);
} else {
uint16_t digs[4] = {};
setDisplay(digs);
}
}
void showTimeLeft(long timeLeft) {
uint16_t digs[4];
makeDigs(digs, (timeLeft + 999) / 1000, (timeLeft / 500) % 2);
setDisplay(digs);
}
void startNextTimer() {
if (runTime == WORK_DURATION) {
setTimer(BREAK_DURATION);
} else {
setTimer(WORK_DURATION);
}
}
void clearDisplay() {
uint16_t digs[4] = {};
setDisplay(digs);
}
void timerCountDown() {
long timeLeft = endTime - millis();
if (timeLeft < -500)
startNextTimer();
else if (countDownMode == 0 || timeLeft < 60000L)
showTimeLeft(timeLeft);
else if (countDownMode == 1)
screenSaver();
else
clearDisplay();
}
void timerFinished() {
}
void onButtonPress()
{
if (state == timerReady)
startTimer();
else if (state == screenSaver)
nextScreenSaver();
else if (state == timerCountDown)
countDownMode = (countDownMode + 1) % 3;
}
void onButtonDoublePress() {
if (state == timerReady)
startNextTimer();
}
void onButtonRelease() {
}
void onButtonHold() {
if (state == timerReady)
startScreenSaver();
else if (state == timerCountDown)
setTimer(WORK_DURATION);
else if (state == screenSaver)
setTimer(runTime);
}
#define TCCR1B_CS (_BV(CS10) | _BV(CS11) | _BV(CS12))
void timerInterruptSetup() {
// 32000 cycles per interrupt = 500 hz = 2 ms/interrupt
ICR1 = 32000 / 2;
// Enable interrupt
TIMSK1 |= _BV(TOIE1);
// Enable timer 1 (mode 8)
TCCR1A = 0;
TCCR1B = _BV(WGM13) | _BV(CS10);
// debug(ICR1);
// debug(TIMSK1);
// debug(TCCR1A);
// debug(TCCR1B);
}
static unsigned long t;
ISR(TIMER1_OVF_vect)
{
showNextDigit();
checkButton();
// debug(currentDigit);
}
void setup()
{
Serial.begin(115200);
sineInit();
buttonInit();
start = millis();
// Set all setDisplay pins to output mode
DDRD = (INVMASKD & DDRD) | MASK_D;
DDRB = (INVMASKB & DDRB) | MASK_B;
DDRC = (INVMASKC & DDRC) | MASK_C;
setTimer(WORK_DURATION);
state = screenSaver;
#if USE_TIMER1
timerInterruptSetup();
#endif
}
void loop() {
state();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment