<NME> walt.ino
<BEF> /*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define VERSION                 "6"

// Commands
// Digits 1 to 9 reserved for clock sync
#define CMD_PING_DELAYED        'D' // Ping/Pong with a delay
#define CMD_RESET               'F' // Reset all vars
#define CMD_SYNC_SEND           'I' // Send some digits for clock sync
#define CMD_PING                'P' // Ping/Pong with a single byte
#define CMD_VERSION             'V' // Determine which version is running
#define CMD_SYNC_READOUT        'R' // Read out sync times
#define CMD_GSHOCK              'G' // Send last shock time and watch for another shock.
#define CMD_TIME_NOW            'T' // Current time
#define CMD_SYNC_ZERO           'Z' // Initial zero

#define CMD_AUTO_SCREEN_ON      'C'
#define CMD_AUTO_SCREEN_OFF     'c'
#define CMD_SEND_LAST_SCREEN    'E'
#define CMD_BRIGHTNESS_CURVE    'U'

#define CMD_AUTO_LASER_ON       'L'
#define CMD_AUTO_LASER_OFF      'l'
#define CMD_SEND_LAST_LASER     'J'

#define CMD_AUDIO               'A'
#define CMD_BEEP                'B'
#define CMD_BEEP_STOP           'S'

#define CMD_SAMPLE_ALL          'Q'


#define NOTE_DELAY 10000 // 10 ms


// On Teensy LC probably need to use the high current pins
// for most LEDs.
#define MIDI_SYSEX_END '\xF7'

// LEDs
#define LED_PIN_INT 13 // Built-in LED
#define DEBUG_LED1 11  // On r0.7 PCB: D4 - Red
#define DEBUG_LED2 12  // On r0.7 PCB: D3 - Green

// WALT sensors
#define PD_LASER_PIN 14
#define PD_SCREEN_PIN 20  // Same as A6
#define G_PIN 15          // Same as A1
#define GZ_PIN 16         // Same as A2
#define AUDIO_PIN 22      // Same as A8
#define MIC_PIN 23        // Same as A9

// Threshold and hysteresis for screen on/off reading
#define SCREEN_THRESH_HIGH  110
#define SCREEN_THRESH_LOW  90

elapsedMicros time_us;

boolean led_state;
char tmp_str[256];

boolean led_state;
char tmp_str[256];

struct trigger {
  long t;  // time of latest occurrence in microseconds
  int value; // value at latest occurrence
  char tag;
};

#define TRIGGER_COUNT 5
struct trigger laser, screen, sound, midi, gshock, copy_trigger;
struct trigger * triggers[TRIGGER_COUNT] = {&laser, &screen, &sound, &midi, &gshock};

#define CLOCK_SYNC_N 9
struct clock_sync {
  boolean is_synced;
  int last_sent;
  unsigned long sync_times[CLOCK_SYNC_N];
};

struct clock_sync clock;

// Interrupt handler for laser photodiode
void irq_laser(void) {
  laser.t = time_us;
  // May need to remove the 'not' if not using internal pullup resistor
  laser.value = !digitalRead(PD_LASER_PIN);
  laser.count++;

  digitalWrite(DEBUG_LED2, laser.value);
  // led_state = !led_state;
}
  led_state = !led_state;
}

void send_trigger(struct trigger t) {
  char s[256];
  sprintf(s, "G %c %ld %d %d", t.tag, t.t, t.value, t.count);
  Serial.println(s);
  Serial.send_now();
}

// flips case for a give char. Unchanged if not in [A-Za-z].
    return c + 32;
  }
  if (c >= 'a' && c <= 'z') {
    return c - 32;
  }
  return c;
}

// Print the same char as the cmd but with flipped case

// Print the same char as the cmd but with flipped case
void send_ack(char cmd) {
  Serial.println(flip_case(cmd));
  Serial.send_now();
}

void init_clock() {
}

void init_vars() {
  noInterrupts();
  init_clock();

  for (int i = 0; i < TRIGGER_COUNT; i++) {
    memset(triggers[i], 0, sizeof(struct trigger));
  }

  laser.tag = 'L';
  screen.tag = 'S';
  gshock.tag = 'G';
  sound.tag = 'A';  // for Audio
  midi.tag = 'M';

  interrupts();
}

void setup() {
    // LEDs
  pinMode(DEBUG_LED1, OUTPUT);
  pinMode(DEBUG_LED2, OUTPUT);
  pinMode(LED_PIN_INT, OUTPUT);

  // Sensors
  pinMode(PD_SCREEN_PIN, INPUT);
  pinMode(G_PIN, INPUT);
  pinMode(GZ_PIN, INPUT);
  pinMode(PD_LASER_PIN, INPUT_PULLUP);
  attachInterrupt(PD_LASER_PIN, irq_laser, CHANGE);

  Serial.begin(115200);
  serial_over_midi = false;
  init_vars();

  led_state = HIGH;  // Turn on all LEDs on startup
  digitalWrite(LED_PIN_INT, led_state);
  digitalWrite(DEBUG_LED1, HIGH);
  // digitalWrite(LED_PIN_RED, HIGH);
  Serial.begin(115200);

  init_vars();
}

  digitalWrite(DEBUG_LED1, HIGH);
  for (i = 0; i < 1000; i++) {
    v = analogRead(PD_SCREEN_PIN);
    t = time_us;
    send(t);
    send(' ');
  for (i = 0; i < 1000; i++) {
    v = analogRead(PD_SCREEN_PIN);
    t = time_us;
    Serial.print(t);
    Serial.print(" ");
    Serial.println(v);
    delayMicroseconds(450);
  }
  digitalWrite(DEBUG_LED2, LOW);
  Serial.send_now();
  Serial.println("end");
  Serial.send_now();
}

void process_command(char cmd) {
    v = analogRead(GZ_PIN);
    t = time_us;
    send(t);
    send(' ');
    send(v);
    send_line();
      interrupts();
      send_ack(CMD_SYNC_ZERO);
    } else if (cmd == CMD_TIME_NOW) {
      Serial.print("t ");
      Serial.println(time_us);
      Serial.send_now();
    } else if (cmd == CMD_PING) {
      send_ack(CMD_PING);
    } else if (cmd == CMD_PING_DELAYED) {
  if (cmd == CMD_SYNC_ZERO) {
    noInterrupts();
    time_us = 0;
    init_clock();
    clock.is_synced = true;
    interrupts();
    led_state = LOW;
      if (clock.last_sent < CLOCK_SYNC_N) {
        t = clock.sync_times[clock.last_sent];
      }
      Serial.print(clock.last_sent + 1);
      Serial.print(":");
      Serial.println(t);
      Serial.send_now();
    } else if (cmd == CMD_SYNC_SEND) {
      clock.last_sent = -1;
      // Send CLOCK_SYNC_N times
    delay(10);
        delayMicroseconds(737); // TODO: change to some congifurable random
        char c = '1' + i;
        clock.sync_times[i] = time_us;
        Serial.print(c);
        Serial.send_now();
      }
      // TODO: This newline is useful for debugging, think if it's ok with the rest.
      Serial.println();
      Serial.send_now();
    } else if (cmd == CMD_RESET) {
      init_vars();
      send_ack(CMD_RESET);
    } else if (cmd == CMD_VERSION) {
      Serial.print(flip_case(cmd));
      Serial.print(" ");
      Serial.println(VERSION);
      Serial.send_now();
    } else if (cmd == CMD_GSHOCK) {
      Serial.println(gshock.t); // TODO: Serialize trigger
      Serial.send_now();
      gshock.t = 0;
      gshock.count = 0;
      gshock.probe = true;
      clock.sync_times[i] = time_us;
      send(c);
      send_line();
    }
  } else if (cmd == CMD_RESET) {
    init_vars();
    } else if (cmd == CMD_BEEP) {
      long beep_time = time_us;
      tone(MIC_PIN, 5000 /* Hz */);
      Serial.print(flip_case(cmd));
      Serial.print(" ");
      Serial.println(beep_time);
      Serial.send_now();
    } else if (cmd == CMD_BEEP_STOP) {
      noTone(MIC_PIN);
      send_ack(CMD_BEEP_STOP);
    gshock.count = 0;
    gshock.probe = true;
  } else if (cmd == CMD_AUDIO) {
    sound.t = 0;
    sound.count = 0;
      send_ack(CMD_MIDI);
    } else if (cmd == CMD_NOTE) {
      unsigned long note_time = time_us + NOTE_DELAY;
      Serial.print(flip_case(cmd));
      Serial.print(" ");
      Serial.println(note_time);
      Serial.send_now();
      while (time_us < note_time);
      usbMIDI.sendNoteOn(60, 99, 1);
      usbMIDI.send_now();
  } else if (cmd == CMD_BEEP_STOP) {
    noTone(MIC_PIN);
    send_ack(CMD_BEEP_STOP);
  } else if (cmd == CMD_MIDI) {
    midi.t = 0;
    midi.count = 0;
    midi.probe = true;
    midi.autosend = true;
    send_ack(CMD_MIDI);
  } else if (cmd == CMD_NOTE) {
    unsigned long note_time = time_us + NOTE_DELAY;
    send(flip_case(cmd));
    send(' ');
    send(note_time);
    send_line();
    while (time_us < note_time);
    usbMIDI.sendNoteOn(60, 99, 1);
    usbMIDI.send_now();
  } else if (cmd == CMD_AUTO_SCREEN_ON) {
    screen.value = analogRead(PD_SCREEN_PIN) > SCREEN_THRESH_HIGH;
    screen.autosend = true;
    screen.probe = true;
    send_ack(CMD_AUTO_SCREEN_ON);
  } else if (cmd == CMD_AUTO_SCREEN_OFF) {
      // This blocks all other execution for about 1 second
      run_brightness_curve();
    } else {
      Serial.print("Unknown command:");
      Serial.println(cmd);
    }
  }

    send_ack(CMD_AUTO_LASER_ON);
  } else if (cmd == CMD_AUTO_LASER_OFF) {
    laser.autosend = false;
    send_ack(CMD_AUTO_LASER_OFF);
  } else if (cmd == CMD_SEND_LAST_LASER) {
    send_trigger(laser);
    laser.count = 0;
  } else if (cmd == CMD_BRIGHTNESS_CURVE) {
    send_ack(CMD_BRIGHTNESS_CURVE);
    // This blocks all other execution for about 1 second
    run_brightness_curve();
  } else if (cmd == CMD_ACCELEROMETER_CURVE) {
    send_ack(CMD_ACCELEROMETER_CURVE);
    // This blocks all other execution for about 2 seconds
    run_accelerometer_curve();
  } else if (cmd == CMD_SAMPLE_ALL) {
    send(flip_case(cmd));
    send(" G:");
    send(analogRead(G_PIN));
    send(" PD_screen:");
    send(analogRead(PD_SCREEN_PIN));
    send(" PD_laser:");
    send(analogRead(PD_LASER_PIN));
    send_line();
  } else {
  }

  // Probe MIDI
  if(midi.probe) {
    if(usbMIDI.read()) {
      midi.t = time_us;
      midi.count++;
      midi.probe = false;
      led_state = !led_state;
    }
  }

  // Probe screen
    if (v > 900) {
      gshock.t = time_us;
      gshock.count++;
      gshock.probe = false;
      led_state = !led_state;
    }
  }

  // Probe audio
  if (sound.probe) {
    int v = analogRead(AUDIO_PIN);
    if (v > 20) {
      sound.t = time_us;
      sound.count++;
      sound.probe = false;
      led_state = !led_state;
    }
  }

  // Probe MIDI
  boolean has_midi = usbMIDI.read(1);
  if(has_midi && midi.probe && usbMIDI.getType() == 0) {  // Type 1: note on
    midi.t = time_us;
    midi.count++;
    midi.probe = false;
    led_state = !led_state;
  }

  // Check if we got incoming commands from the host
  if (Serial.available()) {
    char cmd = Serial.read();
    process_command(cmd);

    noInterrupts();
    if (triggers[i]->autosend && triggers[i]->count > 0) {
      should_send = true;
      copy_trigger = *(triggers[i]);
      triggers[i]->count = 0;
    }
    interrupts();

    if (should_send) {
      send_trigger(copy_trigger);
    }
  }

  // Check if we got incoming commands from the host
  if (has_midi) {
    if (usbMIDI.getType() == MIDI_MODE_TYPE) {
      short program = usbMIDI.getData1();
      serial_over_midi = (program == 1);
      send_buffer = (serial_over_midi ? MIDI_SYSEX_BEGIN : String());
    } else if (usbMIDI.getType() == MIDI_COMMAND_TYPE) {
      char cmd = usbMIDI.getData1();
      process_command(cmd);
    }
  }
  if (Serial.available()) {
    char cmd = Serial.read();
    process_command(cmd);
  }
}


<MSG> Add Serial-over-MIDI communication channel.

WALT commands will also be read from MIDI channel pressure messages, and
sending a MIDI program change message for program number 1 will change
WALT responses to be sent via. MIDI system exclusive messages -- rather
than the USB serial interface (any other program number will disable
this feature).

<DFF> @@ -46,6 +46,12 @@
 
 #define NOTE_DELAY 10000 // 10 ms
 
+// Message types for MIDI encapsulation
+#define MIDI_MODE_TYPE 4  // Program Change
+#define MIDI_COMMAND_TYPE 5  // Channel Pressure
+
+#define MIDI_SYSEX_BEGIN '\xF0'
+#define MIDI_SYSEX_END '\xF7'
 
 // On Teensy LC probably need to use the high current pins
 // for most LEDs.
@@ -76,6 +82,9 @@ elapsedMicros time_us;
 boolean led_state;
 char tmp_str[256];
 
+boolean serial_over_midi;
+String send_buffer;
+
 struct trigger {
   long t;  // time of latest occurrence in microseconds
   int value; // value at latest occurrence
@@ -108,11 +117,52 @@ void irq_laser(void) {
   led_state = !led_state;
 }
 
+void send(char c) { send_buffer += c; }
+void send(String s) { send_buffer += s; }
+
+void send(long l) {
+  char s[32];
+  sprintf(s, "%ld", l);
+  send(s);
+}
+
+void send(unsigned long l) {
+  char s[32];
+  sprintf(s, "%lud", l);
+  send(s);
+}
+
+void send(short i) { send((long)i); }
+void send(int i) { send((long)i); }
+void send(unsigned short i) { send ((unsigned long)i); }
+void send(unsigned int i) { send ((unsigned int)i); }
+
+void send_now() {
+  if (serial_over_midi) {
+    usbMIDI.sendSysEx(send_buffer.length(), (const uint8_t *)send_buffer.c_str());
+    usbMIDI.send_now();
+    send_buffer = MIDI_SYSEX_BEGIN;
+  } else {
+    Serial.write(send_buffer.c_str(), send_buffer.length());
+    Serial.send_now();
+    send_buffer = String();
+  }
+}
+
+void send_line() {
+  if (!serial_over_midi) {
+    send('\n');
+  } else {
+    send(MIDI_SYSEX_END);
+  }
+  send_now();
+}
+
 void send_trigger(struct trigger t) {
   char s[256];
   sprintf(s, "G %c %ld %d %d", t.tag, t.t, t.value, t.count);
-  Serial.println(s);
-  Serial.send_now();
+  send(s);
+  send_line();
 }
 
 // flips case for a give char. Unchanged if not in [A-Za-z].
@@ -128,8 +178,8 @@ char flip_case(char c) {
 
 // Print the same char as the cmd but with flipped case
 void send_ack(char cmd) {
-  Serial.println(flip_case(cmd));
-  Serial.send_now();
+  send(flip_case(cmd));
+  send_line();
 }
 
 void init_clock() {
@@ -175,6 +225,8 @@ void setup() {
   // digitalWrite(LED_PIN_RED, HIGH);
   Serial.begin(115200);
 
+  serial_over_midi = false;
+
   init_vars();
 }
 
@@ -187,15 +239,15 @@ void run_brightness_curve() {
   for (i = 0; i < 1000; i++) {
     v = analogRead(PD_SCREEN_PIN);
     t = time_us;
-    Serial.print(t);
-    Serial.print(" ");
-    Serial.println(v);
+    send(t);
+    send(' ');
+    send(v);
+    send_line();
     delayMicroseconds(450);
   }
   digitalWrite(DEBUG_LED2, LOW);
-  Serial.send_now();
-  Serial.println("end");
-  Serial.send_now();
+  send("end");
+  send_line();
 }
 
 void process_command(char cmd) {
@@ -208,9 +260,9 @@ void process_command(char cmd) {
       interrupts();
       send_ack(CMD_SYNC_ZERO);
     } else if (cmd == CMD_TIME_NOW) {
-      Serial.print("t ");
-      Serial.println(time_us);
-      Serial.send_now();
+      send("t ");
+      send(time_us);
+      send_line();
     } else if (cmd == CMD_PING) {
       send_ack(CMD_PING);
     } else if (cmd == CMD_PING_DELAYED) {
@@ -224,10 +276,10 @@ void process_command(char cmd) {
       if (clock.last_sent < CLOCK_SYNC_N) {
         t = clock.sync_times[clock.last_sent];
       }
-      Serial.print(clock.last_sent + 1);
-      Serial.print(":");
-      Serial.println(t);
-      Serial.send_now();
+      send(clock.last_sent + 1);
+      send(':');
+      send(t);
+      send_line();
     } else if (cmd == CMD_SYNC_SEND) {
       clock.last_sent = -1;
       // Send CLOCK_SYNC_N times
@@ -235,23 +287,20 @@ void process_command(char cmd) {
         delayMicroseconds(737); // TODO: change to some congifurable random
         char c = '1' + i;
         clock.sync_times[i] = time_us;
-        Serial.print(c);
-        Serial.send_now();
+        send(c);
+        send_line();
       }
-      // TODO: This newline is useful for debugging, think if it's ok with the rest.
-      Serial.println();
-      Serial.send_now();
     } else if (cmd == CMD_RESET) {
       init_vars();
       send_ack(CMD_RESET);
     } else if (cmd == CMD_VERSION) {
-      Serial.print(flip_case(cmd));
-      Serial.print(" ");
-      Serial.println(VERSION);
-      Serial.send_now();
+      send(flip_case(cmd));
+      send(' ');
+      send(VERSION);
+      send_line();
     } else if (cmd == CMD_GSHOCK) {
-      Serial.println(gshock.t); // TODO: Serialize trigger
-      Serial.send_now();
+      send(gshock.t);  // TODO: Serialize trigger
+      send_line();
       gshock.t = 0;
       gshock.count = 0;
       gshock.probe = true;
@@ -264,10 +313,10 @@ void process_command(char cmd) {
     } else if (cmd == CMD_BEEP) {
       long beep_time = time_us;
       tone(MIC_PIN, 5000 /* Hz */);
-      Serial.print(flip_case(cmd));
-      Serial.print(" ");
-      Serial.println(beep_time);
-      Serial.send_now();
+      send(flip_case(cmd));
+      send(' ');
+      send(beep_time);
+      send_line();
     } else if (cmd == CMD_BEEP_STOP) {
       noTone(MIC_PIN);
       send_ack(CMD_BEEP_STOP);
@@ -279,10 +328,10 @@ void process_command(char cmd) {
       send_ack(CMD_MIDI);
     } else if (cmd == CMD_NOTE) {
       unsigned long note_time = time_us + NOTE_DELAY;
-      Serial.print(flip_case(cmd));
-      Serial.print(" ");
-      Serial.println(note_time);
-      Serial.send_now();
+      send(flip_case(cmd));
+      send(' ');
+      send(note_time);
+      send_line();
       while (time_us < note_time);
       usbMIDI.sendNoteOn(60, 99, 1);
       usbMIDI.send_now();
@@ -313,8 +362,9 @@ void process_command(char cmd) {
       // This blocks all other execution for about 1 second
       run_brightness_curve();
     } else {
-      Serial.print("Unknown command:");
-      Serial.println(cmd);
+      send("Unknown command: ");
+      send(cmd);
+      send_line();
     }
   }
 
@@ -346,13 +396,12 @@ void loop() {
   }
 
   // Probe MIDI
-  if(midi.probe) {
-    if(usbMIDI.read()) {
-      midi.t = time_us;
-      midi.count++;
-      midi.probe = false;
-      led_state = !led_state;
-    }
+  boolean has_midi = usbMIDI.read(1);
+  if(has_midi && midi.probe && usbMIDI.getType() == 1) {  // Type 1: note on
+    midi.t = time_us;
+    midi.count++;
+    midi.probe = false;
+    led_state = !led_state;
   }
 
   // Probe screen
@@ -385,6 +434,16 @@ void loop() {
   }
 
   // Check if we got incoming commands from the host
+  if (has_midi) {
+    if (usbMIDI.getType() == MIDI_MODE_TYPE) {
+      short program = usbMIDI.getData1();
+      serial_over_midi = (program == 1);
+      send_buffer = (serial_over_midi ? MIDI_SYSEX_BEGIN : String());
+    } else if (usbMIDI.getType() == MIDI_COMMAND_TYPE) {
+      char cmd = usbMIDI.getData1();
+      process_command(cmd);
+    }
+  }
   if (Serial.available()) {
     char cmd = Serial.read();
     process_command(cmd);

Add Serial-over-MIDI communication channel.