Last active
July 13, 2021 16:12
-
-
Save trishume/bbdae75792d2888708a01d5625fa9229 to your computer and use it in GitHub Desktop.
Latency Tester and Foot Pedals Teensyduino sketch
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
// Tristan's Foot Pedals and Latency Tester Arduino Program | |
// Provides 5 buttons: left click, right click, scroll up, scroll down, latency test | |
// If you don't want all of these you can comment out the buttons you don't need. | |
#define BOUNCE_LOCK_OUT | |
#include <Bounce2.h> | |
#include "Keyboard.h" | |
const int scrollInterval = 80; | |
// Create Bounce objects for each button. The Bounce object | |
// automatically deals with contact chatter or "bounce", and | |
// it makes detecting changes very simple. | |
// Five buttons to control the 5 mouse clicks | |
Bounce button1 = Bounce(); | |
Bounce button2 = Bounce(); // if a button is too "sensitive" | |
Bounce button3 = Bounce(); // to rapid touch, you can | |
Bounce button4 = Bounce(); // increase this time. | |
Bounce button5 = Bounce(); | |
unsigned long lastScrollDown = 0; | |
unsigned long lastScrollUp = 0; | |
void setup() { | |
// Configure the pins for input mode with pullup resistors. | |
// The pushbuttons connect from each pin to ground. When | |
// the button is pressed, the pin reads LOW because the button | |
// shorts it to ground. When released, the pin reads HIGH | |
// because the pullup resistor connects to +5 volts inside | |
// the chip. LOW for "on", and HIGH for "off" may seem | |
// backwards, but using the on-chip pullup resistors is very | |
// convenient. The scheme is called "active low", and it's | |
// very commonly used in electronics... so much that the chip | |
// has built-in pullup resistors! | |
pinMode(1, INPUT_PULLUP); | |
pinMode(2, INPUT_PULLUP); | |
pinMode(3, INPUT_PULLUP); | |
pinMode(4, INPUT_PULLUP); | |
pinMode(5, INPUT_PULLUP); | |
button1.interval(30); | |
button2.interval(30); | |
button3.interval(30); | |
button4.interval(30); | |
button5.interval(30); | |
// Which pin each button is on | |
button1.attach(2); | |
button2.attach(5); | |
button3.attach(3); | |
button4.attach(4); | |
button5.attach(1); // latency test button | |
// Serial.begin(38400); | |
} | |
enum TestState { | |
STATE_START, | |
STATE_WATCH, | |
STATE_WAIT, | |
}; | |
static const int kLatencyBufLen = 512; | |
static const int kSensorPin = A8; | |
static const unsigned long kStabilizeMillis = 300; | |
// Don't always start the tests at a constant offset from the measurement of the last test | |
// because this can lead to synchronizing with the display refresh and so under-measuring variance. | |
// We add an amount of randomness larger than the display refresh interval to avoid that. | |
static const unsigned long kStabilizeJitterMillis = 50; | |
static const unsigned kHistogramLen = 50; | |
static const int kHistogramDivisor = 10; | |
static const int kHistogramLevels = 10; | |
static const char kHistogramChars[kHistogramLevels] = {' ', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; | |
int histogram[kHistogramLen]; | |
char histogramOut[kHistogramLen+1]; | |
unsigned long latenciesBuf[kLatencyBufLen]; | |
unsigned long peakSum = 0; | |
int numPeaks = 0; | |
int numLatencies = 0; | |
int numPrints = 0; | |
char msg[256]; | |
// Type out all the latencies in an array of numbers you can use as JSON, Python or Ruby for analysis | |
void outputFullLatencyArray() { | |
for (int i = 0; i < numLatencies; i++) { | |
Keyboard.write(i ? ',' : '['); | |
sprintf(msg, "%lu", latenciesBuf[i]); | |
Keyboard.print(msg); | |
} | |
Keyboard.print("]"); | |
} | |
// Type out the latency test results along with a nice histogram | |
// Looks like: | |
// lat ins= 58.9 +/- 8.3, all= 57, del= 54 (n= 87) | 13991 | | |
void outputLatencyResults() { | |
numPrints += 1; | |
if(numPrints % 2 == 0) { | |
outputFullLatencyArray(); | |
return; | |
} | |
unsigned long sumAll = 0; | |
unsigned long sum = 0; | |
unsigned long sumSq = 0; | |
unsigned long n = 0; | |
for(unsigned i = 0; i < kHistogramLen; i++) histogram[i] = 0; | |
for(int i = 0; i < numLatencies; i++) { | |
unsigned long lat = latenciesBuf[i]; | |
sumAll += lat; | |
// only counting inserts | |
if(i % 2 == 0) { | |
sum += lat; | |
sumSq += lat*lat; | |
n += 1; | |
} | |
int bucket = min(lat/kHistogramDivisor, kHistogramLen-1); | |
histogram[bucket] += 1; | |
} | |
int histogramMax = 0; | |
for(unsigned i = 0; i < kHistogramLen; i++) histogramMax = max(histogramMax, histogram[i]); | |
for(unsigned i = 0; i < kHistogramLen; i++) { | |
int level = (histogram[i]*(kHistogramLevels-1))/histogramMax; | |
histogramOut[i] = kHistogramChars[level]; | |
// always show buckets with samples as at least level one to catch outliers | |
if(level == 0 && histogram[i] > 0) histogramOut[i] = '_'; | |
} | |
histogramOut[kHistogramLen] = 0; | |
float fn = n; | |
float stddev = sqrt((fn * sumSq - sum * sum) / (fn * (fn - 1))); | |
int mean = (10 * sum + n / 2) / n; | |
int stddevi = int(stddev * 10 + 5); | |
int meanAll = (sumAll + numLatencies / 2) / numLatencies; | |
int meanPeak = peakSum / numPeaks; | |
int numDel = numLatencies - n; | |
int meanDel = ((sumAll-sum) + numDel / 2) / numDel; | |
sprintf(msg, "lat i=%3d.%d +/- %3d.%d, a=%3d, d=%3d (n=%3d,q=%3d) |%s|", mean / 10, mean % 10, stddevi / 10, stddevi % 10, meanAll, meanDel, numLatencies, meanPeak, histogramOut); | |
Keyboard.print(msg); | |
// Serial.println(msg); | |
} | |
// Do a latency test using the light sensor | |
void latencyTest() { | |
// If it's a short press of the button we want to output results so wait and see if the press ends quickly | |
for(int i = 0; i < 500; i++) { | |
button5.update(); | |
if(button5.rose()) { | |
outputLatencyResults(); | |
return; | |
} | |
delay(1); | |
} | |
// For long presses we run testing iterations as long as the button is held | |
TestState state = STATE_START; | |
unsigned long pressedAt = 0; | |
int valueWhenPressed = 0; | |
bool characterPresent = false; | |
peakSum = 0; | |
numPeaks = 0; | |
numLatencies = 0; | |
numPrints = 0; | |
while(true) { | |
button5.update(); | |
bool held = !(button5.read()); | |
if(!held) break; | |
if(state == STATE_START) { | |
uint16_t key = characterPresent ? KEY_BACKSPACE : 'a'; | |
int newValue = analogRead(kSensorPin); | |
// We alternate between starting with a stablized 'a' and stabilized empty so we can | |
// measure the peak to peak signal strength to check the validity of our measurements | |
if(valueWhenPressed != 0) { | |
peakSum += abs(valueWhenPressed-newValue); | |
numPeaks += 1; | |
} | |
valueWhenPressed = newValue; | |
Keyboard.press(key); | |
pressedAt = millis(); | |
Keyboard.release(key); | |
characterPresent = !characterPresent; | |
state = STATE_WATCH; | |
} else if(state == STATE_WATCH) { | |
if (abs(analogRead(kSensorPin) - valueWhenPressed) > 5) { | |
unsigned long now = millis(); | |
unsigned long latency = now - pressedAt; | |
// TODO handle overflow? (only happens every 50 days of uptime) | |
if(numLatencies >= kLatencyBufLen) break; | |
latenciesBuf[numLatencies] = latency; | |
numLatencies += 1; | |
state = STATE_WAIT; | |
} else { | |
delayMicroseconds(50); | |
} | |
} else if(state == STATE_WAIT) { | |
// TODO maybe let the loop tick multiple times during wait period | |
delay(kStabilizeMillis); | |
delay(random(kStabilizeJitterMillis)); | |
state = STATE_START; | |
} else { | |
// TODO throw error? | |
break; | |
} | |
} | |
// Dependon when we stopped there might be a typed character left we need to clean up | |
if(characterPresent) { | |
Keyboard.press(KEY_BACKSPACE); | |
Keyboard.release(KEY_BACKSPACE); | |
} | |
} | |
void loop() { | |
// Update all the buttons. There should not be any long | |
// delays in loop(), so this runs repetitively at a rate | |
// faster than the buttons could be pressed and released. | |
button1.update(); | |
button2.update(); | |
button3.update(); | |
button4.update(); | |
button5.update(); | |
if (button1.fell()) { | |
Mouse.press(MOUSE_LEFT); | |
// Keyboard.press(KEY_F6); | |
} | |
if (button1.rose()) { | |
// Keyboard.release(KEY_F6); | |
Mouse.release(MOUSE_LEFT); | |
} | |
if (button2.fell()) { | |
// Keyboard.press(KEY_F5); | |
Mouse.press(MOUSE_RIGHT); | |
} | |
if (button2.rose()) { | |
// Keyboard.release(KEY_F5); | |
Mouse.release(MOUSE_RIGHT); | |
} | |
if (button5.fell()) { | |
latencyTest(); | |
} | |
bool scrollDown = button3.read(); | |
bool scrollUp = button4.read(); | |
unsigned long tick = millis(); | |
if (scrollDown && !scrollUp && (tick > lastScrollDown + scrollInterval || tick < lastScrollDown)) { | |
Mouse.scroll(-1); | |
lastScrollDown = tick; | |
} | |
if (!scrollDown && scrollUp && (tick > lastScrollUp + scrollInterval || tick < lastScrollUp)) { | |
Mouse.scroll(1); | |
lastScrollUp = tick; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment