Skip to content

Instantly share code, notes, and snippets.

@trishume
Last active July 13, 2021 16:12
Show Gist options
  • Save trishume/bbdae75792d2888708a01d5625fa9229 to your computer and use it in GitHub Desktop.
Save trishume/bbdae75792d2888708a01d5625fa9229 to your computer and use it in GitHub Desktop.
Latency Tester and Foot Pedals Teensyduino sketch
// 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