Skip to content

Instantly share code, notes, and snippets.

@battis
Last active May 4, 2018 12:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save battis/d111c0f60e394dd0205418054b083eb6 to your computer and use it in GitHub Desktop.
Save battis/d111c0f60e394dd0205418054b083eb6 to your computer and use it in GitHub Desktop.
Musical Stairs
/*
* Musical Stairs
*
* The core assumptions of this code mostly have to do with the sequence in which the
* XSHUT pins are wired together:
*
* 1. All of the XSHUT pins are contiguous on the Arduino, starting at XSHUT_OFFSET.
* 2. The pins are ordered L1, R1, L2, R2, ... , L10, R10
*
* At the moment, we are triggering a note to be played if REQ_CONSECUTIVE_BREAKS consecutive
* readings are broken (i.e. less than UNBROKEN_RANGE) following an unbroken reading.
*
* We have also set a very short SENSOR_TIMEOUT since we will have to poll every sensor
* every time, and waiting for a single sensor to timeout will impinge on the
* responsiveness of the stairs overall.
*
* FIXME There is also the possibility that both sensors might detect the same break,
* which could trigger two simultaneous(ish) notes. This could be fixed by tracking which
* stairs have been triggered _this round_ perhaps.
*
* @author Seth Battis <sbattis@gannacademy.org>
* @author Zachary Sherman <19zsherman@gannacademy.org>
* @author Ilana Jacobs <19ijacobs@gannacademy.org>
* @author Eli Cole <19ecole@gannacademy.org>
*/
#include <Wire.h>
#include <VL53L0X.h>
#include <MIDI.h>
const bool LOGGING = false; // show output logs in Serial Monitor
const long SENSOR_TIMEOUT = 27; // milliseconds
const int XSHUT_OFFSET = 2; // initial XSHUT pin (must be contiguous)
const int REQ_CONSECUTIVE_BREAKS = 3; // number of consecutive broken readings to count as broken
const int UNBROKEN_RANGE = 1100; // minimum "unbroken" distance for a sensor
const int SERIAL_BAUD_RATE = 115200; // baud
const int MIDI_CHANNEL = 4; // doesn't matter -- laptop listens to all channels
const int MIDI_VELOCITY = 100; // how hard the note is struck [0..128)
const int STAIRS = 5; // number of stairs currently connected
const int SIDES = 2; // one sensor on each side of each stair
// just reminders about how we understand the side indices
const int LEFT = 0;
const int RIGHT = 1;
// some sensors are still too flaky to use -- ignore them
const bool IGNORE_SENSOR[10][2] = {
{false, false}, // stair 1 (L1, R1)
{false, false},
{false, false},
{ true, false},
{false, false}, // stair 5 (L5, R5)
{false, false},
{ true, false},
{ true, false},
{false, true },
{false, false}, // stair 10 (L10, R10)
};
// global variables
MIDI_CREATE_DEFAULT_INSTANCE();
VL53L0X SENSOR[STAIRS][SIDES];
bool HISTORY[STAIRS][SIDES][REQ_CONSECUTIVE_BREAKS]; // history runs from most recent (0) to least recent (REQ_CONSECUTIVE_BREAKS - 1)
int STATE[STAIRS]; // store the loop counter for the most recent triggering of this step
/**
* Display optional Serial Monitor logging messages
* @param String message The message to display
* @param int indent (Optional) How far message should be indented
*/
void logging(String message, int indent = 0) {
if (LOGGING) {
for (int i = 0; i < indent; i++) {
Serial.print(" ");
}
Serial.println(message);
}
}
/**
* A visual divider
* @return String The divider to display
*/
String divider() {
return "------------------------------------------------------------------------";
}
/**
* A human-readable name for a sensor
* @param int stair Stair numer [0..STAIRS)
* @param int side Side [0..SIDES) or {LEFT, RIGHT}
* @return String Human-readable sensor name
*/
String sensorName(int stair, int side) {
return "stair " + (String) (stair + 1) + ", " + (side == LEFT ? "left" : "right") + " side";
}
/**
* Calculate the I^2C address a sensor based on its position on the stairs
* @param int stair Stair number [0..STAIRS)
* @param int side Which side of the stair [0..SIDES) or {LEFT, RIGHT}
* @return int I^2C address of sensor
*/
int sensorId(int stair, int side) {
return (stair * 2) + side;
}
/**
* Calculate XSHUT pin bsaed on sensor ID
* @param int sensorId I^2C address of the sensor
* @return int XSHUT pin number
*/
int xshut(int sensorId) {
return sensorId + XSHUT_OFFSET;
}
/**
* Calculate MIDI pitch (C major scale) based on the stair
* @param int stair Stair number [0..STAIRS)
*/
int note(int stair) {
if (stair < 3) {
return 60 + (stair * 2);
} else if (stair < 7) {
return 59 + (stair * 2);
} else {
return 58 + stair * 2;
}
}
/**
* Determine if a sensor's beam has been broken
* @precondition HISTORY contains whether or not the REQ_CONSECUTIVE_BREAKS
* previous readings were breaks
* @postcondition HISTORY has been updated to include this most recent reading
* at index 0 and other readings have cycled back in history
* @param int stair Stair number [0..STAIRS)
* @param int side Side [0..SIDES) or {LEFT, RIGHT}
* @param int measurement The current measurement from that sensor
* @return bool True if the beam is freshly broken by this reading, false otherwise
*/
bool broken(int stair, int side, int measurement) {
// is it a break in the beam?
bool now = measurement < UNBROKEN_RANGE && (measurement != 0 || !SENSOR[stair][side].timeoutOccurred());
bool result = now;
if (result) {
logging("Activity at " + sensorName(stair, side) + " 1" + (HISTORY[stair][side][0] ? "1" : "0") + (HISTORY[stair][side][1] ? "1" : "0") + (HISTORY[stair][side][2] ? "1" : "0"));
// have there been enough previous broken measurements?
for (int past = 0; past < REQ_CONSECUTIVE_BREAKS - 1 && result; past++) {
result = result && HISTORY[stair][side][past];
}
// was there an unbroken measurement before those breaks?
result = result && !HISTORY[stair][side][REQ_CONSECUTIVE_BREAKS - 1];
}
// add this current break information to the sensor history
for (int i = REQ_CONSECUTIVE_BREAKS - 1; i > 0; i--) {
HISTORY[stair][side][i] = HISTORY[stair][side][i-1];
}
HISTORY[stair][side][0] = now;
return result;
}
/**
* Play a note (if we're not logging to Serial Monitor)
* TODO We could use multiple serial channels to allow MIDI and Serial Monitor logging
* @param int stair Stair number [0..STAIRS)
*/
void playNote(int stair) {
if (!LOGGING) {
MIDI.sendNoteOn(note(stair), MIDI_VELOCITY, MIDI_CHANNEL);
}
}
/**
* Play the ready tone for a given stair
* @param int stair Stair number [0..STAIRS)
*/
void readyTone(int stair) {
playNote(stair);
delay(250);
}
/**
* Initialize a sensor
* @param int stair Stair number [0..STAIRS)
* @param int side Side number [0..SIDES) or {LEFT, RIGHT}
*/
void initializeSensor(int stair, int side) {
logging("Sensor " + (String) sensorId(stair, side) + " initializing");
pinMode(xshut(sensorId(stair, side)), INPUT);
delay(10);
logging("Activated XSHUT " + (String) xshut(sensorId(stair, side)), 1);
SENSOR[stair][side].init();
logging("Sensor " + (String) sensorId(stair, side) + " at " + sensorName(stair, side) + " initialized", 1);
SENSOR[stair][side].setAddress((uint8_t) sensorId(stair, side));
logging("Address set to " + (String) SENSOR[stair][side].getAddress(), 1);
SENSOR[stair][side].setTimeout(SENSOR_TIMEOUT);
logging("Timeout set to " + (String) SENSOR[stair][side].getTimeout(), 1);
for (int i = 0; i < REQ_CONSECUTIVE_BREAKS; i++) {
HISTORY[stair][side][i] = false;
}
logging("Reset sensor history", 1);
}
/**
* Initialize sensors and interfaces
*/
void setup() {
// prepare MIDI
MIDI.begin(MIDI_CHANNEL); // must be before Serial.begin()
// prepare for Serial Monitor logging output
Serial.begin(SERIAL_BAUD_RATE);
logging("");
logging(divider());
logging("MIDI interface initialized");
logging("Serial interface initialized");
// reset all sensors
for (int stair = 0; stair < STAIRS; stair++) {
for (int side = 0; side < SIDES; side++) {
pinMode(xshut(sensorId(stair, side)), OUTPUT);
digitalWrite(xshut(sensorId(stair, side)), LOW);
logging("Reset XSHUT " + (String) xshut(sensorId(stair, side)));
}
}
delay(10);
// set sensor I^2C addresses
Wire.begin(); // must be after XSHUT reset
logging("I^2C interface initialized");
for (int stair = 0; stair < STAIRS; stair++) {
for (int side = 0; side < SIDES; side++) {
initializeSensor(stair, side);
}
STATE[stair] = -1 * REQ_CONSECUTIVE_BREAKS;
logging("Reset stair state");
readyTone(stair);
}
logging(divider());
}
/**
* Poll sensors and send MIDI instructions
*/
bool FIRST_RUN = true;
long counter = 0;
void loop() {
if (FIRST_RUN) {
logging("Starting polling loop");
FIRST_RUN = false;
}
for (int stair = 0; stair < STAIRS; stair++) {
for (int side = 0; side < SIDES; side++) {
if (!IGNORE_SENSOR[stair][side] &&
broken(stair, side, SENSOR[stair][side].readRangeSingleMillimeters()) &&
STATE[stair] < counter - REQ_CONSECUTIVE_BREAKS) {
STATE[stair] = counter;
playNote(stair);
logging("Play note for " + sensorName(stair, side));
}
}
}
counter++;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment